You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1545 lines
57 KiB

<?php
// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
//
// All Rights Reserved. See copyright.txt for details and a complete list of authors.
// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
// $Id$
use Symfony\Component\Yaml\Yaml;
/**
* TikiAccessLib
*
* @uses TikiLib
*
*/
class TikiAccessLib extends TikiLib
{
private $noRedirect = false;
private $noDisplayError = false;
//used in CSRF protection methods
private $ticket;
private $ticketMatch;
private $originMatch;
private $base;
private $origin;
private $originSource;
private $logMsg = '';
private $userMsg = '';
public function preventRedirect($prevent)
{
$this->noRedirect = (bool) $prevent;
}
/**
* Prevent the display of errors
* useful during plugin parsing to mute error redirects
*
* @param bool $prevent
*/
public function preventDisplayError($prevent)
{
$this->noDisplayError = (bool) $prevent;
}
/**
* check that the user is admin or has admin permissions
*
*/
public function check_admin($user, $feature_name = '')
{
global $tiki_p_admin, $prefs;
require_once('tiki-setup.php');
// first check that user is logged in
$this->check_user($user);
if (($user != 'admin') && ($tiki_p_admin != 'y')) {
$msg = tra("You do not have the permission that is needed to use this feature");
if ($feature_name) {
$msg = $msg . ": " . $feature_name;
}
$this->display_error('', $msg, '403');
}
}
/**
* @param $user
*/
public function check_user($user)
{
global $prefs;
require_once('tiki-setup.php');
if (! $user) {
$title = tra("You are not logged in");
$this->display_error('', $title, '403');
}
}
/**
* @param string $user
* @param array $features
* @param array $permissions
* @param string $permission_name
*/
public function check_page($user = 'y', $features = [], $permissions = [], $permission_name = '')
{
require_once('tiki-setup.php');
if ($features) {
$this->check_feature($features);
}
$this->check_user($user);
if ($permissions) {
$this->check_permission($permissions, $permission_name);
}
}
/**
* check_feature: Checks if a feature or a list of features are activated
*
* @param string or array $features If just a string, this method will only test that one. If an array, all features will be tested
* @param string $feature_name Name that will be printed on the error screen
* @param string $relevant_admin_panel Admin panel where the feature can be set to 'Y'. This link is provided on the error screen
* @access public
* @return void
*
*/
public function check_feature($features, $feature_name = '', $relevant_admin_panel = 'features', $either = false)
{
global $prefs;
require_once('tiki-setup.php');
$perms = Perms::get();
if ($perms->admin && isset($_REQUEST['check_feature']) && isset($_REQUEST['lm_preference'])) {
$prefslib = TikiLib::lib('prefs');
$prefslib->applyChanges((array) $_REQUEST['lm_preference'], $_REQUEST);
}
if (! is_array($features)) {
$features = [$features];
}
if ($either) {
// if anyone will do, start assuming no go and test for feature
$allowed = false;
} else {
// if all is needed, start assuming it's a go and test for feature not on
$allowed = true;
}
foreach ($features as $feature) {
if (! $either && $prefs[$feature] != 'y') {
if ($feature_name != '') {
$feature = $feature_name;
}
$allowed = false;
break;
} elseif ($either && $prefs[$feature] == 'y') {
// test for feature in "anyone will do" case
$allowed = true;
break;
}
}
if (! $allowed) {
$smarty = TikiLib::lib('smarty');
if ($perms->admin) {
$smarty->assign('required_preferences', $features);
}
$msg = tr(
'Required features: <b>%0</b>. If you do not have permission to activate these features, ask the site administrator.',
implode(', ', $features)
);
$this->display_error('', $msg, 'no_redirect_login');
}
}
/**
* Check permissions for current user and display an error if not granted
* Multiple perms can be checked at once using an array and all those perms need to be granted to continue
*
* @param string|array $permissions permission name or names (can be old style e.g. 'tiki_p_view' or just 'view')
* @param string $permission_name text used in warning if perm not granted
* @param bool|string $objectType optional object type (e.g. 'wiki page')
* @param bool|string $objectId optional object id (e.g. 'HomePage' or '42' depending on object type)
*/
public function check_permission($permissions, $permission_name = '', $objectType = false, $objectId = false)
{
require_once('tiki-setup.php');
if (! is_array($permissions)) {
$permissions = [$permissions];
}
foreach ($permissions as $permission) {
if (false !== $objectType) {
$applicable = Perms::get($objectType, $objectId);
} else {
$applicable = Perms::get();
}
if ($applicable->$permission) {
continue;
}
if ($permission_name) {
$permission = $permission_name;
}
$this->display_error('', tra("You do not have the permission that is needed to use this feature:") . " " . $permission, '403', false);
if (empty($GLOBALS['user']) && empty($_SESSION['loginfrom'])) {
$_SESSION['loginfrom'] = $_SERVER['REQUEST_URI'];
}
}
}
/**
* Check permissions for current user and display an error if not granted
* Multiple perms can be checked at once using an array and ANY ONE OF those perms only needs to be granted to continue
*
* NOTE that you do NOT have to use this to include admin perms, as admin perms automatically inherit the perms they are admin of
*
* @param string|array $permissions permission name or names (can be old style e.g. 'tiki_p_view' or just 'view')
* @param string $permission_name text used in warning if perm not granted
* @param bool|string $objectType optional object type (e.g. 'wiki page')
* @param bool|string $objectId optional object id (e.g. 'HomePage' or '42' depending on object type)
*/
public function check_permission_either($permissions, $permission_name = '', $objectType = false, $objectId = false)
{
require_once('tiki-setup.php');
$allowed = false;
if (! is_array($permissions)) {
$permissions = [$permissions];
}
foreach ($permissions as $permission) {
if (false !== $objectType) {
$applicable = Perms::get($objectType, $objectId);
} else {
$applicable = Perms::get();
}
if ($applicable->$permission) {
$allowed = true;
break;
}
}
if (! $allowed) {
if ($permission_name) {
$permission = $permission_name;
} else {
$permission = implode(', ', $permissions);
}
$this->display_error('', tra("You do not have the permission that is needed to use this feature") . ": " . $permission, '403', false);
}
}
/**
* check permission, where the permission is normally unset
*
*/
public function check_permission_unset($permissions, $permission_name)
{
require_once('tiki-setup.php');
foreach ($permissions as $permission) {
global $$permission;
if ((isset($$permission) && $$permission == 'n')) {
if ($permission_name) {
$permission = $permission_name;
}
$this->display_error('', tra("You do not have the permission that is needed to use this feature") . ": " . $permission, '403', false);
}
}
}
/**
* check page exists
*
*/
public function check_page_exists($page)
{
require_once('tiki-setup.php');
if (! $this->page_exists($page)) {
$this->display_error($page, tra("Page cannot be found"), '404');
}
}
/**
* Return default security timeout period in seconds.
* Used in setting the global securityTimeout preference used to determine the expiry period for state-changing
* forms and related CSRF ticket. Add the timeout class to the submit element of the form to subject a form to
* the expiration period.
* @return mixed
*/
public function getDefaultTimeout()
{
global $prefs;
$timeSetting = isset($prefs['session_lifetime']) && $prefs['session_lifetime'] > 0 ? $prefs['session_lifetime'] * 60
: ini_get('session.gc_maxlifetime');
return ! empty($timeSetting) ? min(4 * 60 * 60, $timeSetting) : 4 * 60 * 60; //4 hours max
}
/**
* CSRF protection - set the ticket on the server/cookie and as a smarty template variable
*
* Called by the smarty function {ticket}, which should be placed in all forms with actions that change the
* database
* @throws Exception
*/
public function setTicket()
{
global $prefs;
$setCookie = false;
if (($prefs['site_short_lived_csrf_tokens'] ?? 'n') === 'y') {
$this->ticket = TikiLib::lib('tiki')->generate_unique_sequence(32, true);
$_SESSION['tickets'][$this->ticket] = time();
} else {
$this->ticket = $_SESSION['CSRF_TOKEN'] ?? null;
if (! $this->ticket) { // Check the cookie
$this->ticket = $_SESSION['CSRF_TOKEN'] = $this->retrieveTicketFromCookie(); // return ticket or null
}
if (! $this->ticket) {
$this->ticket = $_SESSION['CSRF_TOKEN'] = TikiLib::lib('tiki')->generate_unique_sequence(32, true);
$setCookie = true;
}
}
if ($setCookie || ! getcookie(session_name() . '_CSRF')) {
$this->setTicketInCookie();
}
Tikilib::lib('smarty')->assign('ticket', $this->ticket);
}
/**
* Encrypt the CSRF ticket value
*
* @return string
*/
protected function encryptCsrfTicket(string $value): string
{
$key = TikiLib::lib('tiki')->get_site_hash();
$key = str_pad(substr($key, 0, SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES), SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES);
$nonce = random_bytes(SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES);
return base64_encode($nonce . sodium_crypto_aead_xchacha20poly1305_ietf_encrypt($value, '', $nonce, $key));
}
/**
* Decrypt the CSRF ticket value
*
* @return string|false
*/
protected function decryptCsrfTicket(string $value)
{
$key = TikiLib::lib('tiki')->get_site_hash();
$key = str_pad(substr($key, 0, SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES), SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES);
$value = base64_decode($value);
$nonce = substr($value, 0, SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES);
$cipherText = substr($value, SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES);
return sodium_crypto_aead_xchacha20poly1305_ietf_decrypt($cipherText, '', $nonce, $key);
}
/**
* @return false|string
*/
protected function retrieveTicketFromCookie()
{
$cookie = getcookie(session_name() . '_CSRF');
if (! $cookie) {
return false;
}
return $this->decryptCsrfTicket($cookie);
}
/**
* @return void
*/
protected function setTicketInCookie()
{
if (php_sapi_name() == 'cli') {
return;
}
// Encrypt ticket
$cipherTicket = $this->encryptCsrfTicket($this->ticket);
// you can set even if is the same value
setcookie(session_name() . '_CSRF', $cipherTicket, 0, '', '', false, true);
}
/**
* @param bool|string $postConfirm Whether a confirmation is needed before performing a POST action. Generally,
* confirms are needed POST requests that cannot be easily undone. Default is false.
* If a confirm is desired, can be set to true or a string that will be used as
* the confirmation text.
* @param bool $getNoConfirm Allow GET requests without a confirm. If true, then no confirmation is necessary
* (this should be rare and well controlled). Remember that a ticket should not be
* included in a GET request so it will need to be provided otherwise or the $checkWhat
* parameter could be impacted. If false (default), then the method will create a
* confirmation popup.
* @param string $checkWhat 'hostTicket' to check origin host domain and ticket - this is recommended and the default
* 'host' to check origin host domain only
* 'ticket' to check ticket only
* @param bool $unsetTicket By default a ticket on the server is unset once used (true) - in rare cases
* it may need to be reused by setting this parameter to false. Nevertheless, during
* a single page request the same ticket can be used for multiple uses of checkCsrf()
* since since a successful check result is maintained throught the request
* @param string $ticket Ticket may be provided, e.g., in cases where it is used more than once and is
* stored in a session variable rather than being part of the $_POST. Only tickets
* that originated in a $_POST should be used. Can be used in conjunction with
* $unsetTicket
* @param string $error See csrfError for information on error types and uses
*
* @return bool True if CSRF check passed, false otherwise
* @throws Services_Exception
* @see csrfError() for further details on error types and uses.
*/
public function checkCsrf(
$postConfirm = false,
$getNoConfirm = false,
$checkWhat = 'hostTicket',
$unsetTicket = true,
$ticket = '',
$error = 'session'
) {
global $prefs;
if ($prefs['pwa_feature'] == 'y') {
return true;
}
if (TIKI_API) {
return true;
}
// allow null value to equate to default for skipped parameters
if ($postConfirm === null) {
$postConfirm = false;
$confirmText = '';
} elseif ($postConfirm === true || (is_string($postConfirm) && ! empty($postConfirm))) {
if (is_string($postConfirm) && ! empty($postConfirm)) {
$confirmText = $postConfirm;
} else {
$confirmText = tr('Confirm action');
}
$postConfirm = true;
}
if ($getNoConfirm === null) {
$getNoConfirm = false;
}
if ($checkWhat === null) {
$checkWhat = 'hostTicket';
}
if ($unsetTicket === null) {
$unsetTicket = true;
}
if ($ticket === null) {
$ticket = '';
}
//send requests requiring confirmation to confirmation form
if (
($getNoConfirm === false && ! $this->requestIsPost())
|| ($postConfirm && (empty($_POST['confirmForm']) || $_POST['confirmForm'] !== 'y'))
) {
$this->confirmRedirect($confirmText, $error);
//perform check if action post or required confirmation post
} elseif (
(! $postConfirm && ($this->isActionPost() || $getNoConfirm === true))
|| ($postConfirm && ! empty($_POST['confirmForm']) && $_POST['confirmForm'] === 'y')
) {
//return true if check already performed - e.g., multiple checks on tiki-login.php for same request
if ($this->csrfResult()) {
return true;
}
$result = false;
//check origin host
if (in_array($checkWhat, ['hostTicket', 'host'])) {
$this->originCheck();
//note result if only checking host
if ($checkWhat === 'host') {
$result = $this->originMatch();
}
}
//check ticket
if (in_array($checkWhat, ['hostTicket', 'ticket'])) {
$this->ticketCheck($unsetTicket, $ticket);
//note result if only checking ticket
if ($checkWhat === 'ticket') {
$result = $this->ticketMatch();
}
}
//check both host and ticket
if ($checkWhat === 'hostTicket') {
$result = $this->csrfResult();
}
if ($result) {
return true;
} else {
$this->csrfError($error);
return false;
}
}
if (! $this->requestIsPost()) {
$msg = ' ' . tr('The request was not a POST request as required.');
} else {
$msg = ' ' . tr('There was no security ticket submitted with the request.');
}
$this->logMsg = $msg;
$this->userMsg = ' ' . $this->logMsg;
$this->csrfError($error);
return false;
}
/**
* CSRF protection - Perform origin check to ensure the requesting server matches this server
*
* Sets the originMatch property to true or false depending on the result of the check
*
* @return void
*/
private function originCheck()
{
// $base_url is usually host + directory
global $base_url;
include_once('lib/setup/absolute_urls.php');
$this->origin = '';
$this->originSource = 'empty';
//first check HTTP_ORIGIN
if (! empty($_SERVER['HTTP_ORIGIN'])) {
//HTTP_ORIGIN is usually host only without trailing slash
$this->origin = $_SERVER['HTTP_ORIGIN'];
$this->originSource = 'HTTP_ORIGIN';
//then check HTTP_REFERER
} elseif (! empty($_SERVER['HTTP_REFERER'])) {
//HTTP_REFERER is usually the full path (host + directory + file + query)
$this->origin = $_SERVER['HTTP_REFERER'];
$this->originSource = 'HTTP_REFERER';
}
//identify server host + port
$base = parse_url($base_url);
$baseHost = isset($base['host']) ? $base['host'] : '';
$basePort = isset($base['port']) ? ':' . $base['port'] : '';
$this->base = $baseHost . $basePort;
//identify requesting host + port
$origin = parse_url($this->origin);
$originHost = isset($origin['host']) ? $origin['host'] : '';
$originPort = isset($origin['port']) ? ':' . $origin['port'] : '';
$this->origin = $originHost . $originPort;
//perform compare
$this->originMatch = $this->base === $this->origin;
//error message
if (! $this->originMatch()) {
if ($this->originSource === 'empty') {
$this->logMsg .= tr(
'The requesting site could not be identified because %0 and %1 were empty.',
'HTTP_ORIGIN',
'HTTP_REFERER'
);
$this->userMsg .= ' ' . tr('The requesting site could not be identified.');
} else {
$this->logMsg .= tr(
'The %0 host (%1) does not match this server (%2).',
$this->originSource,
$this->origin,
$this->base
);
$this->userMsg .= ' ' . tr('The requesting site domain does not match this site\'s domain.');
}
}
}
/**
* CSRF protection - Perform ticket check to ensure ticket in the $_POST variable matches the one stored on the
* server and that the ticket has not expired.
*
* Sets the ticketMatch property to true or false depending on the result of the check
*
* @param bool $unsetTicket Whether to unset $_SESSION ticket after checking. Normally, should unset,
* however infrequently it is easier to use a ticket more than once.
* Other code should unset the ticket after the multiple uses are complete and ensure
* repeated use does not create a vulnerability
*
* @param string $ticket Ticket may be provided, e.g., in cases where it is used more than once and is
* stored in a session variable rather than being part of the $_POST. Only tickets
* that originated in a $_POST should be used.
*/
private function ticketCheck($unsetTicket, $ticket)
{
if (! empty($ticket)) {
$this->ticket = $ticket;
} elseif (! empty($_POST['ticket'])) {
$this->ticket = $_POST['ticket'];
} else {
$this->ticket = false;
}
//just in case url decoding is needed
if (strpos($this->ticket, '%') !== false) {
$this->ticket = urldecode($this->ticket);
}
global $prefs;
if ($this->ticket && ($prefs['site_short_lived_csrf_tokens'] ?? 'n') !== 'y' && ($this->ticket === $_SESSION['CSRF_TOKEN'] ?? $this->retrieveTicketFromCookie())) {
$this->ticketMatch = true;
} elseif ($this->ticket && ! empty($_SESSION['tickets'][$this->ticket])) {
//check that request ticket matches server ticket
//check that ticket has not expired
global $prefs;
$maxTime = $prefs['site_security_timeout'];
$ticketTime = $_SESSION['tickets'][$this->ticket];
$requestTime = $_SERVER['REQUEST_TIME'];
if ($ticketTime <= $requestTime && $ticketTime > ($requestTime - $maxTime)) {
//ticket is still valid
$this->ticketMatch = true;
} else {
//ticket is expired
$msg = tr('The security ticket matches but is expired.');
$ticketAgeSeconds = $requestTime - $ticketTime;
$ticketAgeMinutes = $ticketAgeSeconds < 60 ? '' : ' (' . round($ticketAgeSeconds / 60, 1)
. ' ' . tr('minutes') . ')';
$this->logMsg = $msg . PHP_EOL . ' ' . tr('Age of security ticket:') . ' '
. $ticketAgeSeconds . ' ' . tr('seconds') . $ticketAgeMinutes;
$this->userMsg = ' ' . $msg . ' ' . tr('Reload the page.');
$this->ticketMatch = false;
}
if ($unsetTicket) {
unset($_SESSION['tickets'][$this->ticket]);
}
} else {
//ticket doesn't match or is missing
if (! $this->ticket) {
$msg = tr('The security ticket is missing from the request.');
} else {
$msg = tr('The security ticket included in the request does not match a ticket on the server.');
}
$this->logMsg = $msg;
$this->userMsg = ' ' . $this->logMsg . ' ' . tr('Reloading the page may help.');
$this->ticketMatch = false;
}
}
/**
* Generate tiki log entry and user feedback for CSRF errors
* @param string $error * 'session' The regular way of providing feedback (the anti-csrf error message) using the standard Feedback class.
* * 'services' Used to provide feedback for ajax services.
* * 'page' Used when the error needs to be shown on a separate page (redirects to a 400 error page).
* * 'none' Any errors are not displayed
* @throws Exception
* @throws Services_Exception
*/
private function csrfError($error = 'session')
{
if ($error !== 'none') {
$log = ! empty(ini_get('error_log'));
if ($log) {
$moreUserMsg = ' ' . tr('For more information, administrators can check the server php error log as defined in php.ini.');
} else {
$moreUserMsg = ' ' . tr('For more information in the future, administrators can define the error_log setting in the php.ini file.');
}
$this->userMsg = tr('Request could not be completed due to problems encountered in the security check.')
. $this->userMsg . $moreUserMsg;
//log message
$this->csrfPhpErrorLog($this->logMsg);
//user feedback
switch ($error) {
case 'services':
throw new Services_Exception($this->userMsg, 401);
break;
case 'page':
Feedback::errorPage(['mes' => $this->userMsg, 'errortype' => 401]);
break;
case 'session':
default:
Feedback::error($this->userMsg);
break;
}
}
}
/**
* CSRF ticket - Check that the ticket has been matched to the previous ticket set
*
* @return bool Returns true if the request ticket matches the server ticket and is not expired, false if not
*/
private function ticketMatch()
{
return $this->ticketMatch === true;
}
/**
* CSRF origin check - Check that origin matches the server
*
* @return bool Returns true if the request origin matches the origin of the server, false if not
*/
private function originMatch()
{
return $this->originMatch === true;
}
/**
* Check that the request method is POST
*
* @return bool Returns true if the request method is POST, false if not
*/
public function requestIsPost()
{
return $_SERVER['REQUEST_METHOD'] === 'POST';
}
/**
* CSRF ticket - Return results of ticket and origin match
*
* @return bool Returns true if both matches were successful, false if not
*/
public function csrfResult()
{
return $this->originMatch() && $this->ticketMatch();
}
/**
* CSRF ticket - Get the ticket
*
* @return mixed Returns the ticket if set, false if not
*/
public function getTicket()
{
if (! empty($this->ticket)) {
return $this->ticket;
} else {
return false;
}
}
/**
* Check that the request is POST and includes a ticket
*
* @return bool Returns true if the request is post and the request includes a ticket, false if not
*/
public function isActionPost()
{
if (TIKI_API) {
return $this->requestIsPost();
} else {
return ($this->requestIsPost() && ! empty($_POST['ticket']));
}
}
/**
* Utility method for checkCsrfForm and also used in infrequent cases where a database-changing action is initiated
* through an outside link, for example an unsubscribe link, in which case an additional validation method should
* also be applied
*
* @param string $confirmText The confirm question posed to the user.
* @param string $error Options include 'session', 'none', 'services' and 'page'. Used in csrfError()
*
* @return bool True if conformation was accepted, false otherwise
* @throws Services_Exception
* @see csrfError() for further details on error types and uses.
*/
private function confirmRedirect($confirmText, $error = 'session')
{
if (empty($_POST['confirmForm']) || $_POST['confirmForm'] !== 'y') {
if (empty($confirmText)) {
$confirmText = tr('Confirm action');
}
// Display the confirmation in the main tiki.tpl template
$smarty = TikiLib::lib('smarty');
if (empty($smarty->getTemplateVars('confirmaction'))) {
$smarty->assign('confirmaction', $_SERVER['REQUEST_URI'] ?? $_SERVER['PHP_SELF']);
}
$smarty->assign('post', $_REQUEST);
$smarty->assign('print_page', 'n');
$smarty->assign('title', tra('Please confirm action'));
$smarty->assign('confirmation_text', $confirmText);
$smarty->assign('mid', 'confirm.tpl');
$smarty->display('tiki.tpl');
die();
} else {
return $this->checkCsrf($error);
}
}
/**
* Utility to compose and write the error message from CSRF errors to the server php error log, adding on
* certain information regarding the environment. Broken into two pieces since the the GET and POST
* parameters have the potential to exceed the character limit.
*
* @param $msg string Description of the specific error which will be placed first ahead of
* the environmental information
*/
private function csrfPhpErrorLog($msg)
{
global $prefs;
error_log(PHP_EOL
. '**** ' . tr('Start CSRF error from') . $_SERVER['SERVER_NAME'] . ' *****' . PHP_EOL
. ' ' . $msg . PHP_EOL
. ' site_security_timeout' . tr('preference:') . $prefs['site_security_timeout']
. tr('seconds') . '(' . $prefs['site_security_timeout'] / 60 . ' minutes)' . PHP_EOL
. ' SCRIPT_NAME: ' . $_SERVER['SCRIPT_NAME'] . PHP_EOL
. ' REQUEST_URI: ' . $_SERVER['REQUEST_URI'] . PHP_EOL
. ' HTTP_ORIGIN: ' . $_SERVER['HTTP_ORIGIN'] . PHP_EOL
. ' HTTP_REFERER: ' . $_SERVER['HTTP_REFERER'] . PHP_EOL
. ' REQUEST_METHOD: ' . $_SERVER['REQUEST_METHOD'] . PHP_EOL);
$get = count($_GET) ? json_encode($_GET, JSON_PRETTY_PRINT) : tr('empty');
$post = count($_POST) ? json_encode($_POST, JSON_PRETTY_PRINT) : tr('empty');
error_log(PHP_EOL
. ' $_GET: ' . $get . PHP_EOL
. ' $_POST: ' . $post . PHP_EOL
. '**** ' . tr('End CSRF error from') . $_SERVER['SERVER_NAME'] . ' *****');
}
/**
* ***** Note: Being replaced by checkCsrf method above *************
*
*
* @param string $confirmation_text Custom text to use if a confirmation page is brought up first
* @param bool $returnHtml Set to false to not use the standard confirmation page and to not use the
* standard error page. Suitable for popup confirmations when set to false.
* @param bool $errorMsg Set to true to have the Feedback error message sent automatically
* @return array|bool
* @throws Exception
* @throws Services_Exception
* @deprecated replaced by checkCsrfForm() and checkCsrf()
* @see checkCsrf() For post validation with or without confirmation check
*/
public function check_authenticity($confirmation_text = '', $returnHtml = true, $errorMsg = false)
{
$check = true;
if (empty($_POST['confirmForm']) || $_POST['confirmForm'] !== 'y') {
if ($this->checkCsrf(null, null, 'host')) {
if ($returnHtml) {
//redirect to a confirmation page
if (empty($confirmation_text)) {
$confirmation_text = tra('Confirm action');
}
if (empty($confirmaction)) {
$confirmaction = $_SERVER['PHP_SELF'];
}
// Display the confirmation in the main tiki.tpl template
$smarty = TikiLib::lib('smarty');
$smarty->assign('post', $_REQUEST);
$smarty->assign('print_page', 'n');
$smarty->assign('confirmation_text', $confirmation_text);
$smarty->assign('confirmaction', $confirmaction);
$smarty->assign('mid', 'confirm.tpl');
$smarty->display('tiki.tpl');
die();
} else {
//return ticket to be placed in a form with other code
return ['ticket' => $this->ticket];
}
} else {
$check = false;
}
} elseif (! empty($_POST['confirmForm']) && $_POST['confirmForm'] === 'y') {
$check = $this->checkCsrf();
}
if (! $check) {
if ($returnHtml) {
$smarty = TikiLib::lib('smarty');
$smarty->display('error.tpl');
exit();
} else {
return false;
}
}
}
/**
* @param $page
* @param string $errortitle
* @param string $errortype
* @param bool $enableRedirect
* @param string $message
* @throws Exception
*/
public function display_error($page, $errortitle = "", $errortype = "", $enableRedirect = true, $message = '')
{
if ($this->noDisplayError) {
return;
}
global $prefs, $tikiroot, $user;
require_once('tiki-setup.php');
$userlib = TikiLib::lib('user');
$smarty = TikiLib::lib('smarty');
// Don't redirect when calls are made for web services
if (
$enableRedirect && $prefs['feature_redirect_on_error'] == 'y' && ! $this->is_machine_request()
&& $tikiroot . $prefs['tikiIndex'] != $_SERVER['PHP_SELF']
&& ( $page != $userlib->get_user_default_homepage($user) || $page === '' )
) {
$this->redirect($prefs['tikiIndex']);
}
$detail = [
'code' => $errortype,
'errortitle' => $errortitle,
'message' => $message,
];
if (! isset($errortitle)) {
$detail['errortitle'] = tra('unknown error');
}
if (empty($message)) {
$detail['message'] = $detail['errortitle'];
}
// Display the template
switch ($errortype) {
case '404':
header("HTTP/1.0 404 Not Found");
$detail['page'] = $page;
$detail['message'] .= ' (404)';
break;
case '403':
header("HTTP/1.0 403 Forbidden");
break;
case '503':
header("HTTP/1.0 503 Service Unavailable");
break;
default:
$errortype = (int) $errortype;
$title = strip_tags($detail['errortitle']);
if (! $errortype) {
$errortype = 403;
$title = 'Forbidden';
}
header("HTTP/1.0 $errortype $title");
break;
}
if ($this->is_serializable_request()) {
Feedback::error($errortitle, true);
$this->output_serialized($detail);
} elseif ($this->is_xml_http_request()) {
$smarty->assign('detail', $detail);
$smarty->display('error-ajax.tpl');
} else {
if (
($errortype == 401 || $errortype == 403) &&
empty($user) &&
($prefs['permission_denied_login_box'] == 'y' || ! empty($prefs['permission_denied_url']))
) {
if (empty($_SESSION['loginfrom'])) {
$_SESSION['loginfrom'] = $_SERVER['REQUEST_URI'];
}
if ($prefs['login_autologin'] == 'y' && $prefs['login_autologin_redirectlogin'] == 'y' && ! empty($prefs['login_autologin_redirectlogin_url'])) {
$this->redirect($prefs['login_autologin_redirectlogin_url']);
}
}
$smarty->assign('errortitle', $detail['errortitle']);
$smarty->assign('msg', $detail['message']);
$smarty->assign('errortype', $detail['code']);
if (isset($detail['page'])) {
$smarty->assign('page', $page);
}
$smarty->display("error.tpl");
}
die;
}
/**
* @param string $page
* @return string
*/
public function get_home_page($page = '')
{
global $prefs, $use_best_language, $user;
$userlib = TikiLib::lib('user');
$tikilib = TikiLib::lib('tiki');
if (! isset($page) || $page == '') {
if ($prefs['useGroupHome'] == 'y') {
$groupHome = $userlib->get_user_default_homepage($user);
if ($groupHome) {
$page = $groupHome;
} else {
$page = $prefs['wikiHomePage'];
}
} else {
$page = $prefs['wikiHomePage'];
}
if (! $tikilib->page_exists($prefs['wikiHomePage'])) {
$tikilib->create_page($prefs['wikiHomePage'], 0, '', $this->now, 'Tiki initialization');
}
if ($prefs['feature_best_language'] == 'y') {
$use_best_language = true;
}
}
return $page;
}
/**
* Returns an absolute URL for the given one
*
* Inspired on \ZendOpenId\OpenId::absoluteUrl
*
* @param string $url absolute or relative URL
* @return string
*/
public static function absoluteUrl($url)
{
if (empty($url)) {
return self::selfUrl();
} elseif (! preg_match('|^([^:]+)://|', $url)) {
if (preg_match('|^([^:]+)://([^:@]*(?:[:][^@]*)?@)?([^/:@?#]*)(?:[:]([^/?#]*))?(/[^?]*)?((?:[?](?:[^#]*))?(?:#.*)?)$|', self::selfUrl(), $reg)) {
$scheme = $reg[1];
$auth = $reg[2];
$host = $reg[3];
$port = $reg[4];
$path = $reg[5];
$query = $reg[6];
if ($url[0] == '/') {
return $scheme
. '://'
. $auth
. $host
. (empty($port) ? '' : (':' . $port))
. $url;
} else {
$dir = dirname($path);
return $scheme
. '://'
. $auth
. $host
. (empty($port) ? '' : (':' . $port))
. (strlen($dir) > 1 ? $dir : '')
. '/'
. $url;
}
}
}
return $url;
}
/**
* Returns a full URL that was requested on current HTTP request.
*
* Inspired on \ZendOpenId\OpenId::selfUrl
*
* @return string
*/
public static function selfUrl()
{
$url = '';
$port = '';
if (isset($_SERVER['HTTP_HOST'])) {
if (($pos = strpos($_SERVER['HTTP_HOST'], ':')) === false) {
if (isset($_SERVER['SERVER_PORT'])) {
$port = ':' . $_SERVER['SERVER_PORT'];
}
$url = $_SERVER['HTTP_HOST'];
} else {
$url = substr($_SERVER['HTTP_HOST'], 0, $pos);
$port = substr($_SERVER['HTTP_HOST'], $pos);
}
} elseif (isset($_SERVER['SERVER_NAME'])) {
$url = $_SERVER['SERVER_NAME'];
if (isset($_SERVER['SERVER_PORT'])) {
$port = ':' . $_SERVER['SERVER_PORT'];
}
}
if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') {
$url = 'https://' . $url;
if ($port == ':443') {
$port = '';
}
} else {
$url = 'http://' . $url;
if ($port == ':80') {
$port = '';
}
}
$url .= $port;
if (isset($_SERVER['HTTP_X_REWRITE_URL'])) {
$url .= $_SERVER['HTTP_X_REWRITE_URL'];
} elseif (isset($_SERVER['REQUEST_URI'])) {
$url .= $_SERVER['REQUEST_URI'];
} elseif (isset($_SERVER['SCRIPT_URL'])) {
$url .= $_SERVER['SCRIPT_URL'];
} elseif (isset($_SERVER['REDIRECT_URL'])) {
$url .= $_SERVER['REDIRECT_URL'];
} elseif (isset($_SERVER['PHP_SELF'])) {
$url .= $_SERVER['PHP_SELF'];
} elseif (isset($_SERVER['SCRIPT_NAME'])) {
$url .= $_SERVER['SCRIPT_NAME'];
if (isset($_SERVER['PATH_INFO'])) {
$url .= $_SERVER['PATH_INFO'];
}
}
return $url;
}
/**
* Utility function redirect the browser location to another url
* @param string $url The target web address
* @param string $msg An optional message to display
* @param int $code HTTP code
* @param string $msgtype Type of message which determines styling (e.g., success, error, warning, etc.)
*/
public function redirect($url = '', $msg = '', $code = 302, $msgtype = '')
{
global $prefs;
if ($this->noRedirect) {
return;
}
// TODO: Validate URL
if ($url == '') {
$url = $prefs['tikiIndex'];
}
if (trim($msg)) {
$session = session_id();
if (empty($session)) {
// Can happen if session_silent is enabled. But does any instance enable session_silent?
// Removing this case would allow removing the $msg parameters and just have callers using Feedback::add() before calling redirect(). Chealer 2017-08-16
$start = strpos($url, '?') ? '&' : '?';
$url = $start . 'msg=' . urlencode($msg) . '&msgtype=' . urlencode($msgtype);
} else {
$_SESSION['msg'] = $msg;
$_SESSION['msgtype'] = $msgtype;
}
}
TikiLib::events()->trigger('tiki.process.redirect');
session_write_close();
if (headers_sent()) {
echo "<script>document.location.href='" . smarty_modifier_escape($url, 'javascript') . "';</script>\n";
} else {
@ob_end_clean(); // clear output buffer
if ($prefs['feature_obzip'] == 'y') {
@ob_start('ob_gzhandler');
}
header("HTTP/1.0 $code Found");
header("Location: $url");
}
exit();
}
/**
* @param $message
*/
public function flash($message)
{
$this->redirect($_SERVER['REQUEST_URI'], $message);
}
/**
* Authorizes access to Tiki RSS feeds via user/password embedded in a URL
* e.g. https://joe:secret@localhost/tiki/tiki-calendars_rss.php?ver=2
* ~~~~~~~~~~
*
* @param array the permissions that needs to be checked against (e.g. tiki_p_view)
*
* @return null if authorized, otherwise an array(msg,header)
* where msg can be displayed, and header decides whether to
* send 401 Unauthorized headers.
*/
public function authorize_rss($rssrights)
{
global $user, $prefs;
$userlib = TikiLib::lib('user');
$tikilib = TikiLib::lib('tiki');
$smarty = TikiLib::lib('smarty');
$perms = Perms::get();
$result = ['msg' => tra("You do not have permission to view this section"), 'header' => 'n'];
// if current user has appropriate rights, allow.
foreach ($rssrights as $perm) {
if ($perms->$perm) {
return;
}
}
// deny if no basic auth allowed.
if ($prefs['feed_basic_auth'] != 'y') {
return $result;
}
//login is needed to access the contents
$https_mode = isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) == 'on';
//refuse to authenticate in plaintext if https_login_required.
if ($prefs['https_login_required'] == 'y' && ! $https_mode) {
$result['msg'] = tra("For the security of your password, direct access to the feed is only available via HTTPS");
return $result;
}
if ($this->http_auth()) {
$perms = Perms::get();
foreach ($rssrights as $perm) {
if ($perms->$perm) {
// if user/password and the appropriate rights are correct, allow.
return;
}
}
}
return $result;
}
/**
* @return bool
*/
public function http_auth()
{
global $tikidomain, $user;
$userlib = TikiLib::lib('user');
$smarty = TikiLib::lib('smarty');
if (! $tikidomain) {
$tikidomain = "Default";
}
if (! isset($_SERVER['PHP_AUTH_USER'])) {
header('WWW-Authenticate: Basic realm="' . $tikidomain . '"');
header('HTTP/1.0 401 Unauthorized');
exit;
}
$attempt = $_SERVER['PHP_AUTH_USER'] ;
$pass = $_SERVER['PHP_AUTH_PW'] ;
list($res, $rest) = $userlib->validate_user_tiki($attempt, $pass);
if ($res == USER_VALID) {
global $_permissionContext;
$_permissionContext = new Perms_Context($attempt, false);
$_permissionContext->activate(true);
return true;
} else {
header('WWW-Authenticate: Basic realm="' . $tikidomain . '"');
header('HTTP/1.0 401 Unauthorized');
return false;
}
}
/**
* @param bool $acceptFeed
* @return array
*/
public static function get_accept_types($acceptFeed = false)
{
if (TIKI_API) {
return ['json' => 'application/json'];
}
$accept = explode(',', $_SERVER['HTTP_ACCEPT']);
if (isset($_REQUEST['httpaccept'])) {
$accept = array_merge(explode(',', $_REQUEST['httpaccept']), $accept);
}
$types = [];
foreach ($accept as $type) {
$known = null;
if (strpos($type, $t = 'application/json') !== false) {
$known = 'json';
} elseif (strpos($type, $t = 'text/javascript') !== false) {
$known = 'json';
} elseif (strpos($type, $t = 'text/x-yaml') !== false) {
$known = 'yaml';
} elseif (strpos($type, $t = 'application/rss+xml') !== false) {
$known = 'rss';
} elseif (strpos($type, $t = 'application/atom+xml') !== false) {
$known = 'atom';
}
if ($known && ! isset($types[$known])) {
$types[$known] = $t;
}
}
if (empty($types)) {
$types['html'] = 'text/html';
}
return $types;
}
/**
* @return bool
*/
public static function is_machine_request()
{
foreach (self::get_accept_types() as $name => $full) {
switch ($name) {
case 'html':
return false;
case 'json':
case 'yaml':
return true;
}
}
return false;
}
/**
* @param bool $acceptFeed
* @return bool
*/
public static function is_serializable_request($acceptFeed = false)
{
foreach (self::get_accept_types($acceptFeed) as $name => $full) {
switch ($name) {
case 'json':
case 'yaml':
return true;
case 'rss':
case 'atom':
if ($acceptFeed) {
return true;
}
}
}
return false;
}
/**
* @return bool
*/
public function is_xml_http_request()
{
return ! empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest';
}
/**
* Will process the output by serializing in the best way possible based on the request's accept headers.
* To output as an RSS/Atom feed, a descriptor may be provided to map the array data to the feed's properties
* and to supply additional information. The descriptor must contain the following keys:
* [feedTitle] Feed's title, static value
* [feedDescription] Feed's description, static value
* [entryTitleKey] Key to lookup for each entry to find the title
* [entryUrlKey] Key to lookup to find the URL of each entry
* [entryModificationKey] Key to lookup to find the modification date
* [entryObjectDescriptors] Optional. Array containing two key names, object key and object type to lookup missing information (url and title)
*/
public static function output_serialized($data, $feed_descriptor = null)
{
foreach (self::get_accept_types(! is_null($feed_descriptor)) as $name => $full) {
switch ($name) {
case 'json':
header("Content-Type: $full");
$data = json_encode($data);
if ($data === false) {
$error = '';
switch (json_last_error()) {
case JSON_ERROR_NONE:
$error = 'json_encode - No errors';
break;
case JSON_ERROR_DEPTH:
$error = 'json_encode - Maximum stack depth exceeded';
break;
case JSON_ERROR_STATE_MISMATCH:
$error = 'json_encode - Underflow or the modes mismatch';
break;
case JSON_ERROR_CTRL_CHAR:
$error = 'json_encode - Unexpected control character found';
break;
case JSON_ERROR_SYNTAX:
$error = 'json_encode - Syntax error, malformed JSON';
break;
case JSON_ERROR_UTF8:
$error = 'json_encode - Malformed UTF-8 characters, possibly incorrectly encoded';
break;
default:
$error = 'json_encode - Unknown error';
break;
}
throw new Exception($error);
}
if (isset($_REQUEST['callback'])) {
$data = $_REQUEST['callback'] . '(' . $data . ')';
}
echo $data;
return;
case 'yaml':
header("Content-Type: $full");
echo Yaml::dump($data, 20, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK);
return;
case 'rss':
$rsslib = TikiLib::lib('rss');
$writer = $rsslib->generate_feed_from_data($data, $feed_descriptor);
$writer->setFeedLink(self::tikiUrl($_SERVER['REQUEST_URI']), 'rss');
header('Content-Type: application/rss+xml');
echo $writer->export('rss');
return;
case 'atom':
$rsslib = TikiLib::lib('rss');
$writer = $rsslib->generate_feed_from_data($data, $feed_descriptor);
$writer->setFeedLink(self::tikiUrl($_SERVER['REQUEST_URI']), 'atom');
header('Content-Type: application/atom+xml');
echo $writer->export('atom');
return;
case 'html':
header("Content-Type: $full");
echo $data;
return;
}
}
}
/**
* @param $filename string The file name directory structure to test. May be an absolute or relative file path.
*
* @return bool|null Return true upon file access success, false upon failure, and null if the file does not exist.
*/
public function isFileWebAccessible(string $filename): ?bool
{
global $tikipath, $base_url_http, $base_url_https;
// if the directory is within the Tiki root, then remove the prefixed Tiki root
if (0 === strpos($filename, $tikipath)) {
$filename = substr($filename, strlen($tikipath));
}
// if the file does not exist, then don't bother proceeding further
if (! file_exists($filename)) {
return null;
}
// if the file is outside the Tiki Root
if ($filename[0] === '/') {
return false;
}
// now load try accessing the file and check for a 200 (ok) or 300 (moved)
// lets check http first
$response = @get_headers($base_url_http . $filename);
$response = substr($response[0], 9, 1);
if ($response == '2' || $response == '3') {
return true;
} else { // now we try https, just to be sure.
$response = @get_headers($base_url_https . $filename);
$response = substr($response[0], 9, 1);
if ($response == '2' || $response == '3') {
return true;
}
}
// if all else has failed, conclude that the file is not accessible
return false;
}
/**
* @param string $mode closed|busy
*
* @return void
*/
public function showSiteClosed($mode)
{
global $prefs, $error_login;
switch ($mode) {
case 'busy':
$title = $prefs['site_busy_title'];
$error = $prefs['site_busy_msg'];
$prefs['site_closed'] = 'y'; // tell the rest of tiki we're closed
break;
case 'closed':
default:
$title = $prefs['site_closed_title'];
$error = $prefs['site_closed_msg'];
}
if (TIKI_API) {
$this->output_serialized(['error' => $error, 'title' => $title]);
die;
}
if ($prefs['twoFactorAuth'] === 'y' && $error_login === tra('Invalid two-factor code ')) {
$twoFactorAuthCode = '<div class="pass form-group row mx-0 clearfix">
<label for="login-2fa">Two-factor Authenticator Code</label>
<input type="text" name="twoFactorAuthCode" autocomplete="off" class="form-control" id="login-2fa">
</div>
';
$error_login = '';
} else {
$twoFactorAuthCode = '';
}
$style_alert = '' . $error_login != '' ? 'alert alert-danger' : '';
$style_alert_btn = '' . $error_login != '' ? '' : 'display:none';
require_once('lib/setup/cookies.php');
$this->setTicket();
$login = '<form class="form-detail" id="myform" action="tiki-login.php?page=tikiIndex" method="post">
<div class="form-row">
<label>User</label>
<input type="text" name="user" id="your_email" class="input-text" required>
</div>
' . $twoFactorAuthCode . '
<div class="form-row form-row-1">
<label for="password">Password</label>
<input type="password" name="pass" id="password" class="input-text" required>
</div>
<div class="' . $style_alert . '" role="alert">' . $error_login . '
<button type="button" class="close" style="' . $style_alert_btn . '" data-bs-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="form-row-last">
<input type="hidden" class="ticket" name="ticket" value="' . $this->ticket . '" />
<input type="hidden" name="confirmForm" value="y" />
<input type="submit" name="login" value="Log in" class="register">
</div>
</form>';
if (file_exists('themes/base_files/other/site_closed_local.html')) {
$html = file_get_contents('themes/base_files/other/site_closed_local.html');
} else {
$html = file_get_contents('themes/base_files/other/site_closed.html');
}
$html = str_replace('{error}', $error, $html);
$html = str_replace('{title}', $title, $html);
$html = str_replace('{login}', $login, $html);
header("HTTP/1.0 503 Service Unavailable");
die($html);
}
}