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