1<?php 2// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project 3// 4// All Rights Reserved. See copyright.txt for details and a complete list of authors. 5// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details. 6// $Id$ 7 8use Symfony\Component\Yaml\Yaml; 9 10//this script may only be included - so its better to die if called directly. 11if (strpos($_SERVER["SCRIPT_NAME"], basename(__FILE__)) !== false) { 12 header("Location: ../index.php"); 13 die; 14} 15 16 17/** 18 * TikiAccessLib 19 * 20 * @uses TikiLib 21 * 22 */ 23class TikiAccessLib extends TikiLib 24{ 25 private $noRedirect = false; 26 private $noDisplayError = false; 27 //used in CSRF protection methods 28 private $ticket; 29 private $ticketMatch; 30 private $originMatch; 31 private $base; 32 private $origin; 33 private $originSource; 34 private $logMsg = ''; 35 private $userMsg = ''; 36 37 function preventRedirect($prevent) 38 { 39 $this->noRedirect = (bool) $prevent; 40 } 41 42 /** 43 * Prevent the display of errors 44 * useful during plugin parsing to mute error redirects 45 * 46 * @param bool $prevent 47 */ 48 function preventDisplayError($prevent) 49 { 50 $this->noDisplayError = (bool) $prevent; 51 } 52 53 /** 54 * check that the user is admin or has admin permissions 55 * 56 */ 57 function check_admin($user, $feature_name = '') 58 { 59 global $tiki_p_admin, $prefs; 60 require_once('tiki-setup.php'); 61 // first check that user is logged in 62 $this->check_user($user); 63 64 if (($user != 'admin') && ($tiki_p_admin != 'y')) { 65 $msg = tra("You do not have the permission that is needed to use this feature"); 66 if ($feature_name) { 67 $msg = $msg . ": " . $feature_name; 68 } 69 $this->display_error('', $msg, '403'); 70 } 71 } 72 73 /** 74 * @param $user 75 */ 76 function check_user($user) 77 { 78 global $prefs; 79 require_once('tiki-setup.php'); 80 81 if (! $user) { 82 $title = tra("You are not logged in"); 83 $this->display_error('', $title, '403'); 84 } 85 } 86 87 /** 88 * @param string $user 89 * @param array $features 90 * @param array $permissions 91 * @param string $permission_name 92 */ 93 function check_page($user = 'y', $features = [], $permissions = [], $permission_name = '') 94 { 95 require_once('tiki-setup.php'); 96 97 if ($features) { 98 $this->check_feature($features); 99 } 100 $this->check_user($user); 101 102 if ($permissions) { 103 $this->check_permission($permissions, $permission_name); 104 } 105 } 106 107 /** 108 * check_feature: Checks if a feature or a list of features are activated 109 * 110 * @param string or array $features If just a string, this method will only test that one. If an array, all features will be tested 111 * @param string $feature_name Name that will be printed on the error screen 112 * @param string $relevant_admin_panel Admin panel where the feature can be set to 'Y'. This link is provided on the error screen 113 * @access public 114 * @return void 115 * 116 */ 117 function check_feature($features, $feature_name = '', $relevant_admin_panel = 'features', $either = false) 118 { 119 global $prefs; 120 require_once('tiki-setup.php'); 121 122 $perms = Perms::get(); 123 124 if ($perms->admin && isset($_REQUEST['check_feature']) && isset($_REQUEST['lm_preference'])) { 125 $prefslib = TikiLib::lib('prefs'); 126 $prefslib->applyChanges((array) $_REQUEST['lm_preference'], $_REQUEST); 127 } 128 129 if (! is_array($features)) { 130 $features = [$features]; 131 } 132 133 if ($either) { 134 // if anyone will do, start assuming no go and test for feature 135 $allowed = false; 136 } else { 137 // if all is needed, start assuming it's a go and test for feature not on 138 $allowed = true; 139 } 140 141 foreach ($features as $feature) { 142 if (! $either && $prefs[$feature] != 'y') { 143 if ($feature_name != '') { 144 $feature = $feature_name; 145 } 146 $allowed = false; 147 break; 148 } elseif ($either && $prefs[$feature] == 'y') { 149 // test for feature in "anyone will do" case 150 $allowed = true; 151 break; 152 } 153 } 154 155 if (! $allowed) { 156 $smarty = TikiLib::lib('smarty'); 157 158 if ($perms->admin) { 159 $smarty->assign('required_preferences', $features); 160 } 161 162 $msg = tr( 163 'Required features: <b>%0</b>. If you do not have permission to activate these features, ask the site administrator.', 164 implode(', ', $features) 165 ); 166 167 $this->display_error('', $msg, 'no_redirect_login'); 168 } 169 } 170 171 /** 172 * Check permissions for current user and display an error if not granted 173 * Multiple perms can be checked at once using an array and all those perms need to be granted to continue 174 * 175 * @param string|array $permissions permission name or names (can be old style e.g. 'tiki_p_view' or just 'view') 176 * @param string $permission_name text used in warning if perm not granted 177 * @param bool|string $objectType optional object type (e.g. 'wiki page') 178 * @param bool|string $objectId optional object id (e.g. 'HomePage' or '42' depending on object type) 179 */ 180 function check_permission($permissions, $permission_name = '', $objectType = false, $objectId = false) 181 { 182 require_once('tiki-setup.php'); 183 184 if (! is_array($permissions)) { 185 $permissions = [$permissions]; 186 } 187 188 foreach ($permissions as $permission) { 189 if (false !== $objectType) { 190 $applicable = Perms::get($objectType, $objectId); 191 } else { 192 $applicable = Perms::get(); 193 } 194 195 if ($applicable->$permission) { 196 continue; 197 } 198 199 if ($permission_name) { 200 $permission = $permission_name; 201 } 202 $this->display_error('', tra("You do not have the permission that is needed to use this feature:") . " " . $permission, '403', false); 203 if (empty($GLOBALS['user']) && empty($_SESSION['loginfrom'])) { 204 $_SESSION['loginfrom'] = $_SERVER['REQUEST_URI']; 205 } 206 } 207 } 208 209 /** 210 * Check permissions for current user and display an error if not granted 211 * Multiple perms can be checked at once using an array and ANY ONE OF those perms only needs to be granted to continue 212 * 213 * NOTE that you do NOT have to use this to include admin perms, as admin perms automatically inherit the perms they are admin of 214 * 215 * @param string|array $permissions permission name or names (can be old style e.g. 'tiki_p_view' or just 'view') 216 * @param string $permission_name text used in warning if perm not granted 217 * @param bool|string $objectType optional object type (e.g. 'wiki page') 218 * @param bool|string $objectId optional object id (e.g. 'HomePage' or '42' depending on object type) 219 */ 220 function check_permission_either($permissions, $permission_name = '', $objectType = false, $objectId = false) 221 { 222 require_once('tiki-setup.php'); 223 $allowed = false; 224 225 if (! is_array($permissions)) { 226 $permissions = [$permissions]; 227 } 228 229 foreach ($permissions as $permission) { 230 if (false !== $objectType) { 231 $applicable = Perms::get($objectType, $objectId); 232 } else { 233 $applicable = Perms::get(); 234 } 235 236 if ($applicable->$permission) { 237 $allowed = true; 238 break; 239 } 240 } 241 242 if (! $allowed) { 243 if ($permission_name) { 244 $permission = $permission_name; 245 } else { 246 $permission = implode(', ', $permissions); 247 } 248 249 $this->display_error('', tra("You do not have the permission that is needed to use this feature") . ": " . $permission, '403', false); 250 } 251 } 252 253 /** 254 * check permission, where the permission is normally unset 255 * 256 */ 257 function check_permission_unset($permissions, $permission_name) 258 { 259 require_once('tiki-setup.php'); 260 261 foreach ($permissions as $permission) { 262 global $$permission; 263 if ((isset($$permission) && $$permission == 'n')) { 264 if ($permission_name) { 265 $permission = $permission_name; 266 } 267 $this->display_error('', tra("You do not have the permission that is needed to use this feature") . ": " . $permission, '403', false); 268 } 269 } 270 } 271 272 /** 273 * check page exists 274 * 275 */ 276 function check_page_exists($page) 277 { 278 require_once('tiki-setup.php'); 279 if (! $this->page_exists($page)) { 280 $this->display_error($page, tra("Page cannot be found"), '404'); 281 } 282 } 283 284 /** 285 * Return default security timeout period in seconds. 286 * Used in setting the global securityTimeout preference used to determine the expiry period for state-changing 287 * forms and related CSRF ticket. Add the timeout class to the submit element of the form to subject a form to 288 * the expiration period. 289 * @return mixed 290 */ 291 public function getDefaultTimeout() 292 { 293 global $prefs; 294 $timeSetting = isset($prefs['session_lifetime']) && $prefs['session_lifetime'] > 0 ? $prefs['session_lifetime'] * 60 295 : ini_get('session.gc_maxlifetime'); 296 return ! empty($timeSetting) ? min(4 * 60 * 60, $timeSetting) : 4 * 60 * 60; //4 hours max 297 } 298 299 /** 300 * CSRF protection - set the ticket on the server and as a smarty template variable 301 * 302 * Called by the smarty function {ticket}, which should be placed in all forms with actions that change the 303 * database 304 * @throws Exception 305 */ 306 public function setTicket() 307 { 308 $this->ticket = TikiLib::lib('tiki')->generate_unique_sequence(32, true); 309 $_SESSION['tickets'][$this->ticket] = time(); 310 Tikilib::lib('smarty')->assign('ticket', $this->ticket); 311 } 312 313 /** 314 * CSRF protection - This method performs two checks: that the origin matches the tiki site and that the ticket 315 * is valid (i.e., the ticket submitted in the form matches a ticket on the server that has not expired). 316 * 317 * This method does not stop execution upon failure of one of the checks, instead it returns false, so the 318 * executing code should be made conditional on the result of this method 319 * 320 * Typically called at the point of determining whether to perform a state-changing action that does not require 321 * confirmation, for example: 322 * if ($_POST['create'] && $access->checkCsrf()) { 323 * //create item here 324 * } 325 * 326 * Call after checking the $_POST variable otherwise other $_GET requests will throw errors. 327 * The related submit element (usually in a smarty template) should use the checkTimeout() onclick function 328 * 329 * @param string $error Options include 'session', 'none', 'services' and 'page'. Used in csrfError() 330 * @param bool $unsetTicket Whether to unset $_SESSION ticket after checking. Normally, should unset, 331 * however infrequently it is easier to use a ticket more than once. 332 * Other code should unset the ticket after the multiple uses are complete and ensure 333 * repeated use does not create a vulnerability 334 * 335 * @param string $ticket Ticket may be provided, e.g., in cases where it is used more than once and is 336 * stored in a session variable rather than being part of the $_POST. Only tickets 337 * that originated in a $_POST should be used. 338 * 339 * @return bool True if CSRF check passed, false otherwise 340 * @throws Services_Exception 341 * @see csrfError() for further details on error types and uses. 342 */ 343 public function checkCsrf($error = 'session', $unsetTicket = true, $ticket = '') 344 { 345 global $prefs; 346 347 if ($prefs['pwa_feature'] == 'y') { 348 return true; 349 } else { 350 if ($this->isActionPost()) { 351 if ($this->csrfResult()) { 352 return true; 353 } 354 $this->originCheck(); 355 $this->ticketCheck($unsetTicket, $ticket); 356 if ($this->csrfResult()) { 357 return true; 358 } else { 359 $this->csrfError($error); 360 return false; 361 } 362 } else { 363 $msg = ' ' . tra('CSRF check not performed.'); 364 $this->logMsg = $msg; 365 $this->userMsg = $msg; 366 $this->csrfError($error); 367 return false; 368 } 369 } 370 } 371 372 /** 373 * Similar to above but a confirmation form for the user to acknowledge the action is shown first. Once the 374 * confirmation form is submitted then this function will perform the origin and ticket checks. 375 * Used when a confirmation from the user is desired before performing the action. Typically, this is for 376 * state-changing actions that cannot be undone (like delete). 377 * 378 * Also, any state-changing action that can be triggered from a link should be conditioned on this function so 379 * that the action is only performed after the confirmation form is posted. 380 * 381 * The related submit element (usually in a smarty template) should use the confirmSimple() onclick function which 382 * will generate the confirmation form in a popup if javascript is enabled. If javascript is not enabled, this 383 * function will redirect to a confirmation page. 384 * 385 * @param string $confirmText Optional confirmation text. Default is "Confirm action" 386 * @param string $error Options include 'session', 'none', 'services' and 'page'. Used in csrfError() 387 * @return bool Returns true if both checks match, false if either fails 388 * @throws Exception 389 * @throws Services_Exception 390 * @see csrfError() for further details on error types and uses. 391 */ 392 public function checkCsrfForm($confirmText = '', $error = 'session') 393 { 394 if (empty($_POST['confirmForm']) || $_POST['confirmForm'] !== 'y') { 395 if ($this->checkOrigin()) { 396 $this->confirmRedirect($confirmText, $error); 397 } else { 398 $this->csrfError($error); 399 return false; 400 } 401 } else { 402 return $this->checkCsrf($error); 403 } 404 } 405 406 /** 407 * CSRF protection - Perform origin check to ensure the requesting server matches this server 408 * 409 * Sets the originMatch property to true or false depending on the result of the check 410 * 411 * @return void 412 */ 413 private function originCheck() 414 { 415 // $base_url is usually host + directory 416 global $base_url; 417 include_once('lib/setup/absolute_urls.php'); 418 $this->origin = ''; 419 $this->originSource = 'empty'; 420 //first check HTTP_ORIGIN 421 if (! empty($_SERVER['HTTP_ORIGIN'])) { 422 //HTTP_ORIGIN is usually host only without trailing slash 423 $this->origin = $_SERVER['HTTP_ORIGIN']; 424 $this->originSource = 'HTTP_ORIGIN'; 425 //then check HTTP_REFERER 426 } elseif (! empty($_SERVER['HTTP_REFERER'])) { 427 //HTTP_REFERER is usually the full path (host + directory + file + query) 428 $this->origin = $_SERVER['HTTP_REFERER']; 429 $this->originSource = 'HTTP_REFERER'; 430 } 431 //identify server host + port 432 $base = parse_url($base_url); 433 $baseHost = isset($base['host']) ? $base['host'] : ''; 434 $basePort = isset($base['port']) ? ':' . $base['port'] : ''; 435 $this->base = $baseHost . $basePort; 436 //identify requesting host + port 437 $origin = parse_url($this->origin); 438 $originHost = isset($origin['host']) ? $origin['host'] : ''; 439 $originPort = isset($origin['port']) ? ':' . $origin['port'] : ''; 440 $this->origin = $originHost . $originPort; 441 //perform compare 442 $this->originMatch = $this->base === $this->origin; 443 //error message 444 if (! $this->originMatch()) { 445 if ($this->originSource === 'empty') { 446 $this->userMsg .= ' ' . tra('Required headers are missing.'); 447 $this->logMsg .= ' ' . tr( 448 'Requesting site could not be identified because %0 and %1 were empty.', 449 'HTTP_ORIGIN', 450 'HTTP_REFERER' 451 ); 452 } else { 453 $this->userMsg .= ' ' . tra('Request needs to originate from this site.'); 454 $this->logMsg .= ' ' . tr( 455 'The %0 host (%1) does not match this server (%2).', 456 $this->originSource, 457 $this->origin, 458 $this->base 459 ); 460 } 461 } 462 } 463 464 /** 465 * CSRF protection - Perform ticket check to ensure ticket in the $_POST variable matches the one stored on the 466 * server and that the ticket has not expired. 467 * 468 * Sets the ticketMatch property to true or false depending on the result of the check 469 * 470 * @param bool $unsetTicket Whether to unset $_SESSION ticket after checking. Normally, should unset, 471 * however infrequently it is easier to use a ticket more than once. 472 * Other code should unset the ticket after the multiple uses are complete and ensure 473 * repeated use does not create a vulnerability 474 * 475 * @param string $ticket Ticket may be provided, e.g., in cases where it is used more than once and is 476 * stored in a session variable rather than being part of the $_POST. Only tickets 477 * that originated in a $_POST should be used. 478 */ 479 private function ticketCheck($unsetTicket, $ticket) 480 { 481 if (! empty($ticket)) { 482 $this->ticket = $ticket; 483 } elseif (! empty($_POST['ticket'])) { 484 $this->ticket = $_POST['ticket']; 485 } else { 486 $this->ticket = false; 487 } 488 //just in case url decoding is needed 489 if (strpos($this->ticket, '%') !== false) { 490 $this->ticket = urldecode($this->ticket); 491 } 492 //check that request ticket matches server ticket 493 if ($this->ticket && !empty($_SESSION['tickets'][$this->ticket])) { 494 //check that ticket has not expired 495 $ticketTime = $_SESSION['tickets'][$this->ticket]; 496 global $prefs; 497 $maxTime = $prefs['site_security_timeout']; 498 if ($ticketTime <= time() && $ticketTime > (time() - $maxTime)) { 499 $this->ticketMatch = true; 500 } else { 501 //ticket is expired 502 $this->userMsg = ' ' . tra('Ticket has expired. Reload the page.'); 503 $this->logMsg = ' ' . tra('Ticket matches but is expired.'); 504 $this->ticketMatch = false; 505 } 506 if ($unsetTicket) { 507 unset($_SESSION['tickets'][$this->ticket]); 508 } 509 } else { 510 //ticket doesn't match or is missing 511 $this->userMsg = ' ' . tra('Reloading the page may help.'); 512 $this->logMsg = ' ' . tra('Ticket does not match or is missing.'); 513 $this->ticketMatch = false; 514 } 515 } 516 517 /** 518 * Check http origin/referer and provide error feedback if it doesn't match the site domain 519 * Differs from checkCsrf() in that only the origin/referer is checked, not a ticket 520 * 521 * @param string $error Options include 'session', 'none', 'services' and 'page'. Used in csrfError() 522 * @return bool Returns true if origin check matches, false if not 523 * @throws Exception 524 * @throws Services_Exception 525 * @see csrfError() for further details on error types and uses. 526 * @see crsfCheck() crsfCheck() or crsfCheckForm() should be used under most typial conditions. 527 */ 528 public function checkOrigin($error = 'session') 529 { 530 $this->originCheck(); 531 if ($this->originMatch()) { 532 return true; 533 } else { 534 $this->csrfError($error); 535 return false; 536 } 537 } 538 539 /** 540 * Check CSRF ticket and provide error feedback if it doesn't match the site domain 541 * Differs from checkCsrf() in that only the ticket is checked, not the origin/referer 542 * 543 * @param string $error Options include 'session', 'none', 'services' and 'page'. Used in csrfError() 544 * @param bool $unsetTicket Whether to unset $_SESSION ticket after checking. Normally, should unset, 545 * however infrequently it is easier to use a ticket more than once. 546 * Other code should unset the ticket after the multiple uses are complete and ensure 547 * repeated use does not create a vulnerability 548 * 549 * @param string $ticket Ticket may be provided, e.g., in cases where it is used more than once and is 550 * stored in a session variable rather than being part of the $_POST. Only tickets 551 * that originated in a $_POST should be used. 552 * 553 * @return bool Returns true if origin check matches, false if not 554 * @throws Services_Exception 555 * @see csrfError() for further details on error types and uses. 556 */ 557 public function checkTicket($error = 'session', $unsetTicket = true, $ticket = '') 558 { 559 $this->ticketCheck($unsetTicket, $ticket); 560 if ($this->ticketMatch()) { 561 return true; 562 } else { 563 $this->csrfError($error); 564 return false; 565 } 566 } 567 568 /** 569 * Generate tiki log entry and user feedback for CSRF errors 570 * @param string $error * 'session' The regular way of providing feedback (the anti-csrf error message) using the standard Feedback class. 571 * * 'services' Used to provide feedback for ajax services. 572 * * 'page' Used when the error needs to be shown on a separate page (redirects to a 400 error page). 573 * * 'none' Any errors are not displayed 574 * @throws Exception 575 * @throws Services_Exception 576 */ 577 private function csrfError($error = 'session') 578 { 579 if ($error !== 'none') { 580 $this->userMsg = tra('Potential cross-site request forgery (CSRF) detected. Operation blocked.') 581 . $this->userMsg; 582 $this->logMsg = tr('Request to %0 failed CSRF check.', $_SERVER['SCRIPT_NAME']) 583 . $this->logMsg; 584 //log message 585 TikiLib::lib('logs')->add_log('CSRF', $this->logMsg); 586 //user feedback 587 switch ($error) { 588 case 'services': 589 throw new Services_Exception($this->userMsg, 401); 590 break; 591 case 'page': 592 Feedback::errorPage(['mes' => $this->userMsg, 'errortype' => 401]); 593 break; 594 case 'session': 595 default: 596 Feedback::error($this->userMsg); 597 break; 598 } 599 } 600 } 601 602 /** 603 * CSRF ticket - Check that the ticket has been created 604 * 605 * @return bool Returns true if ticket has been set, false if not 606 */ 607 public function ticketSet() 608 { 609 return ! empty($this->ticket); 610 } 611 612 /** 613 * CSRF ticket - Check that the ticket has been matched to the previous ticket set 614 * 615 * @return bool Returns true if the request ticket matches the server ticket and is not expired, false if not 616 */ 617 public function ticketMatch() 618 { 619 return $this->ticketMatch === true; 620 } 621 622 /** 623 * CSRF origin check - Check that origin matches the server 624 * 625 * @return bool Returns true if the request origin matches the origin of the server, false if not 626 */ 627 private function originMatch() 628 { 629 return $this->originMatch === true; 630 } 631 632 /** 633 * Check that the request method is POST 634 * 635 * @return bool Returns true if the request method is POST, false if not 636 */ 637 function requestIsPost() 638 { 639 return $_SERVER['REQUEST_METHOD'] === 'POST'; 640 } 641 642 /** 643 * CSRF ticket - Return results of ticket and origin match 644 * 645 * @return bool Returns true if both matches were successful, false if not 646 */ 647 public function csrfResult() 648 { 649 return $this->originMatch() && $this->ticketMatch(); 650 } 651 652 /** 653 * CSRF ticket - Get the ticket 654 * 655 * @return mixed Returns the ticket if set, false if not 656 */ 657 public function getTicket() 658 { 659 if (! empty($this->ticket)) { 660 return $this->ticket; 661 } else { 662 return false; 663 } 664 } 665 666 /** 667 * Check that the request is POST and includes a ticket 668 * 669 * @return bool Returns true if the request is post and the request includes a ticket, false if not 670 */ 671 public function isActionPost() 672 { 673 return ($this->requestIsPost() && !empty($_POST['ticket'])); 674 } 675 676 /** 677 * Utility method for checkCsrfForm and also used in infrequent cases where a database-changing action is initiated 678 * through an outside link, for example an unsubscribe link, in which case an additional validation method should 679 * also be applied 680 * 681 * @param string $confirmText The confirm question posed to the user. 682 * @param string $error Options include 'session', 'none', 'services' and 'page'. Used in csrfError() 683 * 684 * @return bool True if conformation was accepted, false otherwise 685 * @throws Services_Exception 686 * @see csrfError() for further details on error types and uses. 687 */ 688 public function confirmRedirect($confirmText, $error = 'session') 689 { 690 if (empty($_POST['confirmForm']) || $_POST['confirmForm'] !== 'y') { 691 if (empty($confirmText)) { 692 $confirmText = tr('Confirm action'); 693 } 694 // Display the confirmation in the main tiki.tpl template 695 $smarty = TikiLib::lib('smarty'); 696 if (empty($smarty->getTemplateVars('confirmaction'))) { 697 $smarty->assign('confirmaction', $_SERVER['PHP_SELF']); 698 } 699 $smarty->assign('post', $_REQUEST); 700 $smarty->assign('print_page', 'n'); 701 $smarty->assign('title', tra('Please confirm action')); 702 $smarty->assign('confirmation_text', $confirmText); 703 $smarty->assign('mid', 'confirm.tpl'); 704 $smarty->display('tiki.tpl'); 705 die(); 706 } else { 707 return $this->checkCsrf($error); 708 } 709 } 710 711 712 /** 713 * ***** Note: Being replaced by checkCsrfForm method above ************* 714 * 715 * Similar method as checkCsrfForm() 716 * 717 * @param string $confirmation_text Custom text to use if a confirmation page is brought up first 718 * @param bool $returnHtml Set to false to not use the standard confirmation page and to not use the 719 * standard error page. Suitable for popup confirmations when set to false. 720 * @param bool $errorMsg Set to true to have the Feedback error message sent automatically 721 * @return array|bool 722 * @throws Exception 723 * @throws Services_Exception 724 * @deprecated replaced by checkCsrfForm() and checkCsrf() 725 * @see checkCsrfForm() For post/get validation with conformation check 726 * @see checkCsrf() For post validation with no conformation check 727 */ 728 function check_authenticity($confirmation_text = '', $returnHtml = true, $errorMsg = false) 729 { 730 $check = true; 731 if (empty($_POST['confirmForm']) || $_POST['confirmForm'] !== 'y') { 732 if ($this->checkOrigin()) { 733 if ($returnHtml) { 734 //redirect to a confirmation page 735 if (empty($confirmation_text)) { 736 $confirmation_text = tra('Confirm action'); 737 } 738 if (empty($confirmaction)) { 739 $confirmaction = $_SERVER['PHP_SELF']; 740 } 741 // Display the confirmation in the main tiki.tpl template 742 $smarty = TikiLib::lib('smarty'); 743 $smarty->assign('post', $_REQUEST); 744 $smarty->assign('print_page', 'n'); 745 $smarty->assign('confirmation_text', $confirmation_text); 746 $smarty->assign('confirmaction', $confirmaction); 747 $smarty->assign('mid', 'confirm.tpl'); 748 $smarty->display('tiki.tpl'); 749 die(); 750 } else { 751 //return ticket to be placed in a form with other code 752 return ['ticket' => $this->ticket]; 753 } 754 } else { 755 $check = false; 756 } 757 } elseif (!empty($_POST['confirmForm']) && $_POST['confirmForm'] === 'y') { 758 $check = $this->checkCsrf(); 759 } 760 if (! $check) { 761 if ($returnHtml) { 762 $smarty = TikiLib::lib('smarty'); 763 $smarty->display('error.tpl'); 764 exit(); 765 } else { 766 return false; 767 } 768 } 769 } 770 771 /** 772 * @param $page 773 * @param string $errortitle 774 * @param string $errortype 775 * @param bool $enableRedirect 776 * @param string $message 777 * @throws Exception 778 */ 779 function display_error($page, $errortitle = "", $errortype = "", $enableRedirect = true, $message = '') 780 { 781 if ($this->noDisplayError) { 782 return; 783 } 784 785 global $prefs, $tikiroot, $user; 786 require_once('tiki-setup.php'); 787 $userlib = TikiLib::lib('user'); 788 $smarty = TikiLib::lib('smarty'); 789 790 // Don't redirect when calls are made for web services 791 if ($enableRedirect && $prefs['feature_redirect_on_error'] == 'y' && ! $this->is_machine_request() 792 && $tikiroot . $prefs['tikiIndex'] != $_SERVER['PHP_SELF'] 793 && ( $page != $userlib->get_user_default_homepage($user) || $page === '' ) ) { 794 $this->redirect($prefs['tikiIndex']); 795 } 796 797 $detail = [ 798 'code' => $errortype, 799 'errortitle' => $errortitle, 800 'message' => $message, 801 ]; 802 803 if (! isset($errortitle)) { 804 $detail['errortitle'] = tra('unknown error'); 805 } 806 807 if (empty($message)) { 808 $detail['message'] = $detail['errortitle']; 809 } 810 811 // Display the template 812 switch ($errortype) { 813 case '404': 814 header("HTTP/1.0 404 Not Found"); 815 $detail['page'] = $page; 816 $detail['message'] .= ' (404)'; 817 break; 818 819 case '403': 820 header("HTTP/1.0 403 Forbidden"); 821 break; 822 823 case '503': 824 header("HTTP/1.0 503 Service Unavailable"); 825 break; 826 827 default: 828 $errortype = (int) $errortype; 829 $title = strip_tags($detail['errortitle']); 830 831 if (! $errortype) { 832 $errortype = 403; 833 $title = 'Forbidden'; 834 } 835 header("HTTP/1.0 $errortype $title"); 836 break; 837 } 838 839 if ($this->is_serializable_request()) { 840 Feedback::error($errortitle, true); 841 842 $this->output_serialized($detail); 843 } elseif ($this->is_xml_http_request()) { 844 $smarty->assign('detail', $detail); 845 $smarty->display('error-ajax.tpl'); 846 } else { 847 if (($errortype == 401 || $errortype == 403) && 848 empty($user) && 849 ($prefs['permission_denied_login_box'] == 'y' || ! empty($prefs['permission_denied_url'])) 850 ) { 851 $_SESSION['loginfrom'] = $_SERVER['REQUEST_URI']; 852 if ($prefs['login_autologin'] == 'y' && $prefs['login_autologin_redirectlogin'] == 'y' && ! empty($prefs['login_autologin_redirectlogin_url'])) { 853 $this->redirect($prefs['login_autologin_redirectlogin_url']); 854 } 855 } 856 857 $smarty->assign('errortitle', $detail['errortitle']); 858 $smarty->assign('msg', $detail['message']); 859 $smarty->assign('errortype', $detail['code']); 860 if (isset($detail['page'])) { 861 $smarty->assign('page', $page); 862 } 863 $smarty->display("error.tpl"); 864 } 865 die; 866 } 867 868 /** 869 * @param string $page 870 * @return string 871 */ 872 function get_home_page($page = '') 873 { 874 global $prefs, $use_best_language, $user; 875 $userlib = TikiLib::lib('user'); 876 $tikilib = TikiLib::lib('tiki'); 877 878 if (! isset($page) || $page == '') { 879 if ($prefs['useGroupHome'] == 'y') { 880 $groupHome = $userlib->get_user_default_homepage($user); 881 if ($groupHome) { 882 $page = $groupHome; 883 } else { 884 $page = $prefs['wikiHomePage']; 885 } 886 } else { 887 $page = $prefs['wikiHomePage']; 888 } 889 if (! $tikilib->page_exists($prefs['wikiHomePage'])) { 890 $tikilib->create_page($prefs['wikiHomePage'], 0, '', $this->now, 'Tiki initialization'); 891 } 892 if ($prefs['feature_best_language'] == 'y') { 893 $use_best_language = true; 894 } 895 } 896 return $page; 897 } 898 899 /** 900 * Returns an absolute URL for the given one 901 * 902 * Inspired on \ZendOpenId\OpenId::absoluteUrl 903 * 904 * @param string $url absolute or relative URL 905 * @return string 906 */ 907 public static function absoluteUrl($url) 908 { 909 if (empty($url)) { 910 return self::selfUrl(); 911 } elseif (! preg_match('|^([^:]+)://|', $url)) { 912 if (preg_match('|^([^:]+)://([^:@]*(?:[:][^@]*)?@)?([^/:@?#]*)(?:[:]([^/?#]*))?(/[^?]*)?((?:[?](?:[^#]*))?(?:#.*)?)$|', self::selfUrl(), $reg)) { 913 $scheme = $reg[1]; 914 $auth = $reg[2]; 915 $host = $reg[3]; 916 $port = $reg[4]; 917 $path = $reg[5]; 918 $query = $reg[6]; 919 if ($url[0] == '/') { 920 return $scheme 921 . '://' 922 . $auth 923 . $host 924 . (empty($port) ? '' : (':' . $port)) 925 . $url; 926 } else { 927 $dir = dirname($path); 928 return $scheme 929 . '://' 930 . $auth 931 . $host 932 . (empty($port) ? '' : (':' . $port)) 933 . (strlen($dir) > 1 ? $dir : '') 934 . '/' 935 . $url; 936 } 937 } 938 } 939 return $url; 940 } 941 942 /** 943 * Returns a full URL that was requested on current HTTP request. 944 * 945 * Inspired on \ZendOpenId\OpenId::selfUrl 946 * 947 * @return string 948 */ 949 public static function selfUrl() 950 { 951 $url = ''; 952 $port = ''; 953 954 if (isset($_SERVER['HTTP_HOST'])) { 955 if (($pos = strpos($_SERVER['HTTP_HOST'], ':')) === false) { 956 if (isset($_SERVER['SERVER_PORT'])) { 957 $port = ':' . $_SERVER['SERVER_PORT']; 958 } 959 $url = $_SERVER['HTTP_HOST']; 960 } else { 961 $url = substr($_SERVER['HTTP_HOST'], 0, $pos); 962 $port = substr($_SERVER['HTTP_HOST'], $pos); 963 } 964 } elseif (isset($_SERVER['SERVER_NAME'])) { 965 $url = $_SERVER['SERVER_NAME']; 966 if (isset($_SERVER['SERVER_PORT'])) { 967 $port = ':' . $_SERVER['SERVER_PORT']; 968 } 969 } 970 971 if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') { 972 $url = 'https://' . $url; 973 if ($port == ':443') { 974 $port = ''; 975 } 976 } else { 977 $url = 'http://' . $url; 978 if ($port == ':80') { 979 $port = ''; 980 } 981 } 982 983 $url .= $port; 984 if (isset($_SERVER['HTTP_X_REWRITE_URL'])) { 985 $url .= $_SERVER['HTTP_X_REWRITE_URL']; 986 } elseif (isset($_SERVER['REQUEST_URI'])) { 987 $url .= $_SERVER['REQUEST_URI']; 988 } elseif (isset($_SERVER['SCRIPT_URL'])) { 989 $url .= $_SERVER['SCRIPT_URL']; 990 } elseif (isset($_SERVER['REDIRECT_URL'])) { 991 $url .= $_SERVER['REDIRECT_URL']; 992 } elseif (isset($_SERVER['PHP_SELF'])) { 993 $url .= $_SERVER['PHP_SELF']; 994 } elseif (isset($_SERVER['SCRIPT_NAME'])) { 995 $url .= $_SERVER['SCRIPT_NAME']; 996 if (isset($_SERVER['PATH_INFO'])) { 997 $url .= $_SERVER['PATH_INFO']; 998 } 999 } 1000 return $url; 1001 } 1002 1003 /** 1004 * Utility function redirect the browser location to another url 1005 1006 * @param string $url The target web address 1007 * @param string $msg An optional message to display 1008 * @param int $code HTTP code 1009 * @param string $msgtype Type of message which determines styling (e.g., success, error, warning, etc.) 1010 */ 1011 function redirect($url = '', $msg = '', $code = 302, $msgtype = '') 1012 { 1013 global $prefs; 1014 1015 if ($this->noRedirect) { 1016 return; 1017 } 1018 1019 // TODO: Validate URL 1020 if ($url == '') { 1021 $url = $prefs['tikiIndex']; 1022 } 1023 1024 if (trim($msg)) { 1025 $session = session_id(); 1026 if (empty($session)) { 1027 // Can happen if session_silent is enabled. But does any instance enable session_silent? 1028 // Removing this case would allow removing the $msg parameters and just have callers using Feedback::add() before calling redirect(). Chealer 2017-08-16 1029 $start = strpos($url, '?') ? '&' : '?'; 1030 $url = $start . 'msg=' . urlencode($msg) . '&msgtype=' . urlencode($msgtype); 1031 } else { 1032 $_SESSION['msg'] = $msg; 1033 $_SESSION['msgtype'] = $msgtype; 1034 } 1035 } 1036 1037 TikiLib::events()->trigger('tiki.process.redirect'); 1038 1039 session_write_close(); 1040 if (headers_sent()) { 1041 echo "<script>document.location.href='" . smarty_modifier_escape($url, 'javascript') . "';</script>\n"; 1042 } else { 1043 @ob_end_clean(); // clear output buffer 1044 if ($prefs['feature_obzip'] == 'y') { 1045 @ob_start('ob_gzhandler'); 1046 } 1047 header("HTTP/1.0 $code Found"); 1048 header("Location: $url"); 1049 } 1050 exit(); 1051 } 1052 1053 /** 1054 * @param $message 1055 */ 1056 function flash($message) 1057 { 1058 $this->redirect($_SERVER['REQUEST_URI'], $message); 1059 } 1060 1061 /** 1062 * Authorizes access to Tiki RSS feeds via user/password embedded in a URL 1063 * e.g. https://joe:secret@localhost/tiki/tiki-calendars_rss.php?ver=2 1064 * ~~~~~~~~~~ 1065 * 1066 * @param array the permissions that needs to be checked against (e.g. tiki_p_view) 1067 * 1068 * @return null if authorized, otherwise an array(msg,header) 1069 * where msg can be displayed, and header decides whether to 1070 * send 401 Unauthorized headers. 1071 */ 1072 1073 function authorize_rss($rssrights) 1074 { 1075 global $user, $prefs; 1076 $userlib = TikiLib::lib('user'); 1077 $tikilib = TikiLib::lib('tiki'); 1078 $smarty = TikiLib::lib('smarty'); 1079 $perms = Perms::get(); 1080 $result = ['msg' => tra("You do not have permission to view this section"), 'header' => 'n']; 1081 1082 // if current user has appropriate rights, allow. 1083 foreach ($rssrights as $perm) { 1084 if ($perms->$perm) { 1085 return; 1086 } 1087 } 1088 1089 // deny if no basic auth allowed. 1090 if ($prefs['feed_basic_auth'] != 'y') { 1091 return $result; 1092 } 1093 1094 //login is needed to access the contents 1095 $https_mode = isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) == 'on'; 1096 1097 //refuse to authenticate in plaintext if https_login_required. 1098 if ($prefs['https_login_required'] == 'y' && ! $https_mode) { 1099 $result['msg'] = tra("For the security of your password, direct access to the feed is only available via HTTPS"); 1100 return $result; 1101 } 1102 1103 if ($this->http_auth()) { 1104 $perms = Perms::get(); 1105 foreach ($rssrights as $perm) { 1106 if ($perms->$perm) { 1107 // if user/password and the appropriate rights are correct, allow. 1108 return; 1109 } 1110 } 1111 } 1112 1113 return $result; 1114 } 1115 1116 /** 1117 * @return bool 1118 */ 1119 function http_auth() 1120 { 1121 global $tikidomain, $user; 1122 $userlib = TikiLib::lib('user'); 1123 $smarty = TikiLib::lib('smarty'); 1124 1125 if (! $tikidomain) { 1126 $tikidomain = "Default"; 1127 } 1128 1129 if (! isset($_SERVER['PHP_AUTH_USER'])) { 1130 header('WWW-Authenticate: Basic realm="' . $tikidomain . '"'); 1131 header('HTTP/1.0 401 Unauthorized'); 1132 exit; 1133 } 1134 1135 $attempt = $_SERVER['PHP_AUTH_USER'] ; 1136 $pass = $_SERVER['PHP_AUTH_PW'] ; 1137 list($res, $rest) = $userlib->validate_user_tiki($attempt, $pass); 1138 1139 if ($res == USER_VALID) { 1140 global $_permissionContext; 1141 1142 $_permissionContext = new Perms_Context($attempt, false); 1143 $_permissionContext->activate(true); 1144 1145 return true; 1146 } else { 1147 header('WWW-Authenticate: Basic realm="' . $tikidomain . '"'); 1148 header('HTTP/1.0 401 Unauthorized'); 1149 return false; 1150 } 1151 } 1152 1153 /** 1154 * @param bool $acceptFeed 1155 * @return array 1156 */ 1157 static function get_accept_types($acceptFeed = false) 1158 { 1159 $accept = explode(',', $_SERVER['HTTP_ACCEPT']); 1160 1161 if (isset($_REQUEST['httpaccept'])) { 1162 $accept = array_merge(explode(',', $_REQUEST['httpaccept']), $accept); 1163 } 1164 1165 $types = []; 1166 1167 foreach ($accept as $type) { 1168 $known = null; 1169 1170 if (strpos($type, $t = 'application/json') !== false) { 1171 $known = 'json'; 1172 } elseif (strpos($type, $t = 'text/javascript') !== false) { 1173 $known = 'json'; 1174 } elseif (strpos($type, $t = 'text/x-yaml') !== false) { 1175 $known = 'yaml'; 1176 } elseif (strpos($type, $t = 'application/rss+xml') !== false) { 1177 $known = 'rss'; 1178 } elseif (strpos($type, $t = 'application/atom+xml') !== false) { 1179 $known = 'atom'; 1180 } 1181 1182 if ($known && ! isset($types[$known])) { 1183 $types[$known] = $t; 1184 } 1185 } 1186 1187 if (empty($types)) { 1188 $types['html'] = 'text/html'; 1189 } 1190 1191 return $types; 1192 } 1193 1194 /** 1195 * @return bool 1196 */ 1197 static function is_machine_request() 1198 { 1199 foreach (self::get_accept_types() as $name => $full) { 1200 switch ($name) { 1201 case 'html': 1202 return false; 1203 case 'json': 1204 case 'yaml': 1205 return true; 1206 } 1207 } 1208 1209 return false; 1210 } 1211 1212 /** 1213 * @param bool $acceptFeed 1214 * @return bool 1215 */ 1216 static function is_serializable_request($acceptFeed = false) 1217 { 1218 foreach (self::get_accept_types($acceptFeed) as $name => $full) { 1219 switch ($name) { 1220 case 'json': 1221 case 'yaml': 1222 return true; 1223 case 'rss': 1224 case 'atom': 1225 if ($acceptFeed) { 1226 return true; 1227 } 1228 } 1229 } 1230 1231 return false; 1232 } 1233 1234 /** 1235 * @return bool 1236 */ 1237 function is_xml_http_request() 1238 { 1239 return ! empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest'; 1240 } 1241 1242 /** 1243 * Will process the output by serializing in the best way possible based on the request's accept headers. 1244 * To output as an RSS/Atom feed, a descriptor may be provided to map the array data to the feed's properties 1245 * and to supply additional information. The descriptor must contain the following keys: 1246 * [feedTitle] Feed's title, static value 1247 * [feedDescription] Feed's description, static value 1248 * [entryTitleKey] Key to lookup for each entry to find the title 1249 * [entryUrlKey] Key to lookup to find the URL of each entry 1250 * [entryModificationKey] Key to lookup to find the modification date 1251 * [entryObjectDescriptors] Optional. Array containing two key names, object key and object type to lookup missing information (url and title) 1252 */ 1253 static function output_serialized($data, $feed_descriptor = null) 1254 { 1255 foreach (self::get_accept_types(! is_null($feed_descriptor)) as $name => $full) { 1256 switch ($name) { 1257 case 'json': 1258 header("Content-Type: $full"); 1259 $data = json_encode($data); 1260 if ($data === false) { 1261 $error = ''; 1262 switch (json_last_error()) { 1263 case JSON_ERROR_NONE: 1264 $error = 'json_encode - No errors'; 1265 break; 1266 case JSON_ERROR_DEPTH: 1267 $error = 'json_encode - Maximum stack depth exceeded'; 1268 break; 1269 case JSON_ERROR_STATE_MISMATCH: 1270 $error = 'json_encode - Underflow or the modes mismatch'; 1271 break; 1272 case JSON_ERROR_CTRL_CHAR: 1273 $error = 'json_encode - Unexpected control character found'; 1274 break; 1275 case JSON_ERROR_SYNTAX: 1276 $error = 'json_encode - Syntax error, malformed JSON'; 1277 break; 1278 case JSON_ERROR_UTF8: 1279 $error = 'json_encode - Malformed UTF-8 characters, possibly incorrectly encoded'; 1280 break; 1281 default: 1282 $error = 'json_encode - Unknown error'; 1283 break; 1284 } 1285 throw new Exception($error); 1286 } 1287 if (isset($_REQUEST['callback'])) { 1288 $data = $_REQUEST['callback'] . '(' . $data . ')'; 1289 } 1290 echo $data; 1291 return; 1292 1293 case 'yaml': 1294 header("Content-Type: $full"); 1295 echo Yaml::dump($data, 20, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK); 1296 return; 1297 1298 case 'rss': 1299 $rsslib = TikiLib::lib('rss'); 1300 $writer = $rsslib->generate_feed_from_data($data, $feed_descriptor); 1301 $writer->setFeedLink(self::tikiUrl($_SERVER['REQUEST_URI']), 'rss'); 1302 1303 header('Content-Type: application/rss+xml'); 1304 echo $writer->export('rss'); 1305 return; 1306 1307 case 'atom': 1308 $rsslib = TikiLib::lib('rss'); 1309 $writer = $rsslib->generate_feed_from_data($data, $feed_descriptor); 1310 $writer->setFeedLink(self::tikiUrl($_SERVER['REQUEST_URI']), 'atom'); 1311 1312 header('Content-Type: application/atom+xml'); 1313 echo $writer->export('atom'); 1314 return; 1315 1316 case 'html': 1317 header("Content-Type: $full"); 1318 echo $data; 1319 return; 1320 } 1321 } 1322 } 1323 1324 /** 1325 * @param $filename string The file name directory structure to test. May be an absolute or relative file path. 1326 * 1327 * @return bool|null Return true upon file access success, false upon failure, and null if the file does not exist. 1328 */ 1329 function isFileWebAccessible (string $filename): ? bool { 1330 global $tikipath, $base_url_http, $base_url_https; 1331 // if the directory is within the Tiki root, then remove the prefixed Tiki root 1332 if (0 === strpos ($filename, $tikipath)) { 1333 $filename = substr($filename, strlen($tikipath)); 1334 } 1335 1336 // if the file does not exist, then don't bother proceeding further 1337 if (!file_exists($filename)) { 1338 return null; 1339 } 1340 // if the file is outside the Tiki Root 1341 if ($filename[0] === '/' ) { 1342 return false; 1343 1344 } 1345 1346 // now load try accessing the file and check for a 200 (ok) or 300 (moved) 1347 // lets check http first 1348 $response = @get_headers($base_url_http . $filename); 1349 $response = substr($response[0], 9, 1); 1350 if ($response == '2' || $response == '3') { 1351 return true; 1352 } else { // now we try https, just to be sure. 1353 $response = @get_headers($base_url_https . $filename); 1354 $response = substr($response[0], 9, 1); 1355 if ($response == '2' || $response == '3') { 1356 return true; 1357 } 1358 } 1359 // if all else has failed, conclude that the file is not accessible 1360 return false; 1361 } 1362} 1363