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
8require_once(__DIR__ . '/../lib/debug/Tracer.php');
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	exit;
14}
15
16// This class is included by all the Tiki php scripts, so it's important
17// to keep the class as small as possible to improve performance.
18// What goes in this class:
19// * generic functions that MANY scripts must use
20// * shared functions (marked as /*shared*/) are functions that are
21//   called from Tiki modules.
22
23/**
24 *
25 */
26class TikiLib extends TikiDb_Bridge
27{
28	public $buffer;
29	public $flag;
30	public $usergroups_cache = [];
31
32	public $num_queries = 0;
33	public $now;
34
35	public $cache_page_info = [];
36	public $sessionId = null;
37
38	/**
39	 * Collection of Tiki libraries.
40	 * Populated by TikiLib::lib()
41	 * @var array
42	 */
43	protected static $libraries = [];
44
45	protected static $isExternalContext = false;
46
47	/** Gets a library reference
48	 *
49	 * @param $name string        The name of the library as specified in the id attribute in db/config/tiki.xml
50	 * @return object|\AccountingLib|\ActivityLib|\AdminLib|\AreasLib|\ArtLib|\AttributeLib|\AutoSaveLib|\BannerLib|\BigBlueButtonLib|\blacklistLib|\ocrLib|\BlogLib|\CacheLib|\CalendarLib|\Captcha|\CartLib|\CategLib|\Comments|\ContactLib|\ContributionLib|\CreditsLib|\CryptLib|\cssLib|\Tiki\CustomRoute\CustomRouteLib|\DCSLib|\EditLib|\ErrorReportLib|\FaqLib|\FederatedSearchLib|\FileGalBatchLib|\FileGalLib|\FlaggedRevisionLib|\FreetagLib|\GeoLib|\GoalEventLib|\GoalLib|\GoalRewardLib|\GroupAlertLib|\H5PLib|\HeaderLib|\HistLib|\IconsetLib|\ImageGalsLib|\KalturaLib|\KalturaLib|\Language|\LanguageTranslations|\LdapLib|\LoginLib|\LogsLib|\LogsQueryLib|\MailinLib|\Memcachelib|\MenuLib|\Messu|\MimeLib|\ModLib|\MonitorLib|\MonitorMailLib|\MultilingualLib|\NotificationLib|\OAuthLib|\ObjectLib|\PageContentLib|\ParserLib|\PaymentLib|\PdfImagesLib\PerspectiveLib|\PollLib|\PreferencesLib|\QuantifyLib|\QueueLib|\QuizLib|\RatingConfigLib|\RatingLib|\ReferencesLib|\RegistrationLib|\RelationLib|\RSSLib|\SchedulersLib|\ScoreLib|\ScormLib|\SearchStatsLib|\SemanticLib|\ServiceLib|\SheetLib|\Smarty_Tiki|\SocialLib|\StatsLib|\StoredSearchLib|\StructLib|\TemplatesLib|\ThemeControlLib|\ThemeLib|\Tiki_Connect_Client|\Tiki_Connect_Server|\Tiki_Event_Manager|\Tiki_Profile_SymbolLoader|\Tiki\Object\Selector|\Tiki\Recommendation\BatchProcessor|\Tiki\Wiki\SlugManager|\TikiAccessLib|\TikiCalendarLib|\TikiDate|\TodoLib|\Tracker\Tabular\Manager|\TrackerLib|\TWVersion|\UnifiedSearchLib|\UserMailinLib|\UserModulesLib|\UserPrefsLib|\UsersLib|\Validators|\VimeoLib|\VueJsLib|\WikiLib|\WizardLib|\WYSIWYGLib|\XMPPLib|\ZoteroLib
51	 * @throws Exception
52	 */
53	public static function lib($name)
54	{
55		if (isset(self::$libraries[$name])) {
56			return self::$libraries[$name];
57		}
58
59		$container = TikiInit::getContainer();
60
61		//if no period in the lib name, default to tiki.lib prefix.
62		if (strpos($name, ".") !== false) {
63			$service = $name;
64		} else {
65			$service = "tiki.lib.$name";
66		}
67
68		if ($lib = $container->get($service, \Symfony\Component\DependencyInjection\ContainerInterface::NULL_ON_INVALID_REFERENCE)) {
69			return $lib;
70		}
71
72		// One-time inits of the libraries provided
73		switch ($name) {
74			case 'tiki':
75				global $tikilib;
76				return self::$libraries[$name] = $tikilib;
77		}
78
79		if (file_exists(__DIR__ . '/../temp/cache/container.php')) {
80			unlink(__DIR__ . '/../temp/cache/container.php'); // Remove the container cache to help transition
81		}
82
83		throw new Exception(tr("%0 library not found. This may be due to a typo or caused by a recent update.", $name));
84	}
85
86	/**
87	 * @return Tiki_Event_Manager
88	 * @throws Exception
89	 */
90	public static function events()
91	{
92		return self::lib('events');
93	}
94
95	/**
96	 * @return Tiki_Profile_SymbolLoader
97	 * @throws Exception
98	 */
99	public static function symbols()
100	{
101		return self::lib('symbols');
102	}
103
104	/**
105	 * @return mixed
106	 */
107	public function get_site_hash()
108	{
109		global $prefs;
110
111		if (! isset($prefs['internal_site_hash'])) {
112			$hash = $this->generate_unique_sequence();
113
114			$this->set_preference('internal_site_hash', $hash);
115		}
116
117		return $prefs['internal_site_hash'];
118	}
119
120	/**
121	 * Generates cryptographically secure pseudo-random sequence of bytes encoded into the base 64 character set
122	 *
123	 * @param int $entropy		Number of bytes to return
124	 * @param bool $urlSafe		If true, substitutes '-' and '_', for '+' and '_', and strips the '=' padding
125	 * 								character for url safe sequence.
126	 * @return string
127	 */
128	public function generate_unique_sequence($entropy = 100, $urlSafe = false)
129	{
130		$random_value = \phpseclib\Crypt\Random::string($entropy);
131		$encoded_value = base64_encode($random_value);
132		return $urlSafe ? strtr(str_replace('=', '', $encoded_value), '+/', '-_')
133			: $encoded_value;
134	}
135
136	// DB param left for interface compatibility, although not considered
137	/**
138	 * @param null $db
139	 */
140	function __construct($db = null)
141	{
142		$this->now = time();
143	}
144
145	function allocate_extra($type, $callback)
146	{
147		global $prefs;
148
149		$memory_name = 'allocate_memory_' . $type;
150		$time_name = 'allocate_time_' . $type;
151
152		if (! empty($prefs[$memory_name])) {
153			$memory_limit = new Tiki_MemoryLimit($prefs[$memory_name]);
154		}
155
156		if (! empty($prefs[$time_name])) {
157			$time_limit = new Tiki_TimeLimit($prefs[$time_name]);
158		}
159
160		return call_user_func($callback);
161	}
162
163	/**
164	 * @param bool $url
165	 * @param array $options
166	 * @return mixed|Zend\Http\Client
167	 */
168	function get_http_client($url = false, $options = null)
169	{
170		global $prefs;
171
172		$config = [
173			'timeout' => 10,
174			'keepalive' => true,
175		];
176
177		if ($prefs['use_proxy'] == 'y') {
178			$config['adapter'] = 'Laminas\Http\Client\Adapter\Proxy';
179			$config["proxy_host"] = $prefs['proxy_host'];
180			$config["proxy_port"] = $prefs['proxy_port'];
181
182			if ($prefs['proxy_user'] || $prefs['proxy_pass']) {
183				$config["proxy_user"] = $prefs['proxy_user'];
184				$config["proxy_pass"] = $prefs['proxy_pass'];
185			}
186		} elseif (function_exists('curl_init') && $prefs['zend_http_use_curl'] === 'y') {
187			// Zend\Http\Client defaults to sockets, which aren't allowed in all environments so use curl when available if selected
188			$config['adapter'] = 'Laminas\Http\Client\Adapter\Curl';
189		}
190
191		if ($prefs['zend_http_sslverifypeer'] == 'y') {
192			$config['sslverifypeer'] = true;
193		} else {
194			$config['sslverifypeer'] = false;
195		}
196
197
198		if (is_array($options)) {
199			foreach ($options as $key => $value) {
200				$config[$key] = $value;
201			}
202		}
203
204		$client = new Zend\Http\Client(null, $config);
205		$client->setArgSeparator('&');
206
207		if ($url) {
208			$client = $this->prepare_http_client($client, $url);
209
210			$client->setUri($this->urlencode_accent($url));	// Zend\Http\Client seems to fail with accents in urls (jb june 2011)
211		}
212
213		return $client;
214	}
215
216	/**
217	 * @param $client
218	 * @param $url
219	 * @return mixed
220	 */
221	private function prepare_http_client($client, $url)
222	{
223		$info = parse_url($url);
224
225		// Obtain all methods matching the scheme and domain
226		$table = $this->table('tiki_source_auth');
227		$authentications = $table->fetchAll(
228			['path', 'method', 'arguments'],
229			['scheme' => $info['scheme'],'domain' => $info['host']]
230		);
231
232		// Obtain the method with the longest path matching
233		$max = -1;
234		$method = false;
235		$arguments = false;
236		foreach ($authentications as $auth) {
237			if (0 === strpos($info['path'], $auth['path'])) {
238				$len = strlen($auth['path']);
239				if ($len > $max) {
240					$max = $len;
241					$method = $auth['method'];
242					$arguments = $auth['arguments'];
243				}
244			}
245		}
246
247		if ($method) {
248			$functionName = 'prepare_http_auth_' . $method;
249			if (method_exists($this, $functionName)) {
250				$arguments = json_decode($arguments, true);
251				return $this->$functionName($client, $arguments);
252			}
253		} else {
254			// Nothing special to do
255			return $client;
256		}
257	}
258
259	/**
260	 * @param $client
261	 * @param $arguments
262	 * @return mixed
263	 */
264	private function prepare_http_auth_basic($client, $arguments)
265	{
266		$client->setAuth($arguments['username'], $arguments['password'], Zend\Http\Client::AUTH_BASIC);
267
268		return $client;
269	}
270
271	/**
272	 * @param $client
273	 * @param $arguments
274	 * @return mixed
275	 */
276	private function prepare_http_auth_get($client, $arguments)
277	{
278		$url = $arguments['url'];
279
280		$client->setUri($this->urlencode_accent($url)); // Zend\Http\Client seems to fail with accents in urls
281		$client->setMethod(Zend\Http\Request::METHOD_GET);
282		$response = $client->send();
283		$client->resetParameters();
284
285		return $client;
286	}
287
288	/**
289	 * @param $client
290	 * @param $arguments
291	 * @return mixed
292	 */
293	private function prepare_http_auth_post($client, $arguments)
294	{
295		$url = $arguments['post_url'];
296		unset($arguments['post_url']);
297
298		$client->setUri($this->urlencode_accent($url)); // Zend\Http\Client seems to fail with accents in urls
299		$client->setMethod(Zend\Http\Request::METHOD_GET);
300		$response = $client->send();
301		$client->resetParameters();
302
303		$client->setUri($this->urlencode_accent($url)); // Zend\Http\Client seems to fail with accents in urls
304		$client->setParameterPost($arguments);
305		$client->setMethod(Zend\Http\Request::METHOD_POST);
306		$response = $client->send();
307		$client->resetParameters();
308
309		// check for oauth2 password post returning a Authorization: Bearer token
310		if (! empty($arguments['grant_type']) && $arguments['grant_type'] === 'password') {	// TODO other grant_types may need this too
311			$body = json_decode($response->getBody());
312			if ($body && $body->access_token) {
313				$headers = $client->getRequest()->getHeaders();
314				// add the Bearer token to the request headers
315				$headers->addHeader(new Zend\Http\Header\Authorization('Bearer ' . $body->access_token));
316				$client->setHeaders($headers);
317			}
318		}
319
320		return $client;
321	}
322
323	/**
324	 * Authorization header method
325	 *
326	 * @param $client     \Zend\Http\Client
327	 * @param $arguments  array
328	 * @return \Zend\Http\Client
329	 */
330	private function prepare_http_auth_header($client, $arguments)
331	{
332		$url = $arguments['url'];
333
334		$client->setUri($this->urlencode_accent($url)); // Zend\Http\Client seems to fail with accents in urls
335		$client->setMethod(Zend\Http\Request::METHOD_GET);
336
337		$headers = $client->getRequest()->getHeaders();
338		$headers->addHeader(new Zend\Http\Header\Authorization($arguments['header']));
339		$client->setHeaders($headers);
340
341		return $client;
342	}
343
344	/**
345	 * @param $client
346	 * @return mixed
347	 */
348	function http_perform_request($client)
349	{
350		global $prefs;
351		$response = $client->send();
352
353		$attempts = 0;
354		while ($response->isRedirect() && $attempts < 10) { // prevent redirect loop
355			$client->setUri($client->getUri());
356			$response = $client->send();
357			$attempts++;
358		}
359
360		if ($prefs['http_skip_frameset'] == 'y') {
361			if ($outcome = $this->http_perform_request_skip_frameset($client, $response)) {
362				return $outcome;
363			}
364		}
365
366		return $response;
367	}
368
369	/**
370	 * @param $client
371	 * @param $response
372	 * @return mixed
373	 */
374	private function http_perform_request_skip_frameset($client, $response)
375	{
376		// Only attempt if document is declared as HTML
377		if (0 === strpos($response->getHeaders()->get('Content-Type'), 'text/html')) {
378			$use_int_errors = libxml_use_internal_errors(true); // suppress errors and warnings due to bad HTML
379			$dom = new DOMDocument;
380			if ($response->getBody() && $dom->loadHTML($response->getBody())) {
381				$frames = $dom->getElementsByTagName('frame');
382
383				if (count($frames)) {
384					// Frames were found
385					foreach ($frames as $f) {
386						// Request with the first frame where scrolling is not disabled (likely to be a menu or some other web 2.0 helper)
387						if ($f->getAttribute('scrolling') != 'no') {
388							$client->setUri($this->http_get_uri($client->getUri(), $this->urlencode_accent($f->getAttribute('src'))));
389							libxml_clear_errors();
390							libxml_use_internal_errors($use_int_errors);
391							return $client->send();
392						}
393					}
394				}
395			}
396			libxml_clear_errors();
397			libxml_use_internal_errors($use_int_errors);
398		}
399	}
400
401	/**
402	 * @param Zend\Uri\Http $uri
403	 * @param $relative
404	 * @return Zend\Uri\Http
405	 */
406	function http_get_uri(Zend\Uri\Http $uri, $relative)
407	{
408		if (strpos($relative, 'http://') === 0 || strpos($relative, 'https://') === 0) {
409			$uri = new Zend\Uri\Http($relative);
410		} else {
411			$uri = clone $uri;
412			$uri->setQuery([]);
413			$parts = explode('?', $relative, 2);
414			$relative = $parts[0];
415
416			if ($relative{0} === '/') {
417				$uri->setPath($relative);
418			} else {
419				$path = dirname($uri->getPath());
420				if ($path === '/') {
421					$path = '';
422				}
423
424				$uri->setPath("$path/$relative");
425			}
426
427			if (isset($parts[1])) {
428				$uri->setQuery($parts[1]);
429			}
430		}
431
432		return $uri;
433	}
434
435	/**
436	 * @param $url
437	 * @param string $reqmethod
438	 * @return bool
439	 */
440	function httprequest($url, $reqmethod = "GET")
441	{
442		// test url :
443		// rewrite url if sloppy # added a case for https urls
444		if ((substr($url, 0, 7) <> "http://") and
445				(substr($url, 0, 8) <> "https://")
446			 ) {
447			$url = "http://" . $url;
448		}
449
450		try {
451			$client = $this->get_http_client($url);
452			/* @var $response Zend\Http\Response */
453			$response = $this->http_perform_request($client);
454
455			if (! $response->isSuccess()) {
456				return false;
457			}
458
459			return $response->getBody();
460		} catch (Zend\Http\Exception\ExceptionInterface $e) {
461			return false;
462		}
463	}
464
465	/*shared*/
466	/**
467	 * @param $name
468	 * @return bool
469	 */
470	function get_dsn_by_name($name)
471	{
472		if ($name == 'local') {
473			return true;
474		}
475		return $this->table('tiki_dsn')->fetchOne('dsn', ['name' => $name]);
476	}
477
478	/**
479	 * @param $name
480	 * @return array
481	 */
482	function get_dsn_info($name)
483	{
484		$info = [];
485
486		$dsnsqlplugin = $this->get_dsn_by_name($name);
487
488		$parsedsn = $dsnsqlplugin;
489		$info['driver'] = strtok($parsedsn, ":");
490		$parsedsn = substr($parsedsn, strlen($info['driver']) + 3);
491		$info['user'] = strtok($parsedsn, ":");
492		$parsedsn = substr($parsedsn, strlen($info['user']) + 1);
493		$info['password'] = strtok($parsedsn, "@");
494		$parsedsn = substr($parsedsn, strlen($info['password']) + 1);
495		$info['host'] = strtok($parsedsn, "/");
496		$parsedsn = substr($parsedsn, strlen($info['host']) + 1);
497		$info['database'] = $parsedsn;
498
499		return $info;
500	}
501
502	/**
503	 * @param $name
504	 * @return mixed
505	 */
506	function get_db_by_name($name)
507	{
508		include_once('tiki-setup.php');
509		if ($name == 'local' || empty($name)) {
510			return TikiDb::get();
511		}
512
513		try {
514			static $connectionMap = [];
515
516			if (! isset($connectionMap[$name])) {
517				$connectionMap[$name] = false;
518
519				$info = $this->get_dsn_info($name);
520				$dbdriver = $info['driver'];
521				$dbuserid = $info['user'];
522				$dbpassword = $info['password'];
523				$dbhost = $info['host'];
524				$database = $info['database'];
525
526				$api_tiki = null;
527				require 'db/local.php';
528				if (isset($api_tiki) &&  $api_tiki == 'adodb') {
529					// Force autoloading
530					if (! class_exists('ADOConnection')) {
531						return null;
532					}
533
534					$dbsqlplugin = ADONewConnection($dbdriver);
535					if ($dbsqlplugin->NConnect($dbhost, $dbuserid, $dbpassword, $database)) {
536						$connectionMap[$name] = new TikiDb_AdoDb($dbsqlplugin);
537					}
538				} else {
539					$dbsqlplugin = new PDO("$dbdriver:host=$dbhost;dbname=$database", $dbuserid, $dbpassword);
540					$connectionMap[$name] = new TikiDb_Pdo($dbsqlplugin);
541				}
542			}
543			return $connectionMap[$name];
544		} catch (Exception $e) {
545			Feedback::error($e->getMessage());
546		}
547	}
548
549	/*shared*/
550	// Returns IP address or IP address forwarded by the proxy if feature load balancer is set
551	/**
552	 * @param $firewall true to detect ip behind a firewall
553	 * @return null|string
554	 */
555	function get_ip_address($firewall = 0)
556	{
557		global $prefs;
558		if ($firewall || (isset($prefs['feature_loadbalancer']) && $prefs['feature_loadbalancer'] === "y")) {
559			$header_checks = [
560				'HTTP_CF_CONNECTING_IP',
561				'HTTP_CLIENT_IP',
562				'HTTP_PRAGMA',
563				'HTTP_XONNECTION',
564				'HTTP_CACHE_INFO',
565				'HTTP_XPROXY',
566				'HTTP_PROXY',
567				'HTTP_PROXY_RENAMED',
568				'HTTP_PROXY_CONNECTION',
569				'HTTP_VIA',
570				'HTTP_X_COMING_FROM',
571				'HTTP_COMING_FROM',
572				'HTTP_X_FORWARDED_FOR',
573				'HTTP_X_FORWARDED',
574				'HTTP_X_CLUSTER_CLIENT_IP',
575				'HTTP_FORWARDED_FOR',
576				'HTTP_FORWARDED',
577				'HTTP_CACHE_CONTROL',
578				'HTTP_X_REAL_IP',
579				'REMOTE_ADDR'];
580
581			foreach ($header_checks as $key) {
582				if (array_key_exists($key, $_SERVER) === true) {
583					foreach (explode(',', $_SERVER[$key]) as $ip) {
584						$ip = trim($ip);
585
586						//filter the ip with filter functions
587						if (filter_var($ip, FILTER_VALIDATE_IP) !== false) {
588							return $ip;
589						}
590					}
591				}
592			}
593		}
594		if (isset($_SERVER['REMOTE_ADDR']) && filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP)) {
595			return $_SERVER['REMOTE_ADDR'];
596		} else {
597			return '0.0.0.0';
598		}
599	}
600
601	/*shared*/
602	/**
603	 * @param $user
604	 * @param $section
605	 * @return bool
606	 */
607	function check_rules($user, $section)
608	{
609		// Admin is never banned
610		if ($user == 'admin') {
611			return false;
612		}
613
614		$fullip = $this->get_ip_address();
615		$ips = explode(".", $fullip);
616		$query = "select tb.`message`,tb.`user`,tb.`ip1`,tb.`ip2`,tb.`ip3`,tb.`ip4`,tb.`mode` from `tiki_banning` tb, `tiki_banning_sections` tbs where tbs.`banId`=tb.`banId` and tbs.`section`=? and ( (tb.`use_dates` = ?) or (tb.`date_from` <= FROM_UNIXTIME(?) and tb.`date_to` >= FROM_UNIXTIME(?)))";
617		$result = $this->fetchAll($query, [$section,'n',(int)$this->now,(int)$this->now]);
618
619		foreach ($result as $res) {
620			if (! $res['message']) {
621				$res['message'] = tra('You are banned from') . ': ' . $section;
622			}
623
624			if ($user && $res['mode'] == 'user') {
625				// check user
626				$pattern = '/' . $res['user'] . '/';
627
628				if (preg_match($pattern, $user)) {
629					return $res['message'];
630				}
631			} else {
632				// check ip
633				if (count($ips) == 4) {
634					if (($ips[0] == $res['ip1'] || $res['ip1'] == '*') && ($ips[1] == $res['ip2'] || $res['ip2'] == '*')
635							&& ($ips[2] == $res['ip3'] || $res['ip3'] == '*') && ($ips[3] == $res['ip4'] || $res['ip4'] == '*')) {
636						return $res['message'];
637					}
638				}
639			}
640		}
641		return false;
642	}
643
644	// $noteId 0 means create a new note
645	/**
646	 * @param $user
647	 * @param $noteId
648	 * @param $name
649	 * @param $data
650	 * @param null $parse_mode
651	 * @return mixed
652	 */
653	function replace_note($user, $noteId, $name, $data, $parse_mode = null)
654	{
655		$data = $this->convertAbsoluteLinksToRelative($data);
656		$size = strlen($data);
657
658		$queryData = [
659			'user' => $user,
660			'name' => $name,
661			'data' => $data,
662			'created' => $this->now,
663			'lastModif' => $this->now,
664			'size' => (int) $size,
665			'parse_mode' => $parse_mode,
666		];
667
668		$userNotes = $this->table('tiki_user_notes');
669		if ($noteId) {
670			$userNotes->update($queryData, ['noteId' => (int) $noteId,]);
671		} else {
672			$noteId = $userNotes->insert($queryData);
673		}
674
675		return $noteId;
676	}
677
678	/**
679	 * @param $offset
680	 * @param $maxRecords
681	 * @param $sort_mode
682	 * @param $find
683	 * @return array
684	 */
685	function list_watches($offset, $maxRecords, $sort_mode, $find)
686	{
687		$mid = '';
688		$mid2 = '';
689		$bindvars1 = $bindvars2 = [];
690		if ($find) {
691			$mid = ' where `event` like ? or `email` like ? or `user` like ? or `object` like ? or `type` like ?';
692			$mid2 = ' where `event` like ? or `group` like ? or `object` like ? or `type` like ?';
693			$bindvars1 = ["%$find%", "%$find%", "%$find%", "%$find%", "%$find%"];
694			$bindvars2 = ["%$find%", "%$find%", "%$find%", "%$find%"];
695		}
696		$query = "select 'user' as watchtype, `watchId`, `user`, `event`, `object`, `title`, `type`, `url`, `email` from `tiki_user_watches` $mid
697			UNION ALL
698				select 'group' as watchtype, `watchId`, `group`, `event`, `object`, `title`, `type`, `url`, '' as `email`
699				from `tiki_group_watches` $mid2
700			order by " . $this->convertSortMode($sort_mode);
701		$query_cant = 'select count(*) from `tiki_user_watches` ' . $mid;
702		$query_cant2 = 'select count(*) from `tiki_group_watches` ' . $mid2;
703		$ret = $this->fetchAll($query, array_merge($bindvars1, $bindvars2), $maxRecords, $offset);
704		$cant = $this->getOne($query_cant, $bindvars1) + $this->getOne($query_cant2, $bindvars2);
705		$retval = [];
706		$retval["data"] = $ret;
707		$retval["cant"] = $cant;
708		return $retval;
709	}
710
711
712	/*shared*/
713	/**
714	 * @param      $user
715	 * @param      $event
716	 * @param      $object
717	 * @param null $type
718	 * @param null $title
719	 * @param null $url
720	 * @param null $email
721	 *
722	 * @return int
723	 * @throws Exception
724	 */
725	function add_user_watch($user, $event, $object, $type = null, $title = null, $url = null, $email = null)
726	{
727		// Allow a warning when the watch won't be effective
728		if (empty($email)) {
729			$userlib = TikiLib::lib('user');
730
731			$email = $userlib->get_user_email($user);
732			if (empty($email)) {
733				return false;
734			}
735		}
736
737		if ($event != 'auth_token_called') {
738			$this->remove_user_watch($user, $event, $object, $type, $email);
739		}
740
741		$userWatches = $this->table('tiki_user_watches');
742		return $userWatches->insert(
743			[
744				'user' => $user,
745				'event' => $event,
746				'object' => $object,
747				'email' => $email,
748				'type' => $type,
749				'title' => $title,
750				'url' => $url,
751			]
752		);
753	}
754
755	/**
756	 * @param $group
757	 * @param $event
758	 * @param $object
759	 * @param null $type
760	 * @param null $title
761	 * @param null $url
762	 * @return bool
763	 */
764	function add_group_watch($group, $event, $object, $type = null, $title = null, $url = null)
765	{
766
767		if ($type == 'Category' && $object == 0) {
768			return false;
769		} else {
770			$this->remove_group_watch($group, $event, $object, $type);
771			$groupWatches = $this->table('tiki_group_watches');
772			$groupWatches->insert(
773				[
774					'group' => $group,
775					'event' => $event,
776					'object' => $object,
777					'type' => $type,
778					'title' => $title,
779					'url' => $url,
780				]
781			);
782			return true;
783		}
784	}
785
786	/**
787	 * get_user_notification: returns the owner (user) related to a watchId
788	 *
789	 * @param mixed $id watchId
790	 * @access public
791	 * @return the user login related to the watchId
792	 */
793	function get_user_notification($id)
794	{
795
796		return $this->table('tiki_user_watches')->fetchOne('user', ['watchId' => $id]);
797	}
798	/*shared*/
799	/**
800	 * @param $id
801	 *
802	 * @return bool|TikiDb_Adodb_Result|TikiDb_Pdo_Result
803	 */
804	function remove_user_watch_by_id($id)
805	{
806		global $tiki_p_admin_notifications, $user;
807		if ($tiki_p_admin_notifications === 'y' or $user === $this->get_user_notification($id)) {
808			return $this->table('tiki_user_watches')->delete(['watchId' => (int) $id]);
809		}
810
811		return false;
812	}
813
814	/**
815	 * @param $id
816	 *
817	 * @return TikiDb_Adodb_Result|TikiDb_Pdo_Result
818	 */
819	function remove_group_watch_by_id($id)
820	{
821		return $this->table('tiki_group_watches')->delete(['watchId' => (int) $id,]);
822	}
823
824	/*shared*/
825	/**
826	 * @param string $user
827	 * @param string $event
828	 * @param string $object
829	 * @param string $type  = 'wiki page'
830	 * @param string $email = ''
831	 *
832	 * @return TikiDb_Adodb_Result|TikiDb_Pdo_Result
833	 */
834	function remove_user_watch($user, $event, $object, $type = 'wiki page', $email = '')
835	{
836		$conditions = [
837			'user' => $user,
838			'event' => $event,
839			'object' => $object,
840			'type' => $type,
841		];
842
843		if ($email) {
844			$conditions['email'] = $email;
845		}
846
847		return $this->table('tiki_user_watches')->deleteMultiple($conditions);
848	}
849
850	/*token notification*/
851	/**
852	 * @param $event
853	 * @param $object
854	 * @param string $type
855	 */
856	function remove_user_watch_object($event, $object, $type = 'wiki page')
857	{
858		$query = "delete from `tiki_user_watches` where `event`=? and `object`=? and `type` = ?";
859		$this->query($query, [$event,$object,$type]);
860	}
861
862	function remove_stale_comment_watches()
863	{
864		$query = "DELETE FROM `tiki_user_watches` WHERE `event` = 'thread_comment_replied' AND `object` NOT IN (SELECT `threadId` FROM `tiki_comments`)";
865		$this->query($query);
866	}
867
868	/**
869	 * @param $group
870	 * @param $event
871	 * @param $object
872	 * @param string $type
873	 */
874	function remove_group_watch($group, $event, $object, $type = 'wiki page')
875	{
876		$conditions = [
877			'group' => $group,
878			'event' => $event,
879			'object' => $object,
880		];
881		if (isset($type)) {
882			$conditions['type'] = $type;
883		}
884
885		$this->table('tiki_group_watches')->deleteMultiple($conditions);
886	}
887
888	/*shared*/
889	/**
890	 * @param $user
891	 * @param string $event
892	 * @return mixed
893	 */
894	function get_user_watches($user, $event = '')
895	{
896		$userWatches = $this->table('tiki_user_watches');
897
898		$conditions = [
899			'user' => $userWatches->exactly($user),
900		];
901
902		if ($event) {
903			$conditions['event'] = $event;
904		}
905
906		return $userWatches->fetchAll($userWatches->all(), $conditions);
907	}
908
909	/*shared*/
910	/**
911	 * @return array
912	 */
913	function get_watches_events()
914	{
915		$query = "select distinct `event` from `tiki_user_watches`";
916		$result = $this->fetchAll($query, []);
917		$ret = [];
918		foreach ($result as $res) {
919			$ret[] = $res['event'];
920		}
921		return $ret;
922	}
923
924	/*shared*/
925	/**
926	 * @param $user
927	 * @param $event
928	 * @param $object
929	 * @param null $type
930	 * @return bool
931	 */
932	function user_watches($user, $event, $object, $type = null)
933	{
934		$userWatches = $this->table('tiki_user_watches');
935
936		$conditions = [
937			'user' => $user,
938			'object' => $object,
939		];
940
941		if ($type) {
942			$conditions['type'] = $type;
943		}
944
945		if (is_array($event)) {
946			$conditions['event'] = $userWatches->in($event);
947
948			$ret = $userWatches->fetchColumn('event', $conditions);
949
950			return empty($ret) ? false : $ret;
951		} else {
952			return $userWatches->fetchCount($conditions);
953		}
954	}
955
956	/**
957	 * @param $object
958	 * @param $event
959	 * @param null $type
960	 * @return mixed
961	 */
962	function get_groups_watching($object, $event, $type = null)
963	{
964		$groupWatches = $this->table('tiki_group_watches');
965		$conditions = [
966			'object' => $object,
967			'event' => $event,
968		];
969
970		if ($type) {
971			$conditions['type'] = $type;
972		}
973
974		return $groupWatches->fetchColumn('group', $conditions);
975	}
976
977	/*shared*/
978	/**
979	 * @param $user
980	 * @param $event
981	 * @param $object
982	 * @return mixed
983	 */
984	function get_user_event_watches($user, $event, $object)
985	{
986		$userWatches = $this->table('tiki_user_watches');
987		return $userWatches->fetchAll(
988			$userWatches->all(),
989			[
990				'user' => $user,
991				'event' => $event,
992				'object' => is_array($object) ? $userWatches->in($object) : $object,
993			]
994		);
995	}
996
997	/*shared*/
998	/**
999	 * @param $event
1000	 * @param $object
1001	 * @param null $info
1002	 * @return array
1003	 */
1004	function get_event_watches($event, $object, $info = null)
1005	{
1006		global $prefs;
1007		$ret = [];
1008
1009		$mid = '';
1010		if ($prefs['feature_user_watches_translations'] == 'y'  && $event == 'wiki_page_changed') {
1011			// If $prefs['feature_user_watches_translations'] is turned on, also look for
1012			// pages in a translation group.
1013			$mid = "`event`=?";
1014			$bindvars[] = $event;
1015			$multilinguallib = TikiLib::lib('multilingual');
1016			$page_info = $this->get_page_info($object);
1017			$pages = $multilinguallib->getTranslations('wiki page', $page_info['page_id'], $object, '');
1018			foreach ($pages as $page) {
1019				$mids[] = "`object`=?";
1020				$bindvars[] = $page['objName'];
1021			}
1022			$mid .= ' and (' . implode(' or ', $mids) . ')';
1023		} elseif ($prefs['feature_user_watches_translations'] == 'y'
1024			&& $event == 'wiki_page_created' ) {
1025			$page_info = $this->get_page_info($object);
1026			$mid = "`event`='wiki_page_in_lang_created' and `object`=? and `type`='lang'";
1027			$bindvars[] = $page_info['lang'];
1028		} elseif ($prefs['feature_user_watches_languages'] == 'y' && $event == 'category_changed') {
1029			$mid = "`object`=? and ((`event`='category_changed_in_lang' and `type`=? ) or (`event`='category_changed'))";
1030			$bindvars[] = $object;
1031			$bindvars[] = $info['lang'];
1032		} elseif ($event == 'forum_post_topic') {
1033			$mid = "(`event`=? or `event`=?) and `object`=?";
1034			$bindvars[] = $event;
1035			$bindvars[] = 'forum_post_topic_and_thread';
1036			$bindvars[] = $object;
1037		} elseif ($event == 'forum_post_thread') {
1038			$mid = "(`event`=? and `object`=?) or ( `event`=? and `object`=?)";
1039			$bindvars[] = $event;
1040			$bindvars[] = $object;
1041			$bindvars[] = 'forum_post_topic_and_thread';
1042			$forumId = $info['forumId'];
1043			$bindvars[] = $forumId;
1044		} else {
1045			$extraEvents = "";
1046			if (substr_count($event, 'article_')) {
1047				$extraEvents = " or `event`='article_*'";
1048			} elseif ($event == 'wiki_comment_changes') {
1049				$extraEvents = " or `event`='wiki_page_changed'";
1050			// Blog comment mail
1051			} elseif ($event == 'blog_comment_changes') {
1052				$extraEvents = " or `event`='blog_page_changed'";
1053			}
1054			$mid = "(`event`=?$extraEvents) and (`object`=? or `object`='*')";
1055			$bindvars[] = $event;
1056			$bindvars[] = $object;
1057		}
1058
1059		// Obtain the list of watches on event/object for user watches
1060		// Union obtains all users member of groups being watched
1061		// Distinct union insures there are no duplicates
1062		$query = "select tuw.`watchId`, tuw.`user`, tuw.`event`, tuw.`object`, tuw.`title`, tuw.`type`, tuw.`url`, tuw.`email`,
1063				tup1.`value` as language, tup2.`value` as mailCharset
1064			from
1065				`tiki_user_watches` tuw
1066				left join `tiki_user_preferences` tup1 on (tup1.`user`=tuw.`user` and tup1.`prefName`='language')
1067				left join `tiki_user_preferences` tup2 on (tup2.`user`=tuw.`user` and tup2.`prefName`='mailCharset')
1068				where $mid
1069			UNION DISTINCT
1070			select tgw.`watchId`, uu.`login`, tgw.`event`, tgw.`object`, tgw.`title`, tgw.`type`, tgw.`url`, uu.`email`,
1071				tup1.`value` as language, tup2.`value` as mailCharset
1072			from
1073				`tiki_group_watches` tgw
1074				inner join `users_usergroups` ug on tgw.`group` = ug.`groupName`
1075				inner join `users_users` uu on ug.`userId` = uu.`userId` and uu.`email` is not null and uu.`email` <> ''
1076				left join `tiki_user_preferences` tup1 on (tup1.`user`=uu.`login` and tup1.`prefName`='language')
1077				left join `tiki_user_preferences` tup2 on (tup2.`user`=uu.`login` and tup2.`prefName`='mailCharset')
1078				where $mid
1079				";
1080		$result = $this->fetchAll($query, array_merge($bindvars, $bindvars));
1081
1082		if (count($result) > 0) {
1083			foreach ($result as $res) {
1084				if (empty($res['language'])) {
1085					$res['language'] = $this->get_preference('site_language');
1086				}
1087				switch ($event) {
1088					case 'wiki_page_changed':
1089					case 'wiki_page_created':
1090						$res['perm'] = ($this->user_has_perm_on_object($res['user'], $object, 'wiki page', 'tiki_p_view') ||
1091								$this->user_has_perm_on_object($res['user'], $object, 'wiki page', 'tiki_p_admin_wiki'));
1092						break;
1093					case 'tracker_modified':
1094						$res['perm'] = $this->user_has_perm_on_object($res['user'], $object, 'tracker', 'tiki_p_view_trackers');
1095						break;
1096					case 'tracker_item_modified':
1097						$res['perm'] = $this->user_has_perm_on_object($res['user'], $object, 'trackeritem', 'tiki_p_view_trackers');
1098						break;
1099					case 'blog_post':
1100						$res['perm'] = ($this->user_has_perm_on_object($res['user'], $object, 'blog', 'tiki_p_read_blog') ||
1101								$this->user_has_perm_on_object($res['user'], $object, 'blog', 'tiki_p_admin_blog'));
1102						break;
1103					// Blog comment mail
1104					case 'blog_comment_changes':
1105						$res['perm'] = ($this->user_has_perm_on_object($res['user'], $object, 'blog', 'tiki_p_read_blog') ||
1106								$this->user_has_perm_on_object($res['user'], $object, 'comments', 'tiki_p_read_comments'));
1107						break;
1108					case 'forum_post_topic':
1109						$res['perm'] = ($this->user_has_perm_on_object($res['user'], $object, 'forum', 'tiki_p_forum_read') ||
1110								$this->user_has_perm_on_object($res['user'], $object, 'forum', 'tiki_p_admin_forum'));
1111						break;
1112					case 'forum_post_thread':
1113						$res['perm'] = ($this->user_has_perm_on_object($res['user'], $object, 'thread', 'tiki_p_forum_read') ||
1114								$this->user_has_perm_on_object($res['user'], $object, 'forum', 'tiki_p_admin_forum'));
1115						break;
1116					case 'file_gallery_changed':
1117						$res['perm'] = ($this->user_has_perm_on_object($res['user'], $object, 'file gallery', 'tiki_p_view_file_gallery') ||
1118								$this->user_has_perm_on_object($res['user'], $object, 'file gallery', 'tiki_p_download_files'));
1119						break;
1120					case 'article_submitted':
1121					case 'article_edited':
1122					case 'article_deleted':
1123						$userlib = TikiLib::lib('user');
1124						$res['perm'] = (empty($object) && $userlib->user_has_permission($res['user'], 'tiki_p_read_article'))
1125							|| $this->user_has_perm_on_object($res['user'], $object, 'article', 'tiki_p_read_article');
1126						break;
1127					case 'topic_article_created':
1128					case 'topic_article_edited':
1129					case 'topic_article_deleted':
1130						$userlib = TikiLib::lib('user');
1131						$res['perm'] = (empty($object) && $userlib->user_has_permission($res['user'], 'tiki_p_read_article'))
1132							|| $this->user_has_perm_on_object($res['user'], $object, 'topic', 'tiki_p_read_article');
1133						break;
1134					case 'calendar_changed':
1135						$res['perm'] = $this->user_has_perm_on_object($res['user'], $object, 'calendar', 'tiki_p_view_calendar');
1136						break;
1137					case 'image_gallery_changed':
1138						$res['perm'] = $this->user_has_perm_on_object($res['user'], $object, 'image gallery', 'tiki_p_view_image_gallery');
1139						break;
1140					case 'category_changed':
1141						$categlib = TikiLib::lib('categ');
1142						$res['perm'] = $categlib->has_view_permission($res['user'], $object);
1143						break;
1144					case 'fgal_quota_exceeded':
1145						global $tiki_p_admin_file_galleries;
1146						$res['perm'] = ($tiki_p_admin_file_galleries == 'y');
1147						break;
1148					case 'article_commented':
1149					case 'wiki_comment_changes':
1150						$res['perm'] = $this->user_has_perm_on_object($res['user'], $object, 'comments', 'tiki_p_read_comments');
1151						break;
1152					case 'user_registers':
1153						$userlib = TikiLib::lib('user');
1154						$res['perm'] = $userlib->user_has_permission($res['user'], 'tiki_p_admin');
1155						break;
1156					case 'auth_token_called':
1157						$res['perm'] = true;
1158						break;
1159					case 'user_joins_group':
1160						$res['perm'] = $this->user_has_perm_on_object($res['user'], $object, 'group', 'tiki_p_group_view_members');
1161						break;
1162					case 'thread_comment_replied':
1163						$res['perm'] = true;
1164						break;
1165					default:
1166						// for security we deny all others.
1167						$res['perm'] = false;
1168						break;
1169				}
1170
1171				if ($res['perm'] || empty($res['user']) && ! empty($res['email'])) {
1172					// Allow admin created email (non-user) watches
1173					$ret[] = $res;
1174				}
1175			}
1176		}
1177
1178		// Also include users that are watching a category to which this object belongs to.
1179		if ($event != 'category_changed') {
1180			if ($prefs['feature_categories'] == 'y') {
1181				$categlib = TikiLib::lib('categ');
1182				$objectType = "";
1183				switch ($event) {
1184					case 'wiki_page_changed':
1185						$objectType = "wiki page";
1186						break;
1187					case 'wiki_page_created':
1188						$objectType = "wiki page";
1189						break;
1190					case 'blog_post':
1191						$objectType = "blog";
1192						break;
1193					// Blog comment mail
1194					case 'blog_page_changed':
1195						$objectType = "blog page";
1196						break;
1197					case 'map_changed':
1198						$objectType = "map_changed";
1199						break;
1200					case 'forum_post_topic':
1201						$objectType = "forum";
1202						break;
1203					case 'forum_post_thread':
1204						$objectType = "forum";
1205						break;
1206					case 'file_gallery_changed':
1207						$objectType = "file gallery";
1208						break;
1209					case 'article_submitted':
1210						$objectType = "topic";
1211						break;
1212					case 'image_gallery_changed':
1213						$objectType = "image gallery";
1214						break;
1215					case 'tracker_modified':
1216						$objectType = "tracker";
1217						break;
1218					case 'tracker_item_modified':
1219						$objectType = "tracker";
1220						break;
1221					case 'calendar_changed':
1222						$objectType = "calendar";
1223						break;
1224				}
1225				if ($objectType != "") {
1226					// If a forum post was changed, check the categories of the forum.
1227					if ($event == "forum_post_thread") {
1228						$commentslib = TikiLib::lib('comments');
1229						$object = $commentslib->get_comment_forum_id($object);
1230					}
1231
1232					// If a tracker item was changed, check the categories of the tracker.
1233					if ($event == "tracker_item_modified") {
1234						$trklib = TikiLib::lib('trk');
1235						$object = $trklib->get_tracker_for_item($object);
1236					}
1237
1238					$categs = $categlib->get_object_categories($objectType, $object);
1239
1240					foreach ($categs as $category) {
1241						$watching_users = $this->get_event_watches('category_changed', $category, $info);
1242
1243						// Add all users that are not already included
1244						foreach ($watching_users as $wu) {
1245							$included = false;
1246							foreach ($ret as $item) {
1247								if ($item['user'] == $wu['user']) {
1248									$included = true;
1249								}
1250							}
1251							if (! $included) {
1252								$ret[] = $wu;
1253							}
1254						}
1255					}
1256				}
1257			}
1258		}
1259		return $ret;
1260	}
1261
1262	/*shared*/
1263	/**
1264	 * @return array
1265	 */
1266	function dir_stats()
1267	{
1268		$sites = $this->table('tiki_directory_sites');
1269		$categories = $this->table('tiki_directory_categories');
1270		$search = $this->table('tiki_directory_search');
1271
1272		$aux = [];
1273		$aux["valid"] = $sites->fetchCount(['isValid' => 'y']);
1274		$aux["invalid"] = $sites->fetchCount(['isValid' => 'n']);
1275		$aux["categs"] = $categories->fetchCount([]);
1276		$aux["searches"] = $search->fetchOne($search->sum('hits'), []);
1277		$aux["visits"] = $search->fetchOne($sites->sum('hits'), []);
1278		return $aux;
1279	}
1280
1281	/*shared*/
1282	/**
1283	 * @param $offset
1284	 * @param $maxRecords
1285	 * @param $sort_mode
1286	 * @param $find
1287	 * @return array
1288	 */
1289	function dir_list_all_valid_sites2($offset, $maxRecords, $sort_mode, $find)
1290	{
1291		$sites = $this->table('tiki_directory_sites');
1292		$conditions = [
1293			'isValid' => 'y',
1294		];
1295
1296		if ($find) {
1297			$conditions['search'] = $sites->expr('(`name` like ? or `description` like ?)', ["%$find%", "%$find%"]);
1298		}
1299
1300		return [
1301			'data' => $sites->fetchAll($sites->all(), $conditions, $maxRecords, $offset, $sites->expr($this->convertSortMode($sort_mode))),
1302			'cant' => $sites->fetchCount($conditions),
1303		];
1304	}
1305
1306	/*shared*/
1307	/**
1308	 * @param $categId
1309	 * @return mixed
1310	 */
1311	function get_directory($categId)
1312	{
1313		return $this->table('tiki_directory_categories')->fetchFullRow(['categId' => $categId]);
1314	}
1315
1316	/*shared*/
1317	/**
1318	 * @param $user
1319	 * @return mixed
1320	 */
1321	function user_unread_messages($user)
1322	{
1323		$messages = $this->table('messu_messages');
1324		return $messages->fetchCount(
1325			[
1326				'user' => $user,
1327				'isRead' => 'n',
1328			]
1329		);
1330	}
1331
1332	/*shared*/
1333	/**
1334	 * @return array
1335	 */
1336	function get_online_users()
1337	{
1338		if (! isset($this->online_users_cache)) {
1339			$this->update_session();
1340			$this->online_users_cache = [];
1341			$query = "select s.`user`, p.`value` as `realName`, `timestamp`, `tikihost` from `tiki_sessions` s left join `tiki_user_preferences` p on s.`user`<>? and s.`user` = p.`user` and p.`prefName` = 'realName' where s.`user` is not null;";
1342			$result = $this->fetchAll($query, ['']);
1343			foreach ($result as $res) {
1344				$res['user_information'] = $this->get_user_preference($res['user'], 'user_information', 'public');
1345				$res['allowMsgs'] = $this->get_user_preference($res['user'], 'allowMsgs', 'y');
1346				$this->online_users_cache[$res['user']] = $res;
1347			}
1348		}
1349		return $this->online_users_cache;
1350	}
1351
1352	/*shared*/
1353	/**
1354	 * @param $whichuser
1355	 * @return bool
1356	 */
1357	function is_user_online($whichuser)
1358	{
1359		if (! isset($this->online_users_cache)) {
1360			$this->get_online_users();
1361		}
1362
1363		return(isset($this->online_users_cache[$whichuser]));
1364	}
1365
1366	/*
1367	 * Score methods begin
1368	 */
1369	// All information about an event type
1370	// shared
1371	/**
1372	 * @param $event
1373	 * @return mixed
1374	 */
1375	function get_event($event)
1376	{
1377		return $this->table('tiki_score')->fetchFullRow(['event' => $event]);
1378	}
1379
1380	// List users by best scoring
1381	// shared
1382	/**
1383	 * @param int $limit
1384	 * @param int $start
1385	 * @return mixed
1386	 */
1387	function rank_users($limit = 10, $start = 0)
1388	{
1389		global $prefs;
1390		$score_expiry_days = $prefs['feature_score_expday'];
1391
1392		if (! $start) {
1393			$start = "0";
1394		}
1395
1396		if (empty($score_expiry_days)) {
1397			// score does not expire
1398			$query = "select `recipientObjectId` as `login`,
1399				`pointsBalance` as `score`
1400				from `tiki_object_scores` tos
1401				where `recipientObjectType`='user'
1402				and tos.`id` = (select max(id) from `tiki_object_scores` where `recipientObjectId` = tos.`recipientObjectId` and `recipientObjectType`='user' group by `recipientObjectId`)
1403				group by `recipientObjectId`, `pointsBalance` order by `score` desc";
1404
1405			$result = $this->fetchAll($query, null, $limit, $start);
1406		} else {
1407			// score expires
1408			$query = "select `recipientObjectId` as `login`,
1409				`pointsBalance` - ifnull((select `pointsBalance` from `tiki_object_scores`
1410					where `recipientObjectId`=tos.`recipientObjectId`
1411					and `recipientObjectType`='user'
1412					and `date` < UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL ? DAY))
1413					order by id desc limit 1), 0) as `score`
1414				from `tiki_object_scores` tos
1415				where `recipientObjectType`='user'
1416				and tos.`id` = (select max(id) from `tiki_object_scores` where `recipientObjectId` = tos.`recipientObjectId` and `recipientObjectType`='user' group by `recipientObjectId`)
1417				group by `recipientObjectId`, `pointsBalance` order by `score` desc";
1418
1419			$result = $this->fetchAll($query, $score_expiry_days, $limit, $start);
1420		}
1421
1422		foreach ($result as & $res) {
1423			$res['position'] = ++$start;
1424		}
1425		return $result;
1426	}
1427
1428	// Returns html <img> tag to star corresponding to user's score
1429	// shared
1430	/**
1431	 * @param $score
1432	 * @return string
1433	 */
1434	function get_star($score)
1435	{
1436		global $prefs;
1437		$star = '';
1438		$star_colors = [0 => 'grey',
1439				100 => 'blue',
1440				500 => 'green',
1441				1000 => 'yellow',
1442				2500 => 'orange',
1443				5000 => 'red',
1444				10000 => 'purple'];
1445		foreach ($star_colors as $boundary => $color) {
1446			if ($score >= $boundary) {
1447				$star = 'star_' . $color . '.gif';
1448			}
1449		}
1450		if (! empty($star)) {
1451			$alt = sprintf(tra("%d points"), $score);
1452			if ($prefs['theme_iconset'] === 'legacy') {
1453				$star = "<img src='img/icons/$star' height='11' width='11' alt='$alt' />&nbsp;";
1454			} else {
1455				$smarty = TikiLib::lib('smarty');
1456				$smarty->loadPlugin('smarty_function_icon');
1457				$star = smarty_function_icon(['name' => 'star', 'istyle' => 'color:' . $color, 'iclass' => 'tips',
1458					'ititle' => ':' . $alt], $smarty->getEmptyInternalTemplate()) . "&nbsp;";
1459			}
1460		}
1461		return $star;
1462	}
1463
1464	/*
1465	 * Score methods end
1466	 */
1467	//shared
1468	// \todo remove all hardcoded html in get_user_avatar()
1469	/**
1470	 * @param $user
1471	 * @param string $float
1472	 * @return string
1473	 */
1474	function get_user_avatar($user, $float = '')
1475	{
1476		global $prefs;
1477
1478		if (empty($user)) {
1479			return '';
1480		}
1481
1482		if (is_array($user)) {
1483			$res = $user;
1484			$user = $user['login'];
1485		} else {
1486			$res = $this->table('users_users')->fetchRow(['login', 'avatarType', 'avatarLibName', 'email'], ['login' => $user]);
1487		}
1488
1489		if (! $res) {
1490			return '';
1491		}
1492
1493		if ($prefs['user_use_gravatar'] == 'y' && $res['email']) {
1494			$https_mode = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on';
1495			$hash = md5(strtolower(trim($res['email'])));
1496
1497			if ($https_mode) {
1498				$url = "https://secure.gravatar.com/avatar/$hash?s=45";
1499			} else {
1500				$url = "http://www.gravatar.com/avatar/$hash?s=45";
1501			}
1502			$type = 'g';
1503		} else {
1504			$type = $res["avatarType"] ? $res["avatarType"] : 'u';
1505			$libname = $res["avatarLibName"];
1506			$ret = '';
1507		}
1508
1509		$style = '';
1510
1511		if (strcasecmp($float, "left") == 0) {
1512			$style = "style='float:left;margin-right:5px;'";
1513		} elseif (strcasecmp($float, "right") == 0) {
1514			$style = "style='float:right;margin-left:5px;'";
1515		}
1516
1517		$username = htmlspecialchars(
1518			TikiLib::lib('user')->clean_user($user),
1519			ENT_COMPAT
1520		);
1521
1522		switch ($type) {
1523			case 'l':
1524				if ($libname) {
1525					$ret = '<img class="user-profile-picture rounded" width="45" height="45" src="' . $libname . '" ' . $style . ' alt="' . $username . '">';
1526				}
1527				break;
1528			case 'u':
1529				$userprefslib = TikiLib::lib('userprefs');
1530				$path = $userprefslib->get_public_avatar_path($user);
1531
1532				if ($path) {
1533					$url = $this->tikiUrlOpt($path);
1534					$ret = '<img class="user-profile-picture rounded" src="' . htmlspecialchars($url, ENT_NOQUOTES) . '" ' . $style . ' alt="' . $username . '">';
1535				}
1536				break;
1537			case 'g':
1538				$ret = '<img class="user-profile-picture rounded" src="' . htmlspecialchars($url, ENT_NOQUOTES) . '" ' . $style . ' alt="' . $username . '">';
1539				break;
1540			case 'n':
1541			default:
1542				$ret = '';
1543				break;
1544		}
1545		return $ret;
1546	}
1547
1548	/**
1549	 * Return user avatar as Base64 encoded inline image.
1550	 */
1551	function get_user_avatar_inline($user)
1552	{
1553		global $prefs;
1554
1555		if (empty($user)) {
1556			return '';
1557		}
1558
1559		if (is_array($user)) {
1560			$res = $user;
1561			$user = $user['login'];
1562		} else {
1563			$res = $this->table('users_users')->fetchRow(['login', 'avatarType', 'avatarFileType', 'avatarData', 'avatarLibName', 'email'], ['login' => $user]);
1564		}
1565
1566		if (! $res) {
1567			return '';
1568		}
1569
1570		if ($prefs['user_use_gravatar'] == 'y' && $res['email']) {
1571			$hash = md5(strtolower(trim($res['email'])));
1572			$url = "https://secure.gravatar.com/avatar/$hash.jpg?s=45";
1573			$data = file_get_contents($url);
1574			$mime = 'image/jpeg';
1575		} elseif ($res['avatarType'] == 'l') {
1576			$url = $this->tikiUrlOpt($res['avatarLibName']);
1577			$data = file_get_contents($url);
1578			if (class_exists('finfo')) {
1579				$finfo = new finfo(FILEINFO_MIME_TYPE);
1580				$mime = $finfo->buffer($data);
1581			} else {
1582				$mime = 'image/jpeg';
1583			}
1584		} else {
1585			$data = $res['avatarData'];
1586			$mime = $res['avatarFileType'];
1587		}
1588
1589		if ($data && $mime) {
1590			return "data:$mime;base64,".base64_encode($data);
1591		} else {
1592			return '';
1593		}
1594	}
1595
1596	/*shared*/
1597	/**
1598	 * @return array
1599	 */
1600	function get_forum_sections()
1601	{
1602		$query = "select distinct `section` from `tiki_forums` where `section`<>?";
1603		$result = $this->fetchAll($query, ['']);
1604		$ret = [];
1605		foreach ($result as $res) {
1606			$ret[] = $res["section"];
1607		}
1608		return $ret;
1609	}
1610
1611	/* Referer stats */
1612	/*shared*/
1613	/**
1614	 * @param $referer
1615	 * @param $fullurl
1616	 */
1617	function register_referer($referer, $fullurl)
1618	{
1619		$refererStats = $this->table('tiki_referer_stats');
1620
1621		$cant = $refererStats->fetchCount(['referer' => $referer]);
1622
1623		if ($cant) {
1624			$refererStats->update(
1625				[
1626					'hits' => $refererStats->increment(1),
1627					'last' => $this->now,
1628					'lasturl' => $fullurl,
1629				],
1630				['referer' => $referer]
1631			);
1632		} else {
1633			$refererStats->insert(
1634				[
1635					'last' => $this->now,
1636					'referer' => $referer,
1637					'hits' => 1,
1638					'lasturl' => $fullurl,
1639				]
1640			);
1641		}
1642	}
1643
1644	// File attachments functions for the wiki ////
1645	/*shared*/
1646	/**
1647	 * @param $id
1648	 * @return bool
1649	 */
1650	function add_wiki_attachment_hit($id)
1651	{
1652		global $prefs, $user;
1653		if (StatsLib::is_stats_hit()) {
1654			$wikiAttachments = $this->table('tiki_wiki_attachments');
1655			$wikiAttachments->update(
1656				['hits' => $wikiAttachments->increment(1)],
1657				['attId' => (int) $id]
1658			);
1659		}
1660		return true;
1661	}
1662
1663	/*shared*/
1664	/**
1665	 * @param $attId
1666	 * @return mixed
1667	 */
1668	function get_wiki_attachment($attId)
1669	{
1670		return $this->table('tiki_wiki_attachments')->fetchFullRow(['attId' => (int) $attId]);
1671	}
1672
1673	/*shared*/
1674	/**
1675	 * @param $id
1676	 * @return mixed
1677	 */
1678	function get_gallery($id)
1679	{
1680		return $this->table('tiki_galleries')->fetchFullRow(['galleryId' => (int) $id]);
1681	}
1682
1683	// Last visit module ////
1684	/*shared*/
1685	/**
1686	 * @param $user
1687	 * @return array|bool
1688	 */
1689	function get_news_from_last_visit($user)
1690	{
1691		if (! $user) {
1692			return false;
1693		}
1694
1695		$last = $this->table('users_users')->fetchOne('lastLogin', ['login' => $user]);
1696
1697		$ret = [];
1698		if (! $last) {
1699			$last = time();
1700		}
1701		$ret["lastVisit"] = $last;
1702		$ret["images"] = $this->getOne("select count(*) from `tiki_images` where `created`>?", [(int)$last]);
1703		$ret["pages"] = $this->getOne("select count(*) from `tiki_pages` where `lastModif`>?", [(int)$last]);
1704		$ret["files"] = $this->getOne("select count(*) from `tiki_files` where `created`>?", [(int)$last]);
1705		$ret["comments"] = $this->getOne("select count(*) from `tiki_comments` where `commentDate`>?", [(int)$last]);
1706		$ret["users"] = $this->getOne("select count(*) from `users_users` where `registrationDate`>? and `provpass`=?", [(int)$last, '']);
1707		$ret["trackers"] = $this->getOne("select count(*) from `tiki_tracker_items` where `lastModif`>?", [(int)$last]);
1708		$ret["calendar"] = $this->getOne("select count(*) from `tiki_calendar_items` where `lastmodif`>?", [(int)$last]);
1709		return $ret;
1710	}
1711
1712	/**
1713	 * @return mixed|string
1714	 */
1715	function pick_cookie()
1716	{
1717		$cant = $this->getOne("select count(*) from `tiki_cookies`", []);
1718		if (! $cant) {
1719			return '';
1720		}
1721
1722		$bid = rand(0, $cant - 1);
1723		//$cookie = $this->getOne("select `cookie`  from `tiki_cookies` limit $bid,1"); getOne seems not to work with limit
1724		$result = $this->query("select `cookie`  from `tiki_cookies`", [], 1, $bid);
1725		if ($res = $result->fetchRow()) {
1726			$cookie = str_replace("\n", "", $res['cookie']);
1727			return preg_replace('/^(.+?)(\s*--.+)?$/', '<em>"$1"</em>$2', $cookie);
1728		} else {
1729			return "";
1730		}
1731	}
1732
1733	function get_usage_chart_data()
1734	{
1735		TikiLib::lib('quiz')->compute_quiz_stats();
1736
1737		$data['xdata'][] = tra('wiki');
1738		$data['ydata'][] = $this->getOne('select sum(`hits`) from `tiki_pages`', []);
1739		$data['xdata'][] = tra('img-g');
1740		$data['ydata'][] = $this->getOne('select sum(`hits`) from `tiki_galleries`', []);
1741
1742		$data['xdata'][] = tra('file-g');
1743		$data['ydata'][] = $this->getOne('select sum(`hits`) from `tiki_file_galleries`', []);
1744
1745		$data['xdata'][] = tra('FAQs');
1746		$data['ydata'][] = $this->getOne('select sum(`hits`) from `tiki_faqs`', []);
1747
1748		$data['xdata'][] = tra('quizzes');
1749		$data['ydata'][] = $this->getOne('select sum(`timesTaken`) from `tiki_quiz_stats_sum`', []);
1750
1751		$data['xdata'][] = tra('arts');
1752		$data['ydata'][] = $this->getOne('select sum(`nbreads`) from `tiki_articles`', []);
1753
1754		$data['xdata'][] = tra('blogs');
1755		$data['ydata'][] = $this->getOne('select sum(`hits`) from `tiki_blogs`', []);
1756
1757		$data['xdata'][] = tra('forums');
1758		$data['ydata'][] = $this->getOne('select sum(`hits`) from `tiki_forums`', []);
1759
1760		return $data;
1761	}
1762
1763	// User assigned modules ////
1764	/*shared*/
1765	/**
1766	 * @param $id
1767	 * @return mixed
1768	 */
1769	function get_user_login($id)
1770	{
1771		return $this->table('users_users')->fetchOne('login', ['userId' => (int) $id]);
1772	}
1773
1774	/**
1775	 * @param $u
1776	 * @return int
1777	 */
1778	function get_user_id($u)
1779	{
1780		// Anonymous is not in db
1781		if ($u == '') {
1782			return -1;
1783		}
1784
1785		// If we ask for the current user id and if we already know it in session
1786		$current = ( isset($_SESSION['u_info']) && $u == $_SESSION['u_info']['login'] );
1787		if (isset($_SESSION['u_info']['id']) && $current) {
1788			return $_SESSION['u_info']['id'];
1789		}
1790
1791		// In other cases, we look in db
1792		$id = $this->table('users_users')->fetchOne('userId', ['login' => $u]);
1793		$id = ($id === false) ? -1 : $id;
1794		if ($current) {
1795			$_SESSION['u_info']['id'] = $id;
1796		}
1797		return $id;
1798	}
1799
1800	/*shared*/
1801	/**
1802	 * @param $group
1803	 * @return array
1804	 */
1805	function get_groups_all($group)
1806	{
1807		$result = $this->table('tiki_group_inclusion')->fetchColumn('groupName', ['includeGroup' => $group]);
1808		$ret = $result;
1809		foreach ($result as $res) {
1810			$ret = array_merge($ret, $this->get_groups_all($res));
1811		}
1812		return array_unique($ret);
1813	}
1814
1815	/*shared*/
1816	/**
1817	 * @param $group
1818	 * @return array
1819	 */
1820	function get_included_groups($group)
1821	{
1822		$result = $this->table('tiki_group_inclusion')->fetchColumn('includeGroup', ['groupName' => $group]);
1823		$ret = $result;
1824		foreach ($result as $res) {
1825			$ret = array_merge($ret, $this->get_included_groups($res));
1826		}
1827		return array_unique($ret);
1828	}
1829
1830	/*shared*/
1831	/**
1832	 * @param string  $user              username
1833	 * @param bool    $included_groups   include inherited/included groups
1834	 *
1835	 * @return array
1836	 */
1837	function get_user_groups($user, $included_groups = true)
1838	{
1839		global $prefs;
1840		$userlib = TikiLib::lib('user');
1841		if (empty($user) || $user === 'Anonymous') {
1842			$ret = [];
1843			$ret[] = "Anonymous";
1844			return $ret;
1845		}
1846		if ($prefs['feature_intertiki'] == 'y' and empty($prefs['feature_intertiki_mymaster']) and strstr($user, '@')) {
1847			$realm = substr($user, strpos($user, '@') + 1);
1848			if (isset($prefs['interlist'][$realm])) {
1849				$user = substr($user, 0, strpos($user, '@'));
1850				$groups = $prefs['interlist'][$realm]['groups'] . ',Anonymous';
1851				return explode(',', $groups);
1852			}
1853		}
1854		$cachekey = $user . ($included_groups ? '' : '_direct');
1855		if (! isset($this->usergroups_cache[$cachekey])) {
1856			$userid = $this->get_user_id($user);
1857			$result = $this->table('users_usergroups')->fetchColumn('groupName', ['userId' => $userid]);
1858			$ret = $result;
1859			if ($included_groups) {
1860				foreach ($result as $res) {
1861					$ret = array_merge($ret, $userlib->get_included_groups($res));
1862				}
1863			}
1864			$ret[] = "Registered";
1865
1866			if (isset($_SESSION["groups_are_emulated"]) && $_SESSION["groups_are_emulated"] == "y") {
1867				if (in_array('Admins', $ret)) {
1868					// Members of group 'Admins' can emulate being in any list of groups
1869					$ret = unserialize($_SESSION['groups_emulated']);
1870				} else {
1871					// For security purposes, user can only emulate a subset of user's list of groups
1872					// This prevents privilege escalation
1873					$ret = array_intersect($ret, unserialize($_SESSION['groups_emulated']));
1874				}
1875			}
1876			$ret = array_values(array_unique($ret));
1877			$this->usergroups_cache[$cachekey] = $ret;
1878			return $ret;
1879		} else {
1880			return $this->usergroups_cache[$cachekey];
1881		}
1882	}
1883
1884	/**
1885	 * @param $user
1886	 */
1887	function invalidate_usergroups_cache($user)
1888	{
1889		unset($this->usergroups_cache[$user]);
1890		unset($this->usergroups_cache[$user . '_direct']);
1891	}
1892
1893	/**
1894	 * @param $user
1895	 * @return string
1896	 */
1897	function get_user_cache_id($user)
1898	{
1899		$groups = $this->get_user_groups($user);
1900		sort($groups, SORT_STRING);
1901		$cacheId = implode(":", $groups);
1902		if ($user == 'admin') {
1903			// in this case user get permissions from no group
1904			$cacheId = 'ADMIN:' . $cacheId;
1905		}
1906		return $cacheId;
1907	}
1908
1909	/*shared*/
1910	/**
1911	 * @return string
1912	 * @see UsersLib::genPass(), which generates passwords easier to remember
1913	 * TODO: Merge with above
1914	 */
1915	static function genPass()
1916	{
1917		global $prefs;
1918		$length = max($prefs['min_pass_length'], 8);
1919		$list = ['aeiou', 'AEIOU', 'bcdfghjklmnpqrstvwxyz', 'BCDFGHJKLMNPQRSTVWXYZ', '0123456789'];
1920		$list[] = $prefs['pass_chr_special'] == 'y' ? '_*&+!*-=$@' : '_';
1921		shuffle($list);
1922		$r = '';
1923		for ($i = 0; $i < $length; $i++) {
1924			$ch = $list[$i % count($list)];
1925			$r .= $ch{rand(0, strlen($ch) - 1)};
1926		}
1927		return $r;
1928	}
1929
1930	// generate a random string (for unsubscription code etc.)
1931	/**
1932	 * @param string $base
1933	 * @return string
1934	 */
1935	function genRandomString($base = "")
1936	{
1937		if ($base == "") {
1938			$base = $this->genPass();
1939		}
1940		$base .= microtime();
1941		return md5($base);
1942	}
1943
1944	// This function calculates the pageRanks for the tiki_pages
1945	// it can be used to compute the most relevant pages
1946	// according to the number of links they have
1947	// this can be a very interesting ranking for the Wiki
1948	// More about this on version 1.3 when we add the pageRank
1949	// column to tiki_pages
1950	/**
1951	 * @param int $loops
1952	 * @return array
1953	 */
1954	function pageRank($loops = 16)
1955	{
1956		$pagesTable = $this->table('tiki_pages');
1957
1958		$ret = $pagesTable->fetchColumn('pageName', []);
1959
1960		// Now calculate the loop
1961		$pages = [];
1962
1963		foreach ($ret as $page) {
1964			$val = 1 / count($ret);
1965
1966			$pages[$page] = $val;
1967
1968			$pagesTable->update(['pageRank' => (int) $val], ['pageName' => $page]);
1969		}
1970
1971		for ($i = 0; $i < $loops; $i++) {
1972			foreach ($pages as $pagename => $rank) {
1973				// Get all the pages linking to this one
1974				// Fixed query.  -rlpowell
1975				$query = "select `fromPage`  from `tiki_links` where `toPage` = ? and `fromPage` not like 'objectlink:%'";
1976				// page rank does not count links from non-page objects TODO: full feature allowing this with options
1977				$result = $this->fetchAll($query, [$pagename]);
1978				$sum = 0;
1979
1980				foreach ($result as $res) {
1981					$linking = $res["fromPage"];
1982
1983					if (isset($pages[$linking])) {
1984						// Fixed query.  -rlpowell
1985						$q2 = "select count(*) from `tiki_links` where `fromPage`= ? and `fromPage` not like 'objectlink:%'";
1986						// page rank does not count links from non-page objects TODO: full feature allowing this with options
1987						$cant = $this->getOne($q2, [$linking]);
1988						if ($cant == 0) {
1989							$cant = 1;
1990						}
1991						$sum += $pages[$linking] / $cant;
1992					}
1993				}
1994
1995				$val = (1 - 0.85) + 0.85 * $sum;
1996				$pages[$pagename] = $val;
1997
1998				$pagesTable->update(['pageRank' => (int) $val], ['pageName' => $pagename]);
1999			}
2000		}
2001		arsort($pages);
2002		return $pages;
2003	}
2004
2005	/**
2006	 * @param $maxRecords
2007	 * @return array
2008	 */
2009	function list_recent_forum_topics($maxRecords)
2010	{
2011		$bindvars = ['forum', 0];
2012
2013		$query = 'select `threadId`, `forumId` from `tiki_comments`,`tiki_forums`'
2014			  . " where `object`=`forumId` and `objectType`=? and `parentId`=? order by " . $this->convertSortMode('commentDate_desc');
2015		$result = $this->fetchAll($query, $bindvars, $maxRecords * 3, 0); // Load a little more, for permission filters
2016		$res = $ret = $retids = [];
2017		$n = 0;
2018
2019		foreach ($result as $res) {
2020			$objperm = $this->get_perm_object($res['threadId'], 'thread', '', false);
2021			if ($objperm['tiki_p_forum_read'] == 'y') {
2022				$retids[] = $res['threadId'];
2023
2024				$n++;
2025
2026				if ($n >= $maxRecords) {
2027					break;
2028				}
2029			}
2030		}
2031
2032		if ($n > 0) {
2033			$query = 'select * from `tiki_comments`'
2034			  . ' where `threadId` in (' . implode(',', $retids) . ') order by ' . $this->convertSortMode('commentDate_desc');
2035			$ret = $this->fetchAll($query);
2036		}
2037
2038		$retval = [];
2039		$retval['data'] = $ret;
2040		$retval['cant'] = $n;
2041		return $retval;
2042	}
2043
2044	/*shared*/
2045	/**
2046	 * @param $forumId
2047	 * @param $offset
2048	 * @param $maxRecords
2049	 * @param $sort_mode
2050	 * @param $find
2051	 * @return array
2052	 */
2053	function list_forum_topics($forumId, $offset, $maxRecords, $sort_mode, $find)
2054	{
2055		$bindvars = [$forumId,$forumId,'forum',0];
2056		if ($find) {
2057			$findesc = '%' . $find . '%';
2058			$mid = " and (`title` like ? or `data` like ?)";
2059			$bindvars[] = $findesc;
2060			$bindvars[] = $findesc;
2061		} else {
2062			$mid = "";
2063		}
2064
2065		$query = "select * from `tiki_comments`,`tiki_forums` where ";
2066		$query .= " `forumId`=? and `object`=? and `objectType`=? and `parentId`=? $mid order by " . $this->convertSortMode($sort_mode);
2067		$query_cant = "select count(*) from `tiki_comments`,`tiki_forums` where ";
2068		$query_cant .= " `forumId`=? and `object`=? and `objectType`=? and `parentId`=? $mid";
2069		$ret = $this->fetchAll($query, $bindvars, $maxRecords, $offset);
2070		$cant = $this->getOne($query_cant, $bindvars);
2071
2072		$retval = [];
2073		$retval["data"] = $ret;
2074		$retval["cant"] = $cant;
2075		return $retval;
2076	}
2077
2078	/*shared*/
2079	/**
2080	 * @param $type
2081	 * @param $id
2082	 * @return bool
2083	 */
2084	function remove_object($type, $id)
2085	{
2086		global $prefs;
2087		$categlib = TikiLib::lib('categ');
2088		$objectlib = TikiLib::lib('object');
2089		$categlib->uncategorize_object($type, $id);
2090
2091		// Now remove comments
2092		$threads = $this->table('tiki_comments')->fetchColumn('threadId', ['object' => $id, 'objectType' => $type]);
2093		if (! empty($threads)) {
2094			$commentslib = TikiLib::lib('comments');
2095
2096			foreach ($threads as $threadId) {
2097				$commentslib->remove_comment($threadId);
2098			}
2099		}
2100
2101		// Remove individual permissions for this object if they exist
2102		$object = $type . $id;
2103		$this->table('users_objectpermissions')->deleteMultiple(['objectId' => md5($object), 'objectType' => $type]);
2104		// remove links from this object to pages
2105		$linkhandle = "objectlink:$type:$id";
2106		$this->table('tiki_links')->deleteMultiple(['fromPage' => $linkhandle]);
2107		// remove fgal backlinks
2108		if ($prefs['feature_file_galleries'] == 'y') {
2109			$filegallib = TikiLib::lib('filegal');
2110			$filegallib->deleteBacklinks(['type' => $type, 'object' => $id]);
2111		}
2112		// remove object
2113		$objectlib->delete_object($type, $id);
2114
2115		$objectAttributes = $this->table('tiki_object_attributes');
2116		$objectAttributes->deleteMultiple(['type' => $type,'itemId' => $id]);
2117
2118		$objectRelations = $this->table('tiki_object_relations');
2119		$objectRelations->deleteMultiple(['source_type' => $type,	'source_itemId' => $id]);
2120		$objectRelations->deleteMultiple(['target_type' => $type,	'target_itemId' => $id]);
2121
2122		return true;
2123	}
2124
2125	/*shared*/
2126	/**
2127	 * @param $offset
2128	 * @param $maxRecords
2129	 * @param $sort_mode
2130	 * @param string $find
2131	 * @param string $type
2132	 * @param string $structureName
2133	 * @return array
2134	 */
2135	function list_received_pages($offset, $maxRecords, $sort_mode, $find = '', $type = '', $structureName = '')
2136	{
2137		$bindvars = [];
2138		if ($type == 's') {
2139			$mid = ' `trp`.`structureName` is not null ';
2140		}
2141		if (! $sort_mode) {
2142			$sort_mode = '`structureName_asc';
2143		} elseif ($type == 'p') {
2144			$mid = ' `trp`.`structureName` is null ';
2145		}
2146		if (! $sort_mode) {
2147			$sort_mode = '`pageName_asc';
2148		} else {
2149			$mid = '';
2150		}
2151
2152		if ($find) {
2153			$findesc = '%' . $find . '%';
2154			if ($mid) {
2155				$mid .= ' and ';
2156			}
2157			$mid .= '(`trp`.`pageName` like ? or `trp`.`structureName` like ? or `trp`.`data` like ?)';
2158			$bindvars[] = $findesc;
2159			$bindvars[] = $findesc;
2160			$bindvars[] = $findesc;
2161		}
2162		if ($structureName) {
2163			if ($mid) {
2164				$mid .= ' and ';
2165			}
2166			$mid .= ' `trp`.`structureName`=? ';
2167			$bindvars[] = $structureName;
2168		}
2169		if ($mid) {
2170			$mid = "where $mid";
2171		}
2172
2173		$query = "select trp.*, tp.`pageName` as pageExists from `tiki_received_pages` trp left join `tiki_pages` tp on (tp.`pageName`=trp.`pageName`) $mid order by `structureName` asc, `pos` asc," . $this->convertSortMode($sort_mode);
2174		$query_cant = "select count(*) from `tiki_received_pages` trp $mid";
2175		$ret = $this->fetchAll($query, $bindvars, $maxRecords, $offset);
2176		$cant = $this->getOne($query_cant, $bindvars);
2177
2178		$retval = [];
2179		$retval["data"] = $ret;
2180		$retval["cant"] = $cant;
2181		return $retval;
2182	}
2183
2184	// User voting system ////
2185	// Used to vote everything (polls,comments,files,submissions,etc) ////
2186	// Checks if a user has voted
2187	/*shared*/
2188	/**
2189	 * @param $user
2190	 * @param $id
2191	 * @return bool
2192	 */
2193	function user_has_voted($user, $id)
2194	{
2195		global $prefs;
2196
2197		$ret = false;
2198
2199		if (isset($_SESSION['votes'])) {
2200			$votes = $_SESSION['votes'];
2201			if (is_array($votes) && in_array($id, $votes)) { // has already voted in the session (logged or not)
2202				return true;
2203			}
2204		}
2205
2206		if (! $user) {
2207			if ($prefs['ip_can_be_checked'] != 'y' && ! isset($_COOKIE[ session_name() ])) {// cookie has not been activated too bad for him
2208				$ret = true;
2209			} elseif (isset($_COOKIE[md5("tiki_wiki_poll_$id")])) {
2210				$ret = true;
2211			}
2212			// we have no idea if cookie was deleted  or if really he has not voted
2213		} else {
2214			$query = "select count(*) from `tiki_user_votings` where `user`=? and `id`=?";
2215			if ($this->getOne($query, [$user,(string) $id]) > 0) {
2216				$ret = true;
2217			}
2218		}
2219		if ($prefs['ip_can_be_checked'] == 'y') {
2220			$query = 'select count(*) from `tiki_user_votings` where `ip`=? and `id`=?';
2221			if ($this->getOne($query, [$this->get_ip_address(), $id]) > 0) {
2222				return true; // IP has already voted logged or not
2223			}
2224		}
2225		return $ret;
2226	}
2227
2228	// Registers a user vote
2229	/*shared*/
2230	/**
2231	 * @param $user
2232	 * @param $id
2233	 * @param bool $optionId
2234	 * @param array $valid_options
2235	 * @param bool $allow_revote
2236	 * @return bool
2237	 */
2238	function register_user_vote($user, $id, $optionId = false, array $valid_options = [], $allow_revote = false)
2239	{
2240		global $prefs;
2241
2242		// If an option is specified and the valid options are specified, skip the vote entirely if not valid
2243		if (false !== $optionId && count($valid_options) > 0 && ! in_array($optionId, $valid_options)) {
2244			return false;
2245		}
2246
2247		if ($user && ! $allow_revote && $this->user_has_voted($user, $id)) {
2248			return false;
2249		}
2250
2251		$userVotings = $this->table('tiki_user_votings');
2252
2253		$ip = $this->get_ip_address();
2254		$_SESSION['votes'][] = $id;
2255		setcookie(md5("tiki_wiki_poll_$id"), $ip, time() + 60 * 60 * 24 * 300);
2256		if (! $user) {
2257			if ($prefs['ip_can_be_checked'] == 'y') {
2258				$userVotings->delete(['ip' => $ip, 'id' => $id, 'user' => '']);
2259				if ($optionId !== false && $optionId != 'NULL') {
2260					$userVotings->insert(
2261						[
2262							'user' => '',
2263							'ip' => $ip,
2264							'id' => (string) $id,
2265							'optionId' => (int) $optionId,
2266							'time' => $this->now,
2267						]
2268					);
2269				}
2270			} elseif (isset($_COOKIE[md5("tiki_wiki_poll_$id")])) {
2271				return false;
2272			} elseif ($optionId !== false && $optionId != 'NULL') {
2273				$userVotings->insert(
2274					[
2275						'user' => '',
2276						'ip' => $ip,
2277						'id' => (string) $id,
2278						'optionId' => (int) $optionId,
2279						'time' => $this->now,
2280					]
2281				);
2282			}
2283		} else {
2284			if ($prefs['ip_can_be_checked'] == 'y') {
2285				$userVotings->delete(['user' => $user,'id' => $id]);
2286				$userVotings->delete(['ip' => $ip,'id' => $id]);
2287			} else {
2288				$userVotings->delete(['user' => $user,'id' => $id]);
2289			}
2290			if ($optionId !== false  && $optionId !== 'NULL') {
2291				$userVotings->insert(
2292					[
2293						'user' => $user,
2294						'ip' => $ip,
2295						'id' => (string) $id,
2296						'optionId' => (int) $optionId,
2297						'time' => $this->now,
2298					]
2299				);
2300			}
2301		}
2302
2303		return true;
2304	}
2305
2306	/**
2307	 * @param $id
2308	 * @param $user
2309	 * @return null
2310	 */
2311	function get_user_vote($id, $user)
2312	{
2313		global $prefs;
2314		$vote = null;
2315		if ($user) {
2316			$vote = $this->getOne("select `optionId` from `tiki_user_votings` where `user` = ? and `id` = ? order by `time` desc", [ $user, $id]);
2317		}
2318		if ($vote == null && $prefs['ip_can_be_checked'] == 'y') {
2319			$vote = $this->getOne("select `optionId` from `tiki_user_votings` where `ip` = ? and `id` = ? order by `time` desc", [ $user, $id]);
2320		}
2321		return $vote;
2322	}
2323	// end of user voting methods
2324
2325	/**
2326	 * @param int $offset
2327	 * @param $maxRecords
2328	 * @param string $sort_mode
2329	 * @param string $find
2330	 * @param bool $include_prefs
2331	 * @return array
2332	 */
2333	function list_users($offset = 0, $maxRecords = -1, $sort_mode = 'pref:realName', $find = '', $include_prefs = false)
2334	{
2335		global $user, $prefs;
2336		$userprefslib = TikiLib::lib('userprefs');
2337
2338		$bindvars = [];
2339		if ($find) {
2340			$findesc = '%' . $find . '%';
2341			$mid = 'where (`login` like ? or p1.`value` like ?)';
2342			$mid_cant = $mid;
2343			$bindvars[] = $findesc;
2344			$bindvars[] = $findesc;
2345			$bindvars2 = [$findesc, $findesc];
2346			$find_join = " left join `tiki_user_preferences` p1 on (u.`login` = p1.`user` and p1.`prefName` = 'realName')";
2347			$find_join_cant = $find_join;
2348		} else {
2349			$mid = '';
2350			$bindvars2 = [];
2351			$find_join = '';
2352			$find_join_cant = '';
2353			$mid_cant = '';
2354		}
2355
2356		// This allows to use a sort_mode by prefs
2357		// In this case, sort_mode must have this syntax :
2358		//   pref:PREFERENCE_NAME[_asc|_desc]
2359		// e.g. to sort on country :
2360		//   pref:country  OR  pref:country_asc  OR  pref:country_desc
2361
2362		if ($ppos = strpos($sort_mode, ':')) {
2363			$sort_value = substr($sort_mode, $ppos + 1);
2364			$sort_way = 'asc';
2365
2366			if (preg_match('/^(.+)_(asc|desc)$/i', $sort_value, $regs)) {
2367				$sort_value = $regs[1];
2368				$sort_way = $regs[2];
2369				unset($regs);
2370			}
2371
2372			if ($find_join != '' && $sort_value == 'realName') {
2373				// Avoid two joins if we can do only one
2374				$find_join = '';
2375				$mid = 'where (`login` like ? or p.`value` like ?)';
2376			}
2377			$sort_mode = "p.`value` $sort_way";
2378			$pref_where = ( ( $mid == '' ) ? 'where' : $mid . ' and' ) . " p.`prefName` = '$sort_value'";
2379			$pref_join = 'left join `tiki_user_preferences` p on (u.`login` = p.`user`)';
2380			$pref_field = ', p.`value` as sf';
2381		} else {
2382			$sort_mode = $this->convertSortMode($sort_mode);
2383			$pref_where = $mid;
2384			$pref_join = '';
2385			$pref_field = '';
2386		}
2387
2388		if ($sort_mode != '') {
2389			$sort_mode = 'order by ' . $sort_mode;
2390		}
2391
2392		$query = "select u.* $pref_field  from `users_users` u $pref_join $find_join $pref_where $sort_mode";
2393
2394		$query_cant = "select count(distinct u.`login`) from `users_users` u $find_join_cant $mid_cant";
2395		$result = $this->fetchAll($query, $bindvars, $maxRecords, $offset);
2396		$cant = $this->getOne($query_cant, $bindvars2);
2397
2398		$ret = [];
2399		foreach ($result as $res) {
2400			if ($include_prefs) {
2401				$res['preferences'] = $userprefslib->get_userprefs($res['login']);
2402			}
2403			$ret[] = $res;
2404		}
2405
2406		return ['data' => $ret, 'cant' => $cant];
2407	}
2408
2409	// CMS functions -ARTICLES- & -SUBMISSIONS- ////
2410	/*shared*/
2411	/**
2412	 * @param int $max
2413	 * @return mixed
2414	 */
2415	function get_featured_links($max = 10)
2416	{
2417		$query = "select * from `tiki_featured_links` where `position` > ? order by " . $this->convertSortMode("position_asc");
2418		return  $this->fetchAll($query, [0], (int)$max, 0);
2419	}
2420
2421	/**
2422	 * @param $sessionId
2423	 */
2424	function setSessionId($sessionId)
2425	{
2426		$this->sessionId = $sessionId;
2427	}
2428
2429	/**
2430	 * @return null
2431	 */
2432	function getSessionId()
2433	{
2434		return $this->sessionId;
2435	}
2436
2437	/**
2438	 * @return bool
2439	 */
2440	function update_session()
2441	{
2442		static $uptodate = false;
2443		if ($uptodate === true || $this->sessionId === null) {
2444			return true;
2445		}
2446
2447		global $user, $prefs;
2448		$logslib = TikiLib::lib('logs');
2449
2450		if ($user === false) {
2451			$user = '';
2452		}
2453		// If pref login_multiple_forbidden is set, length of tiki_sessions must match real session length to be up to date so we can detect concurrent logins of same user
2454		if ($prefs['login_multiple_forbidden'] == 'y') {
2455			$delay = ini_get('session.gc_maxlifetime');
2456		} else {	// Low value so as to guess who actually is in front of the computer
2457			$delay = 5 * 60; // 5 minutes
2458		}
2459		$oldy = $this->now - $delay;
2460		if ($user != '') { // was the user timeout?
2461			$query = "select count(*) from `tiki_sessions` where `sessionId`=?";
2462			$cant = $this->getOne($query, [$this->sessionId]);
2463			if ($cant == 0) {
2464				if ($prefs['login_multiple_forbidden'] != 'y' || $user == 'admin') {
2465					// Recover after timeout
2466					$logslib->add_log("login", "back", $user, '', '', $this->now);
2467				} else {
2468					// Prevent multiple sessions for same user
2469					// Must check any user session, not only timed out ones
2470					$query = "SELECT count(*) FROM `tiki_sessions` WHERE user = ?";
2471					$cant = $this->getOne($query, [$user]);
2472					if ($cant == 0) {
2473						// Recover after timeout (no other session)
2474						$logslib->add_log("login", "back", $user, '', '', $this->now);
2475					} else {
2476						// User has an active session on another browser
2477						$userlib = TikiLib::lib('user');
2478						$userlib->user_logout($user, false, '');
2479					}
2480				}
2481			}
2482		}
2483		$query = "select * from `tiki_sessions` where `timestamp`<?";
2484		$result = $this->fetchAll($query, [$oldy]);
2485		foreach ($result as $res) {
2486			if ($res['user'] && $res['user'] != $user) {
2487				$logslib->add_log('login', 'timeout', $res['user'], ' ', ' ', $res['timestamp'] + $delay);
2488			}
2489		}
2490
2491		$sessions = $this->table('tiki_sessions');
2492
2493		$sessions->delete(['sessionId' => $this->sessionId]);
2494		$sessions->deleteMultiple(['timestamp' => $sessions->lesserThan($oldy)]);
2495
2496		if ($user) {
2497			$sessions->delete(['user' => $user]);
2498		}
2499
2500		$sessions->insert(
2501			[
2502				'sessionId' => $this->sessionId,
2503				'timestamp' => $this->now,
2504				'user' => $user,
2505				'tikihost' => isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : 'localhost',
2506			]
2507		);
2508		if ($prefs['session_storage'] == 'db') {
2509			// clean up adodb sessions as well in case adodb session garbage collection not working
2510			$sessions = $this->table('sessions');
2511
2512			$sessions->deleteMultiple(['expiry' => $sessions->lesserThan($oldy)]);
2513		}
2514
2515		$uptodate = true;
2516		return true;
2517	}
2518
2519	// Returns the number of registered users which logged in or were active in the last 5 minutes.
2520	/**
2521	 * @return mixed
2522	 */
2523	function count_sessions()
2524	{
2525		$this->update_session();
2526		return $this->table('tiki_sessions')->fetchCount([]);
2527	}
2528
2529	// Returns a string-indexed array with all the hosts/servers active in the last 5 minutes. Keys are hostnames. Values represent the number of registered users which logged in or were active in the last 5 minutes on the host.
2530	/**
2531	 * @return array
2532	 */
2533	function count_cluster_sessions()
2534	{
2535		$this->update_session();
2536		$query = "select `tikihost`, count(`tikihost`) as cant from `tiki_sessions` group by `tikihost`";
2537		return $this->fetchMap($query, []);
2538	}
2539
2540	/**
2541	 * @param $links
2542	 * @return bool
2543	 */
2544	function cache_links($links)
2545	{
2546		global $prefs;
2547		if ($prefs['cachepages'] != 'y') {
2548			return false;
2549		}
2550		foreach ($links as $link) {
2551			if (! $this->is_cached($link)) {
2552				$this->cache_url($link);
2553			}
2554		}
2555	}
2556
2557	/**
2558	 * @param $data
2559	 * @return array
2560	 */
2561	function get_links($data)
2562	{
2563		$links = [];
2564
2565		/// Prevent the substitution of link [] inside a <tag> ex: <input name="tracker[9]" ... >
2566		$data = preg_replace("/<[^>]*>/", "", $data);
2567
2568		/// Match things like [...], but ignore things like [[foo].
2569		// -Robin
2570		if (preg_match_all("/(?<!\[)\[([^\[\|\]]+)(?:\|?[^\[\|\]]*){0,2}\]/", $data, $r1)) {
2571			$res = $r1[1];
2572			$links = array_unique($res);
2573		}
2574
2575		return $links;
2576	}
2577
2578	/**
2579	 * Convert internal links from absolute to relative
2580	 *
2581	 * @param string $data
2582	 * @return string
2583	 */
2584	public function convertAbsoluteLinksToRelative($data)
2585	{
2586		global $prefs, $tikilib;
2587
2588		preg_match_all('/\[(([^|\]]+)(\|([^|\]]+))?)\]/', $data, $matches);
2589
2590		$counter = count($matches[0]);
2591		for ($i = 0; $i < $counter; $i++) {
2592			$label = ! empty($matches[3][$i]) ? ltrim($matches[3][$i], '|') : '';
2593			if (! empty($label) && $matches[2][$i] == $label) {
2594				$data = str_replace($matches[0][$i], '[' . $matches[2][$i] . ']', $data);
2595			}
2596
2597			// Check if link part is valid url
2598			if (filter_var($matches[2][$i], FILTER_VALIDATE_URL) === false) {
2599				continue;
2600			}
2601
2602			// Check if url matches tiki instance links
2603			if ($url = $this->getMatchBaseUrlSchema($matches[2][$i]) && $matches[2][$i] == $matches[4][$i]) {
2604				$newLink = '[' . $matches[2][$i] . ']';
2605				$data = str_replace($matches[0][$i], $newLink, $data);
2606			}
2607		}
2608
2609		preg_match_all('/\(\((([^|)]+)(\|([^|)]+))?)\)\)/', $data, $matches);
2610
2611		$counter = count($matches[0]);
2612		for ($i = 0; $i < $counter; $i++) {
2613			if ($matches[0][$i]) {
2614				$linkArray = explode('|', trim($matches[0][$i], '(())'));
2615				if (count($linkArray) == 2 && $linkArray[0] == $linkArray[1]) {
2616					$newLink = '((' . $linkArray[0] . '))';
2617					$data = str_replace($matches[0][$i], $newLink, $data);
2618				}
2619			}
2620		}
2621
2622		if ($prefs['feature_absolute_to_relative_links'] != 'y') {
2623			return $data;
2624		}
2625
2626		$notification = false;
2627
2628		$from = 0;
2629		$to = strlen($data);
2630		$replace = [];
2631		foreach ($this->getWikiMarkers() as $marker) {
2632			while (false !== $open = $this->findText($data, $marker[0], $from, $to)) {
2633				// Wiki marker -+ begin should be proceeded by space or a newline
2634				if ($marker[0] == '-+' && $open != 0 && ! preg_match('/\s/', $data[$open - 1])) {
2635					$from = $open + 1;
2636					continue;
2637				}
2638
2639				if (false !== $close = $this->findText($data, $marker[1], $open, $to)) {
2640					$from = $close;
2641					$size = ($close - $open) + strlen($marker[1]);
2642					$markerBody = substr($data, $open, $size);
2643					$key = "§" . md5($tikilib->genPass()) . "§" ;
2644					$replace[$key] = $markerBody;
2645					$data = str_replace($markerBody, $key, $data);
2646				} else {
2647					break;
2648				}
2649			}
2650		}
2651
2652		// convert absolute to relative links
2653		$pluginMatches = WikiParser_PluginMatcher::match($data);
2654		foreach ($pluginMatches as $pluginMatch) {
2655			$pluginBody = $pluginMatch->getBody();
2656			if (empty($pluginBody)) {
2657				$pluginBody = $pluginMatch->getArguments();
2658			}
2659
2660			$key = "§" . md5($tikilib->genPass()) . "§" ;
2661			$replace[$key] = $pluginBody;
2662			$data = str_replace($pluginBody, $key, $data);
2663		}
2664
2665		// Detect tiki internal links
2666		preg_match_all('/\(\((([^|)]+)(\|([^|)]+))?)\)\)/', $data, $matches);
2667
2668		$counter = count($matches[0]);
2669		for ($i = 0; $i < $counter; $i++) {
2670			$linkArray = explode('|', trim($matches[0][$i], '(())'));
2671			if (count($linkArray) == 2 && $linkArray[0] == $linkArray[1]) {
2672				$newLink = '((' . $linkArray[0] . '))';
2673				$data = str_replace($matches[0][$i], $newLink, $data);
2674				$notification = true;
2675			}
2676
2677			// Check if link part is valid url
2678			if (filter_var($matches[2][$i], FILTER_VALIDATE_URL) === false) {
2679				continue;
2680			}
2681
2682			// Check if url matches tiki instance links
2683			if ($url = $this->getMatchBaseUrlSchema($matches[2][$i])) {
2684				$newPath = str_replace($url, '', $matches[2][$i]);
2685				// In case of a tikibase instance point link to Homepage
2686				if (empty($newPath) || $newPath == '/') {
2687					$newPath = 'Homepage';
2688				}
2689				$newLink = '((' . $newPath . $matches[3][$i] . '))';
2690				$data = str_replace($matches[0][$i], $newLink, $data);
2691				$notification = true;
2692			}
2693		}
2694
2695		// Detect external links
2696		preg_match_all('/\[(([^|\]]+)(\|([^|\]]+))?)\]/', $data, $matches);
2697
2698		$counter = count($matches[0]);
2699		for ($i = 0; $i < $counter; $i++) {
2700			// Check if link part is valid url
2701			if (filter_var($matches[2][$i], FILTER_VALIDATE_URL) === false) {
2702				continue;
2703			}
2704
2705			// Check if url matches tiki instance links
2706			if ($url = $this->getMatchBaseUrlSchema($matches[2][$i])) {
2707				$newPath = str_replace($url, '', $matches[2][$i]);
2708				if (! empty($newPath)) {
2709					$newLink = '[' . $newPath . $matches[3][$i] . ']';
2710
2711					$newLinkArray = explode('|', trim($newLink, '[]'));
2712					if (count($newLinkArray) === 2 && $newLinkArray[0] == str_replace($url, '', $newLinkArray[1])) {
2713						$newLink = '[' . $newLinkArray[0] . ']';
2714					}
2715
2716					$data = str_replace($matches[0][$i], $newLink, $data);
2717					$notification = true;
2718				}
2719			}
2720		}
2721
2722		// Detect links outside wikiplugin or wiki markers
2723		preg_match_all('/(http|https):\/\/([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])?/', $data, $matches);
2724
2725		$counter = count($matches[0]);
2726		for ($i = 0; $i < $counter; $i++) {
2727			// Check if link part is valid url
2728			if (filter_var($matches[0][$i], FILTER_VALIDATE_URL) === false) {
2729				continue;
2730			}
2731
2732			// Check if url matches tiki instance links
2733			if ($url = $this->getMatchBaseUrlSchema($matches[0][$i])) {
2734				$newPath = str_replace($url, '', $matches[0][$i]);
2735				$objectLink = $this->getObjectRelativeLink($newPath);
2736				if (! empty($newPath) && ! empty($objectLink)) {
2737					$objStartPos = strpos($data, $matches[0][$i]);
2738					$objLength = strlen($matches[0][$i]);
2739					$data = substr_replace($data, $objectLink, $objStartPos, $objLength);
2740					$notification = true;
2741				}
2742			}
2743		}
2744
2745		foreach ($replace as $key => $body) {
2746			$data = str_replace($key, $body, $data);
2747		}
2748
2749		if ($notification) {
2750			Feedback::note(tr('Tiki links converted to relative links'));
2751		}
2752
2753		return $data;
2754	}
2755
2756	/**
2757	 * Return the base url in the matched link protocol (http or https)
2758	 *
2759	 * @param string $link The link to check
2760	 *
2761	 * @return string The tiki base url with the matched schema (http or https)
2762	 */
2763	public function getMatchBaseUrlSchema($link)
2764	{
2765		global $base_url_http, $base_url_https;
2766
2767		if (strpos($link, $base_url_http) !== false) {
2768			return $base_url_http;
2769		} elseif (strpos($link, rtrim($base_url_http, '/')) !== false) {
2770			return rtrim($base_url_http, '/');
2771		} elseif (strpos($link, $base_url_https) !== false) {
2772			return $base_url_https;
2773		} elseif (strpos($link, rtrim($base_url_https, '/')) !== false) {
2774			return rtrim($base_url_https, '/');
2775		} else {
2776			return null;
2777		}
2778	}
2779
2780	/**
2781	 * Returns the object internal link
2782	 *
2783	 * @param string $uri
2784	 * @return string
2785	 */
2786	public function getObjectRelativeLink($uri)
2787	{
2788		global $prefs;
2789		$objectLink = '';
2790
2791		if (! empty($prefs['feature_sefurl']) && $prefs['feature_sefurl'] === 'y') {
2792			$slug = explode('-', $uri);
2793			$slug = $slug[0];
2794
2795			switch ($slug) {
2796				case (substr($slug, 0, 7) === 'article' || substr($slug, 0, 3) === 'art'):
2797					$articleId = substr($slug, 0, 7) === 'article' ? substr($slug, 7) : substr($slug, 3);
2798					$artlib = TikiLib::lib('art');
2799					$article = $artlib->get_article($articleId);
2800					$objectLink = ! empty($article['title']) ? '[' . $uri . '|' . $article['title'] . ']' : '';
2801					break;
2802				case substr($slug, 0, 8) === 'blogpost':
2803					$blogPostId = substr($slug, 8);
2804					$bloglib = TikiLib::lib('blog');
2805					$blogPost = $bloglib->get_post($blogPostId);
2806					$objectLink = ! empty($blogPost['title']) ? '[' . $uri . '|' . $blogPost['title'] . ']' : '';
2807					break;
2808				case substr($slug, 0, 4) === 'blog':
2809					$blogId = substr($slug, 4);
2810					$bloglib = TikiLib::lib('blog');
2811					$blog = $bloglib->get_blog($blogId);
2812					$objectLink = ! empty($blog['title']) ? '[' . $uri . '|' . $blog['title'] . ']' : '';
2813					break;
2814				case (substr($slug, 0, 11) === 'browseimage' || substr($slug, 0, 5) === 'image' || substr($slug, 0, 3) === 'img'):
2815					if (substr($slug, 0, 11) === 'browseimage') {
2816						$imageId = substr($slug, 11);
2817					} elseif (substr($slug, 0, 5) === 'image') {
2818						$imageId = substr($slug, 5);
2819					} else {
2820						$imageId = substr($slug, 3);
2821					}
2822					$imagegallib = TikiLib::lib('imagegal');
2823					$image = $imagegallib->get_image_info($imageId);
2824					$objectLink = ! empty($image['name']) ? '[' . $uri . '|' . $image['name'] . ']' : '';
2825					break;
2826				case substr($slug, 0, 8) === 'calevent':
2827					$eventId = substr($slug, 8);
2828					$calendarlib = TikiLib::lib('calendar');
2829					$event = $calendarlib->get_item($eventId);
2830					$objectLink = ! empty($event['name']) ? '[' . $uri . '|' . $event['name'] . ']' : '';
2831					break;
2832				case substr($slug, 0, 3) === 'cal':
2833					$calendarId = substr($slug, 3);
2834					$calendarlib = TikiLib::lib('calendar');
2835					$calendar = $calendarlib->get_calendar($calendarId);
2836					$objectLink = ! empty($calendar['name']) ? '[' . $uri . '|' . $calendar['name'] . ']' : '';
2837					break;
2838				case substr($slug, 0, 3) === 'cat':
2839					$catId = substr($slug, 3);
2840					$categlib = TikiLib::lib('categ');
2841					$cat = $categlib->get_category($catId);
2842					$objectLink = ! empty($cat['name']) ? '[' . $uri . '|' . $cat['name'] . ']' : '';
2843					break;
2844				case substr($slug, 0, 9) === 'directory':
2845					$directoryCatId = substr($slug, 9);
2846					if ($directoryCatId == 0) {
2847						$objectLink = '[' . $uri . '|Top]';
2848					} else {
2849						global $dirlib;
2850						include_once('lib/directory/dirlib.php');
2851						$directoryCat = $dirlib->dir_get_category($directoryCatId);
2852						$objectLink = ! empty($directoryCat['name']) ? '[' . $uri . '|' . $directoryCat['name'] . ']' : '';
2853					}
2854					break;
2855				case substr($slug, 0, 7) === 'dirlink':
2856					$siteId = substr($slug, 7);
2857					global $dirlib;
2858					include_once('lib/directory/dirlib.php');
2859					$site = $dirlib->dir_get_site($siteId);
2860					$objectLink = ! empty($site['name']) ? '[' . $uri . '|' . $site['name'] . ']' : '';
2861					break;
2862				case substr($slug, 0, 5) === 'event':
2863					$eventId = substr($slug, 5);
2864					$calendarlib = TikiLib::lib('calendar');
2865					$event = $calendarlib->get_item($eventId);
2866					$objectLink = ! empty($event['name']) ? '[' . $uri . '|' . $event['name'] . ']' : '';
2867					break;
2868				case substr($slug, 0, 3) === 'faq':
2869					$faqId = substr($slug, 3);
2870					$faqlib = TikiLib::lib('faq');
2871					$faq = $faqlib->get_faq($faqId);
2872					$objectLink = ! empty($faq['title']) ? '[' . $uri . '|' . $faq['title'] . ']' : '';
2873					break;
2874				case substr($slug, 0, 4) === 'file':
2875					$fileGalleryId = substr($slug, 4);
2876					$filegallib = TikiLib::lib('filegal');
2877					$gallery = $filegallib->get_file_gallery($fileGalleryId);
2878					$objectLink = ! empty($gallery['name']) ? '[' . $uri . '|' . $gallery['name'] . ']' : '';
2879					break;
2880				case substr($slug, 0, 7) === 'gallery':
2881					$galleryId = substr($slug, 7);
2882					$filegallib = TikiLib::lib('filegal');
2883					$gallery = $filegallib->get_file_gallery($galleryId);
2884					$objectLink = ! empty($gallery['name']) ? '[' . $uri . '|' . $gallery['name'] . ']' : '';
2885					break;
2886				case (substr($slug, 0, 2) === 'dl' || substr($slug, 0, 9) === 'thumbnail' || substr($slug, 0, 7) === 'display' || substr($slug, 0, 7) === 'preview'):
2887					if (substr($slug, 0, 2) === 'dl') {
2888						$fileId = substr($slug, 2);
2889					} elseif (substr($slug, 0, 9) === 'thumbnail') {
2890						$fileId = substr($slug, 9);
2891					} else {
2892						$fileId = substr($slug, 7);
2893					}
2894					$filegallib = TikiLib::lib('filegal');
2895					$file = $filegallib->get_file($fileId);
2896					$objectLink = ! empty($file['name']) ? '[' . $uri . '|' . $file['name'] . ']' : '';
2897					break;
2898				case substr($slug, 0, 11) === 'forumthread':
2899					$forumCommentId = substr($slug, 11);
2900					$commentslib = TikiLib::lib('comments');
2901					$forumComment = $commentslib->get_comment($forumCommentId);
2902					$objectLink = ! empty($forumComment['title']) ? '[' . $uri . '|' . $forumComment['title'] . ']' : '';
2903					break;
2904				case substr($slug, 0, 5) === 'forum':
2905					$forumId = substr($slug, 5);
2906					$commentslib = TikiLib::lib('comments');
2907					$forum = $commentslib->get_forum($forumId);
2908					$objectLink = ! empty($forum['name']) ? '[' . $uri . '|' . $forum['name'] . ']' : '';
2909					break;
2910				case substr($slug, 0, 4) === 'item':
2911					$itemId = substr($slug, 4);
2912					$trklib = TikiLib::lib('trk');
2913					$trackerItem = $trklib->get_tracker_item($itemId);
2914					$objectLink = ! empty($trackerItem) ? '[' . $uri . '|' . $slug . ']' : '';
2915					break;
2916				case substr($slug, 0, 3) === 'int':
2917					$repID = substr($slug, 3);
2918					$integrator = new TikiIntegrator($dbTiki);
2919					$rep = $integrator->get_repository($repID);
2920					$objectLink = ! empty($rep['name']) ? '[' . $uri . '|' . $rep['name'] . ']' : '';
2921					break;
2922				case (substr($slug, 0, 10) === 'newsletter' || substr($slug, 0, 2) === 'nl'):
2923					$newsletterId = substr($slug, 0, 10) === 'newsletter' ? substr($slug, 10) : substr($slug, 2);
2924					global $nllib;
2925					include_once('lib/newsletters/nllib.php');
2926					$newsletter = $nllib->get_newsletter($newsletterId);
2927					$objectLink = ! empty($newsletter['name']) ? '[' . $uri . '|' . $newsletter['name'] . ']' : '';
2928					break;
2929				case substr($slug, 0, 4) === 'poll':
2930					$pollId = substr($slug, 4);
2931					$polllib = TikiLib::lib('poll');
2932					$poll = $polllib->get_poll($pollId);
2933					$objectLink = ! empty($poll['title']) ? '[' . $uri . '|' . $poll['title'] . ']' : '';
2934					break;
2935				case substr($slug, 0, 4) === 'quiz':
2936					$quizId = substr($slug, 4);
2937					$quizlib = TikiLib::lib('quiz');
2938					$quiz = $quizlib->get_quiz($quizId);
2939					$objectLink = ! empty($quiz['name']) ? '[' . $uri . '|' . $quiz['name'] . ']' : '';
2940					break;
2941				case substr($slug, 0, 7) === 'tracker':
2942					$trackerId = substr($slug, 7);
2943					$trklib = TikiLib::lib('trk');
2944					$tracker = $trklib->get_tracker($trackerId);
2945					$objectLink = ! empty($tracker['name']) ? '[' . $uri . '|' . $tracker['name'] . ']' : '';
2946					break;
2947				case substr($slug, 0, 5) === 'sheet':
2948					$sheetId = substr($slug, 5);
2949					$sheetlib = TikiLib::lib("sheet");
2950					$sheet = $sheetlib->get_sheet_info($sheetId);
2951					$objectLink = ! empty($sheet['title']) ? '[' . $uri . '|' . $sheet['title'] . ']' : '';
2952					break;
2953				case substr($slug, 0, 6) === 'survey':
2954					include_once('lib/surveys/surveylib.php');
2955					$surveyId = substr($slug, 6);
2956					$survey = $srvlib->get_survey($surveyId);
2957					$objectLink = ! empty($survey['name']) ? '[' . $uri . '|' . $survey['name'] . ']' : '';
2958					break;
2959				case substr($slug, 0, 4) === 'user':
2960					$userId = substr($slug, 4);
2961					$user = $this->get_user_login($userId);
2962					$objectLink = ! empty($user) ? '[' . $uri . '|' . $user . ']' : '';
2963					break;
2964				default:
2965					$pageName = $this->getPageBySlug($uri);
2966					$objectLink = ! empty($pageName) ? '((' . $pageName . '))' : '';
2967			}
2968		}
2969
2970		$uriParams = explode('?', $uri);
2971		$param = ! empty($uriParams[1]) ? $uriParams[1] : '';
2972		$clearParam = ! empty($param) ? explode('&', $param) : '';
2973		$param = ! empty($clearParam[0]) ? $clearParam[0] : '';
2974		if (! empty($param)) {
2975			switch ($param) {
2976				case substr($param, 0, 9) === 'articleId':
2977					$articleId = substr($param, 10);
2978					$artlib = TikiLib::lib('art');
2979					$article = $artlib->get_article($articleId);
2980					$objectLink = ! empty($article['title']) ? '[' . $uri . '|' . $article['title'] . ']' : '';
2981					break;
2982				case substr($param, 0, 6) === 'blogId':
2983					$blogId = substr($param, 7);
2984					$bloglib = TikiLib::lib('blog');
2985					$blog = $bloglib->get_blog($blogId);
2986					$objectLink = ! empty($blog['title']) ? '[' . $uri . '|' . $blog['title'] . ']' : '';
2987					break;
2988				case substr($param, 0, 6) === 'postId':
2989					$blogPostId = substr($param, 7);
2990					$bloglib = TikiLib::lib('blog');
2991					$blogPost = $bloglib->get_post($blogPostId);
2992					$objectLink = ! empty($blogPost['title']) ? '[' . $uri . '|' . $blogPost['title'] . ']' : '';
2993					break;
2994				case substr($param, 0, 10) === 'calendarId':
2995					$calendarId = substr($param, 11);
2996					$calendarlib = TikiLib::lib('calendar');
2997					$calendar = $calendarlib->get_calendar($calendarId);
2998					$objectLink = ! empty($calendar['name']) ? '[' . $uri . '|' . $calendar['name'] . ']' : '';
2999					break;
3000				case substr($param, 0, 17) === 'comments_parentId':
3001					$forumCommentId = substr($param, 18);
3002					$commentslib = TikiLib::lib('comments');
3003					$forumComment = $commentslib->get_comment($forumCommentId);
3004					$objectLink = ! empty($forumComment['title']) ? '[' . $uri . '|' . $forumComment['title'] . ']' : '';
3005					break;
3006				case substr($param, 0, 6) === 'parent':
3007					$directoryCatId = substr($param, 7);
3008					if ($directoryCatId == 0) {
3009						$objectLink = '[' . $uri . '|Top]';
3010					} else {
3011						global $dirlib;
3012						include_once('lib/directory/dirlib.php');
3013						$directoryCat = $dirlib->dir_get_category($directoryCatId);
3014						$objectLink = ! empty($directoryCat['name']) ? '[' . $uri . '|' . $directoryCat['name'] . ']' : '';
3015					}
3016					break;
3017				case substr($param, 0, 9) === 'galleryId':
3018					$fileGalleryId = substr($param, 10);
3019					$filegallib = TikiLib::lib('filegal');
3020					$gallery = $filegallib->get_file_gallery($fileGalleryId);
3021					$objectLink = ! empty($gallery['name']) ? '[' . $uri . '|' . $gallery['name'] . ']' : '';
3022					break;
3023				case substr($param, 0, 5) === 'faqId':
3024					$faqId = substr($param, 6);
3025					$faqlib = TikiLib::lib('faq');
3026					$faq = $faqlib->get_faq($faqId);
3027					$objectLink = ! empty($faq['title']) ? '[' . $uri . '|' . $faq['title'] . ']' : '';
3028					break;
3029				case substr($param, 0, 6) === 'fileId':
3030					$fileId = substr($param, 7);
3031					$filegallib = TikiLib::lib('filegal');
3032					$file = $filegallib->get_file($fileId);
3033					$objectLink = ! empty($file['name']) ? '[' . $uri . '|' . $file['name'] . ']' : '';
3034					break;
3035				case substr($param, 0, 7) === 'forumId':
3036					$forumId = substr($param, 8);
3037					$commentslib = TikiLib::lib('comments');
3038					$forum = $commentslib->get_forum($forumId);
3039					$objectLink = ! empty($forum['name']) ? '[' . $uri . '|' . $forum['name'] . ']' : '';
3040					break;
3041				case (substr($param, 0, 7) === 'imageId' || substr($param, 0, 2) === 'id'):
3042					$imageId = (substr($param, 0, 7) === 'imageId') ? substr($param, 8) : substr($param, 3);
3043					$imagegallib = TikiLib::lib('imagegal');
3044					$image = $imagegallib->get_image_info($imageId);
3045					$objectLink = ! empty($image['name']) ? '[' . $uri . '|' . $image['name'] . ']' : '';
3046					break;
3047				case substr($param, 0, 4) === 'nlId':
3048					$newsletterId = substr($param, 5);
3049					global $nllib;
3050					include_once('lib/newsletters/nllib.php');
3051					$newsletter = $nllib->get_newsletter($newsletterId);
3052					$objectLink = ! empty($newsletter['name']) ? '[' . $uri . '|' . $newsletter['name'] . ']' : '';
3053					break;
3054				case substr($param, 0, 4) === 'page':
3055					$pageSlug = substr($param, 5);
3056					if ($uriParams[0] == 'tiki-index.php') {
3057						$pageName = $this->getPageBySlug($pageSlug);
3058						$objectLink = ! empty($pageName) ? '((' . $pageName . '))' : '';
3059					} else {
3060						$objectLink = '[' . $uri . '|' . $uri . ']';
3061					}
3062					break;
3063				case substr($param, 0, 8) === 'parentId':
3064					$catId = substr($param, 9);
3065					$categlib = TikiLib::lib('categ');
3066					$cat = $categlib->get_category($catId);
3067					$objectLink = ! empty($cat['name']) ? '[' . $uri . '|' . $cat['name'] . ']' : '';
3068					break;
3069				case substr($param, 0, 6) === 'pollId':
3070					$pollId = substr($param, 7);
3071					$polllib = TikiLib::lib('poll');
3072					$poll = $polllib->get_poll($pollId);
3073					$objectLink = ! empty($poll['title']) ? '[' . $uri . '|' . $poll['title'] . ']' : '';
3074					break;
3075				case substr($param, 0, 6) === 'quizId':
3076					$quizId = substr($param, 7);
3077					$quizlib = TikiLib::lib('quiz');
3078					$quiz = $quizlib->get_quiz($quizId);
3079					$objectLink = ! empty($quiz['name']) ? '[' . $uri . '|' . $quiz['name'] . ']' : '';
3080					break;
3081				case substr($param, 0, 5) === 'repID':
3082					$repID = substr($param, 6);
3083					$integrator = new TikiIntegrator($dbTiki);
3084					$rep = $integrator->get_repository($repID);
3085					$objectLink = ! empty($rep['name']) ? '[' . $uri . '|' . $rep['name'] . ']' : '';
3086					break;
3087				case substr($param, 0, 6) === 'siteId':
3088					$siteId = substr($param, 7);
3089					global $dirlib;
3090					include_once('lib/directory/dirlib.php');
3091					$site = $dirlib->dir_get_site($siteId);
3092					$objectLink = ! empty($site['name']) ? '[' . $uri . '|' . $site['name'] . ']' : '';
3093					break;
3094				case substr($param, 0, 7) === 'sheetId':
3095					$sheetId = substr($param, 8);
3096					$sheetlib = TikiLib::lib("sheet");
3097					$sheet = $sheetlib->get_sheet_info($sheetId);
3098					$objectLink = ! empty($sheet['title']) ? '[' . $uri . '|' . $sheet['title'] . ']' : '';
3099					break;
3100				case substr($param, 0, 8) === 'surveyId':
3101					include_once('lib/surveys/surveylib.php');
3102					$surveyId = substr($param, 9);
3103					$survey = $srvlib->get_survey($surveyId);
3104					$objectLink = ! empty($survey['name']) ? '[' . $uri . '|' . $survey['name'] . ']' : '';
3105					break;
3106				case substr($param, 0, 9) === 'trackerId':
3107					$trackerId = substr($param, 10);
3108					$trklib = TikiLib::lib('trk');
3109					$tracker = $trklib->get_tracker($trackerId);
3110					$objectLink = ! empty($tracker['name']) ? '[' . $uri . '|' . $tracker['name'] . ']' : '';
3111					break;
3112				case substr($param, 0, 6) === 'userId':
3113					$userId = substr($param, 7);
3114					$user = $this->get_user_login($userId);
3115					$objectLink = ! empty($user) ? '[' . $uri . '|' . $user . ']' : '';
3116					break;
3117				case substr($param, 0, 13) === 'viewcalitemId':
3118					$eventId = substr($param, 14);
3119					$calendarlib = TikiLib::lib('calendar');
3120					$event = $calendarlib->get_item($eventId);
3121					$objectLink = ! empty($event['name']) ? '[' . $uri . '|' . $event['name'] . ']' : '';
3122					break;
3123			}
3124		}
3125
3126		if (empty($objectLink)) {
3127			$pageName = $this->getPageBySlug($uri);
3128			if (in_array($uri, ['index.php', 'tiki-index.php'])) {
3129				$objectLink = '[' . $uri . '|' . $uri . ']';
3130			} else {
3131				$objectLink = ! empty($pageName) ? '((' . $pageName . '))' : '[' . $uri . '|' . $uri . ']';
3132			}
3133		}
3134
3135		return $objectLink;
3136	}
3137
3138	/**
3139	 * Return wiki pages
3140	 *
3141	 * @param $slug
3142	 * @return string
3143	 */
3144	public function getPageBySlug($slug)
3145	{
3146		global $prefs;
3147
3148		$pages = TikiDb::get()->table('tiki_pages');
3149		$found = $pages->fetchOne('pageName', ['pageSlug' => $slug]);
3150
3151		return ! empty($found) ? $found : '';
3152	}
3153
3154	/**
3155	 * @param $data
3156	 * @return array
3157	 */
3158	function get_links_nocache($data)
3159	{
3160		$links = [];
3161
3162		if (preg_match_all("/\[([^\]]+)/", $data, $r1)) {
3163			$res = [];
3164
3165			foreach ($r1[1] as $alink) {
3166				$parts = explode('|', $alink);
3167
3168				if (isset($parts[1]) && $parts[1] == 'nocache') {
3169					$res[] = $parts[0];
3170				} elseif (isset($parts[2]) && $parts[2] == 'nocache') {
3171					$res[] = $parts[0];
3172				} else {
3173					if (isset($parts[3]) && $parts[3] == 'nocache') {
3174						$res[] = $parts[0];
3175					}
3176				}
3177				/// avoid caching URLs with common binary file extensions
3178				$extension = substr($parts[0], -4);
3179				$binary = [
3180						'.arj',
3181						'.asf',
3182						'.avi',
3183						'.bz2',
3184						'.com',
3185						'.dat',
3186						'.doc',
3187						'.exe',
3188						'.hqx',
3189						'.mid',
3190						'.mov',
3191						'.mp3',
3192						'.mpg',
3193						'.ogg',
3194						'.pdf',
3195						'.ram',
3196						'.rar',
3197						'.rpm',
3198						'.rtf',
3199						'.sea',
3200						'.sit',
3201						'.tar',
3202						'.tgz',
3203						'.wav',
3204						'.wmv',
3205						'.xls',
3206						'.zip',
3207						'ar.Z', // .tar.Z
3208						'r.gz'  // .tar.gz
3209							];
3210				if (in_array($extension, $binary)) {
3211					$res[] = $parts[0];
3212				}
3213			}
3214
3215			$links = array_unique($res);
3216		}
3217
3218		return $links;
3219	}
3220
3221	/**
3222	 * @param $url
3223	 * @return bool
3224	 */
3225	function is_cacheable($url)
3226	{
3227		// simple implementation: future versions should analyse
3228		// if this is a link to the local machine
3229		if (strstr($url, 'tiki-')) {
3230			return false;
3231		}
3232
3233		if (strstr($url, 'messu-')) {
3234			return false;
3235		}
3236
3237		return true;
3238	}
3239
3240	/**
3241	 * @param $url
3242	 * @return mixed
3243	 */
3244	function is_cached($url)
3245	{
3246		return $this->table('tiki_link_cache')->fetchCount(['url' => $url]);
3247	}
3248
3249	/**
3250	 * @param $offset
3251	 * @param $maxRecords
3252	 * @param $sort_mode
3253	 * @param $find
3254	 * @return array
3255	 */
3256	function list_cache($offset, $maxRecords, $sort_mode, $find)
3257	{
3258
3259		if ($find) {
3260			$findesc = '%' . $find . '%';
3261
3262			$mid = " where (`url` like ?) ";
3263			$bindvars = [$findesc];
3264		} else {
3265			$mid = "";
3266			$bindvars = [];
3267		}
3268
3269		$query = "select `cacheId` ,`url`,`refresh` from `tiki_link_cache` $mid order by " . $this->convertSortMode($sort_mode);
3270		$query_cant = "select count(*) from `tiki_link_cache` $mid";
3271		$ret = $this->fetchAll($query, $bindvars, $maxRecords, $offset);
3272		$cant = $this->getOne($query_cant, $bindvars);
3273
3274		$retval = [];
3275		$retval["data"] = $ret;
3276		$retval["cant"] = $cant;
3277		return $retval;
3278	}
3279
3280	/**
3281	 * @param $cacheId
3282	 * @return bool
3283	 */
3284	function refresh_cache($cacheId)
3285	{
3286		$linkCache = $this->table('tiki_link_cache');
3287
3288		$url = $linkCache->fetchOne('url', ['cacheId' => $cacheId]);
3289
3290		$data = $this->httprequest($url);
3291
3292		$linkCache->update(['data' => $data,	'refresh' => $this->now], ['cacheId' => $cacheId]);
3293		return true;
3294	}
3295
3296	/**
3297	 * @param $cacheId
3298	 * @return bool
3299	 */
3300	function remove_cache($cacheId)
3301	{
3302		$linkCache = $this->table('tiki_link_cache');
3303		$linkCache->delete(['cacheId' => $cacheId]);
3304
3305		return true;
3306	}
3307
3308	/**
3309	 * @param $cacheId
3310	 * @return mixed
3311	 */
3312	function get_cache($cacheId)
3313	{
3314		return $this->table('tiki_link_cache')->fetchFullRow(['cacheId' => $cacheId]);
3315	}
3316
3317	/**
3318	 * @param $url
3319	 * @return bool
3320	 */
3321	function get_cache_id($url)
3322	{
3323		$id = $this->table('tiki_link_cache')->fetchOne('cacheId', ['url' => $url]);
3324		return $id ? $id : false;
3325	}
3326	/* cachetime = 0 => no cache, otherwise duration cache is valid */
3327	/**
3328	 * @param $url
3329	 * @param $isFresh
3330	 * @param int $cachetime
3331	 * @return mixed
3332	 */
3333	function get_cached_url($url, &$isFresh, $cachetime = 0)
3334	{
3335		$linkCache = $this->table('tiki_link_cache');
3336
3337		$res = $linkCache->fetchFullRow(['url' => $url]);
3338		$now = $this->now;
3339
3340		if (empty($res) || ($now - $res['refresh']) > $cachetime) { // no cache or need to refresh
3341			$res['data'] = $this->httprequest($url);
3342			$isFresh = true;
3343			//echo '<br />Not cached:'.$url.'/'.strlen($res['data']);
3344			$res['refresh'] = $now;
3345			if ($cachetime > 0) {
3346				if (empty($res['cacheId'])) {
3347					$linkCache->insert(['url' => $url, 'data' => $res['data'], 'refresh' => $res['refresh']]);
3348
3349					$res = $linkCache->fetchFullRow(['url' => $url]);
3350				} else {
3351					$linkCache->update(['data' => $res['data'], 'refresh' => $res['refresh']], ['cacheId' => $res['cacheId']]);
3352				}
3353			}
3354		} else {
3355			//echo '<br />Cached:'.$url;
3356			$isFresh = false;
3357		}
3358		return $res;
3359	}
3360
3361	// This funcion return the $limit most accessed pages
3362	// it returns pageName and hits for each page
3363	/**
3364	 * @param $limit
3365	 * @return array
3366	 */
3367	function get_top_pages($limit)
3368	{
3369		$query = "select `pageName` , `hits`
3370			from `tiki_pages`
3371			order by `hits` desc";
3372
3373		$result = $this->fetchAll($query, [], $limit);
3374		$ret = [];
3375
3376		foreach ($result as $res) {
3377			$aux["pageName"] = $res["pageName"];
3378
3379			$aux["hits"] = $res["hits"];
3380			$ret[] = $aux;
3381		}
3382
3383		return $ret;
3384	}
3385
3386	// Returns the name of all pages
3387	/**
3388	 * @return mixed
3389	 */
3390	function get_all_pages()
3391	{
3392		return $this->table('tiki_pages')->fetchAll(['pageName'], []);
3393	}
3394
3395	/**
3396	 * \brief Cache given url
3397	 * If \c $data present (passed) it is just associated \c $url and \c $data.
3398	 * Else it will request data for given URL and store it in DB.
3399	 * Actualy (currently) data may be proviced by TIkiIntegrator only.
3400	 */
3401	function cache_url($url, $data = '')
3402	{
3403		// Avoid caching internal references... (only if $data not present)
3404		// (cdx) And avoid other protocols than http...
3405		// 03-Nov-2003, by zaufi
3406		// preg_match("_^(mailto:|ftp:|gopher:|file:|smb:|news:|telnet:|javascript:|nntp:|nfs:)_",$url)
3407		// was removed (replaced to explicit http[s]:// detection) bcouse
3408		// I now (and actualy use in my production Tiki) another bunch of protocols
3409		// available in my konqueror... (like ldap://, ldaps://, nfs://, fish://...)
3410		// ... seems like it is better to enum that allowed explicitly than all
3411		// noncacheable protocols.
3412		if (((strstr($url, 'tiki-') || strstr($url, 'messu-')) && $data == '')
3413				|| (substr($url, 0, 7) != 'http://' && substr($url, 0, 8) != 'https://')) {
3414			return false;
3415		}
3416		// Request data for URL if nothing given in parameters
3417		// (reuse $data var)
3418		if ($data == '') {
3419			$data = $this->httprequest($url);
3420		}
3421
3422		// If stuff inside [] is *really* malformatted, $data
3423		// will be empty.  -rlpowell
3424		if ($data) {
3425			$linkCache = $this->table('tiki_link_cache');
3426			$linkCache->insert(['url' => $url, 'data' => $data, 'refresh' => $this->now]);
3427			return true;
3428		} else {
3429			return false;
3430		}
3431	}
3432
3433	// Removes all the versions of a page and the page itself
3434	/*shared*/
3435	/**
3436	 * @param $page
3437	 * @param string $comment
3438	 * @return bool
3439	 */
3440	function remove_all_versions($page, $comment = '')
3441	{
3442		$page_info = $this->get_page_info($page);
3443		if (! $page_info) {
3444			return false;
3445		}
3446		global $user, $prefs;
3447		if ($prefs['feature_actionlog'] == 'y' && isset($page_info['data'])) {
3448			$params = 'del=' . strlen($page_info['data']);
3449		} else {
3450			$params = '';
3451		}
3452		//  Deal with mail notifications.
3453		include_once(__DIR__ . '/notifications/notificationemaillib.php');
3454		$foo = parse_url($_SERVER["REQUEST_URI"]);
3455		$machine = self::httpPrefix(true) . dirname($foo["path"]);
3456		sendWikiEmailNotification('wiki_page_deleted', $page, $user, $comment, 1, $page_info['data'], $machine);
3457
3458		//Remove the bibliography references for this page
3459		$this->removePageReference($page);
3460
3461		$wikilib = TikiLib::lib('wiki');
3462		$multilinguallib = TikiLib::lib('multilingual');
3463		$multilinguallib->detachTranslation('wiki page', $multilinguallib->get_page_id_from_name($page));
3464		$this->invalidate_cache($page);
3465		//Delete structure references before we delete the page
3466		$query  = "select `page_ref_id` ";
3467		$query .= "from `tiki_structures` ts, `tiki_pages` tp ";
3468		$query .= "where ts.`page_id`=tp.`page_id` and `pageName`=?";
3469		$result = $this->fetchAll($query, [$page]);
3470		foreach ($result as $res) {
3471			$this->remove_from_structure($res["page_ref_id"]);
3472		}
3473
3474		$this->table('tiki_pages')->delete(['pageName' => $page]);
3475		if ($prefs['feature_contribution'] == 'y') {
3476			$contributionlib = TikiLib::lib('contribution');
3477			$contributionlib->remove_page($page);
3478		}
3479		$this->table('tiki_history')->deleteMultiple(['pageName' => $page]);
3480		$this->table('tiki_links')->deleteMultiple(['fromPage' => $page]);
3481		$logslib = TikiLib::lib('logs');
3482		$logslib->add_action('Removed', $page, 'wiki page', $params);
3483		//get_strings tra("Removed");
3484		$this->table('users_groups')->updateMultiple(['groupHome' => null], ['groupHome' => $page]);
3485
3486		$this->table('tiki_theme_control_objects')->deleteMultiple(['name' => $page,'type' => 'wiki page']);
3487		$this->table('tiki_copyrights')->deleteMultiple(['page' => $page]);
3488
3489		$this->remove_object('wiki page', $page);
3490
3491		$this->table('tiki_user_watches')->deleteMultiple(['event' => 'wiki_page_changed', 'object' => $page]);
3492		$this->table('tiki_group_watches')->deleteMultiple(['event' => 'wiki_page_changed', 'object' => $page]);
3493
3494		$atts = $wikilib->list_wiki_attachments($page, 0, -1, 'created_desc', '');
3495		foreach ($atts["data"] as $at) {
3496			$wikilib->remove_wiki_attachment($at["attId"]);
3497		}
3498
3499		$wikilib->remove_footnote('', $page);
3500		$this->refresh_index('wiki page', $page);
3501
3502		return true;
3503	}
3504
3505	/*shared*/
3506	/**
3507	 * @param $page_ref_id
3508	 * @return bool
3509	 */
3510	function remove_from_structure($page_ref_id)
3511	{
3512		// Now recursively remove
3513		$query  = "select `page_ref_id` ";
3514		$query .= "from `tiki_structures` as ts, `tiki_pages` as tp ";
3515		$query .= "where ts.`page_id`=tp.`page_id` and `parent_id`=?";
3516		$result = $this->fetchAll($query, [$page_ref_id]);
3517
3518		foreach ($result as $res) {
3519			$this->remove_from_structure($res["page_ref_id"]);
3520		}
3521
3522		$structlib = TikiLib::lib('struct');
3523		$page_info = $structlib->s_get_page_info($page_ref_id);
3524
3525		$structures = $this->table('tiki_structures');
3526
3527		$structures->updateMultiple(
3528			['pos' => $structures->decrement(1)],
3529			['pos' => $structures->greaterThan((int) $page_info['pos']),	'parent_id' => (int) $page_info['parent_id'],]
3530		);
3531
3532		$structures->delete(['page_ref_id' => $page_ref_id]);
3533		return true;
3534	}
3535
3536	/*shared*/
3537	/**
3538	 * @param int $offset
3539	 * @param $maxRecords
3540	 * @param string $sort_mode
3541	 * @param string $user
3542	 * @param null $find
3543	 * @return array
3544	 */
3545	function list_galleries($offset = 0, $maxRecords = -1, $sort_mode = 'name_desc', $user = '', $find = null)
3546	{
3547		// If $user is admin then get ALL galleries, if not only user galleries are shown
3548		global $tiki_p_admin_galleries, $tiki_p_admin;
3549
3550		$old_sort_mode = '';
3551
3552		if (in_array($sort_mode, ['images desc', 'images asc'])) {
3553			$old_offset = $offset;
3554
3555			$old_maxRecords = $maxRecords;
3556			$old_sort_mode = $sort_mode;
3557			$sort_mode = 'user desc';
3558			$offset = 0;
3559			$maxRecords = -1;
3560		}
3561
3562		// If the user is not admin then select `it` 's own galleries or public galleries
3563		if ($tiki_p_admin_galleries === 'y' or $tiki_p_admin === 'y') {
3564			$whuser = "";
3565			$bindvars = [];
3566		} else {
3567			$whuser = "where `user`=? or public=?";
3568			$bindvars = [$user,'y'];
3569		}
3570
3571		if (! empty($find)) {
3572			$findesc = '%' . $find . '%';
3573
3574			if (empty($whuser)) {
3575				$whuser = "where `name` like ? or `description` like ?";
3576				$bindvars = [$findesc,$findesc];
3577			} else {
3578				$whuser .= " and `name` like ? or `description` like ?";
3579				$bindvars[] = $findesc;
3580				$bindvars[] = $findesc;
3581			}
3582		}
3583
3584		// If sort mode is versions then offset is 0, maxRecords is -1 (again) and sort_mode is nil
3585		// If sort mode is links then offset is 0, maxRecords is -1 (again) and sort_mode is nil
3586		// If sort mode is backlinks then offset is 0, maxRecords is -1 (again) and sort_mode is nil
3587		$query = "select * from `tiki_galleries` $whuser order by " . $this->convertSortMode($sort_mode);
3588		$query_cant = "select count(*) from `tiki_galleries` $whuser";
3589		$result = $this->fetchAll($query, $bindvars, $maxRecords, $offset);
3590		$cant = $this->getOne($query_cant, $bindvars);
3591		$ret = [];
3592
3593		$images = $this->table('tiki_images');
3594		foreach ($result as $res) {
3595			global $user;
3596			$add = $this->user_has_perm_on_object($user, $res['galleryId'], 'image gallery', 'tiki_p_view_image_gallery');
3597			if ($add) {
3598				$aux = [];
3599
3600				$aux["name"] = $res["name"];
3601				$gid = $res["galleryId"];
3602				$aux["visible"] = $res["visible"];
3603				$aux["id"] = $gid;
3604				$aux["galleryId"] = $res["galleryId"];
3605				$aux["description"] = $res["description"];
3606				$aux["created"] = $res["created"];
3607				$aux["lastModif"] = $res["lastModif"];
3608				$aux["user"] = $res["user"];
3609				$aux["hits"] = $res["hits"];
3610				$aux["public"] = $res["public"];
3611				$aux["theme"] = $res["theme"];
3612				$aux["geographic"] = $res["geographic"];
3613				$aux["images"] = $images->fetchCount(['galleryId' => $gid]);
3614				$ret[] = $aux;
3615			}
3616		}
3617
3618		if ($old_sort_mode == 'images asc') {
3619			usort($ret, 'compare_images');
3620		}
3621
3622		if ($old_sort_mode == 'images desc') {
3623			usort($ret, 'r_compare_images');
3624		}
3625
3626		if (in_array($old_sort_mode, ['images desc', 'images asc'])) {
3627			$ret = array_slice($ret, $old_offset, $old_maxRecords);
3628		}
3629
3630		$retval = [];
3631		$retval["data"] = $ret;
3632		$retval["cant"] = $cant;
3633		return $retval;
3634	}
3635
3636	// Deprecated in favor of list_pages
3637	/**
3638	 * @param $maxRecords
3639	 * @param string $categories
3640	 * @return array
3641	 */
3642	function last_pages($maxRecords = -1, $categories = '')
3643	{
3644		if (is_array($categories)) {
3645			$filter = ["categId" => $categories];
3646		} else {
3647			$filter = [];
3648		}
3649
3650		return $this->list_pages(0, $maxRecords, "lastModif_desc", '', '', true, true, false, false, $filter);
3651	}
3652
3653	// Broken. Equivalent to last_pages($maxRecords)
3654	/**
3655	 * @param $maxRecords
3656	 * @return array
3657	 */
3658	function last_major_pages($maxRecords = -1)
3659	{
3660		return $this->list_pages(0, $maxRecords, "lastModif_desc");
3661	}
3662	// use this function to speed up when pagename is only needed (the 3 getOne can killed tikiwith more that 3000 pages)
3663	/**
3664	 * @param int $offset
3665	 * @param $maxRecords
3666	 * @param string $sort_mode
3667	 * @param string $find
3668	 * @return array
3669	 */
3670	function list_pageNames($offset = 0, $maxRecords = -1, $sort_mode = 'pageName_asc', $find = '')
3671	{
3672		return $this->list_pages($offset, $maxRecords, $sort_mode, $find, '', true, true);
3673	}
3674
3675	/**
3676	 * @param int $offset
3677	 * @param $maxRecords
3678	 * @param string $sort_mode
3679	 * @param string $find
3680	 * @param string $initial
3681	 * @param bool $exact_match
3682	 * @param bool $onlyName
3683	 * @param bool $forListPages
3684	 * @param bool $only_orphan_pages
3685	 * @param string $filter
3686	 * @param bool $onlyCant
3687	 * @param string $ref
3688	 * @return array
3689	 */
3690	function list_pages($offset = 0, $maxRecords = -1, $sort_mode = 'pageName_desc', $find = '', $initial = '', $exact_match = true, $onlyName = false, $forListPages = false, $only_orphan_pages = false, $filter = '', $onlyCant = false, $ref = '', $exclude_pages = '')
3691	{
3692		global $prefs, $tiki_p_wiki_view_ratings;
3693
3694		$loadCategories = (isset($prefs['wiki_list_categories']) && $prefs['wiki_list_categories'] == 'y') || (isset($prefs['wiki_list_categories_path']) && $prefs['wiki_list_categories_path'] == 'y');
3695		$loadCategories = $loadCategories && $forListPages;
3696
3697		$join_tables = '';
3698		$join_bindvars = [];
3699		$old_sort_mode = '';
3700		if ($sort_mode == 'size_desc') {
3701			$sort_mode = 'page_size_desc';
3702		}
3703		if ($sort_mode == 'size_asc') {
3704			$sort_mode = 'page_size_asc';
3705		}
3706		$select = '';
3707
3708		// If sort mode is versions, links or backlinks then offset is 0, maxRecords is -1 (again) and sort_mode is nil
3709		$need_everything = false;
3710		if (in_array($sort_mode, ['versions_desc', 'versions_asc', 'links_asc', 'links_desc', 'backlinks_asc', 'backlinks_desc'])) {
3711			$old_sort_mode = $sort_mode;
3712			$sort_mode = 'user_desc';
3713			$need_everything = true;
3714		}
3715
3716		if (is_array($find)) { // you can use an array of pages
3717			$mid = " where LOWER(`pageName`) IN (" . implode(',', array_fill(0, count($find), 'LOWER(?)')) . ")";
3718			$bindvars = $find;
3719		} elseif (is_string($find) && ! empty($find)) { // or a string
3720			if (! $exact_match && $find) {
3721				$find = preg_replace("/([^\s]+)/", "%\\1%", $find);
3722				$f = preg_split("/[\s]+/", $find, -1, PREG_SPLIT_NO_EMPTY);
3723				if (empty($f)) {//look for space...
3724					$mid = " where LOWER(`pageName`) like LOWER('%$find%')";
3725				} else {
3726					$findop = $forListPages ? ' AND' : ' OR';
3727					$mid = " where LOWER(`pageName`) like " . implode($findop . ' LOWER(`pageName`) like ', array_fill(0, count($f), 'LOWER(?)'));
3728					$bindvars = $f;
3729				}
3730			} else {
3731				$mid = " where LOWER(`pageName`) like LOWER(?) ";
3732				$bindvars = [$find];
3733			}
3734		} else {
3735			$bindvars = [];
3736			$mid = '';
3737		}
3738
3739		//check if exclude page is array and then add its values in bindvars and
3740		if ($exclude_pages && is_array($exclude_pages)) { // you can use an array of pages
3741			if (! empty($mid)) {
3742				$mid .= " AND (LOWER(`pageName`) NOT IN (" . implode(',', array_fill(0, count($exclude_pages), 'LOWER(?)')) . "))";
3743			} else {
3744				$mid = " where LOWER(`pageName`) NOT IN (" . implode(',', array_fill(0, count($exclude_pages), 'LOWER(?)')) . ")";
3745			}
3746
3747			foreach ($exclude_pages as $epKey => $epVal) {
3748				$bindvars[] = $epVal;
3749			}
3750		}
3751
3752		$categlib = TikiLib::lib('categ');
3753		$category_jails = $categlib->get_jail();
3754
3755		if (! isset($filter['andCategId']) && ! isset($filter['categId']) && empty($filter['noCateg']) && ! empty($category_jails)) {
3756			$filter['categId'] = $category_jails;
3757		}
3758
3759		// If language is set to '', assume that no language filtering should be done.
3760		if (isset($filter['lang']) && $filter['lang'] == '') {
3761			unset($filter['lang']);
3762		}
3763
3764		$distinct = '';
3765		if (! empty($filter)) {
3766			$tmp_mid = [];
3767			foreach ($filter as $type => $val) {
3768				if ($type == 'andCategId') {
3769					$categories = $categlib->get_jailed((array) $val);
3770					$join_tables .= " inner join `tiki_objects` as tob on (tob.`itemId`= tp.`pageName` and tob.`type`= ?) ";
3771					$join_bindvars[] = 'wiki page';
3772					foreach ($categories as $i => $categId) {
3773						$join_tables .= " inner join `tiki_category_objects` as tc$i on (tc$i.`catObjectId`=tob.`objectId` and tc$i.`categId` =?) ";
3774						$join_bindvars[] = $categId;
3775					}
3776				} elseif ($type == 'categId') {
3777					$categories = $categlib->get_jailed((array) $val);
3778					$categories[] = -1;
3779
3780					$cat_count = count($categories);
3781					$join_tables .= " inner join `tiki_objects` as tob on (tob.`itemId`= tp.`pageName` and tob.`type`= ?) inner join `tiki_category_objects` as tc on (tc.`catObjectId`=tob.`objectId` and tc.`categId` IN(" . implode(', ', array_fill(0, $cat_count, '?')) . ")) ";
3782
3783					if ($cat_count > 1) {
3784						$distinct = ' DISTINCT ';
3785					}
3786
3787					$join_bindvars = array_merge(['wiki page'], $categories);
3788				} elseif ($type == 'noCateg') {
3789					$join_tables .= ' left join `tiki_objects` as tob on (tob.`itemId`= tp.`pageName` and tob.`type`= ?) left join `tiki_categorized_objects` as tcdo on (tcdo.`catObjectId`=tob.`objectId`) left join `tiki_category_objects` as tco on (tcdo.`catObjectId`=tco.`catObjectId`)';
3790					$join_bindvars[] = 'wiki page';
3791					$tmp_mid[] = '(tco.`categId` is null)';
3792				} elseif ($type == 'notCategId') {
3793					foreach ($val as $v) {
3794						$tmp_mid[] = '(tp.`pageName` NOT IN(SELECT itemId FROM tiki_objects INNER JOIN tiki_category_objects ON catObjectId = objectId WHERE type = "wiki page" AND categId = ?))';
3795						$bindvars[] = $v;
3796					}
3797				} elseif ($type == 'lang') {
3798					$tmp_mid[] = 'tp.`lang`=?';
3799					$bindvars[] = $val;
3800				} elseif ($type == 'structHead') {
3801					$join_tables .= " inner join `tiki_structures` as ts on (ts.`page_id` = tp.`page_id` and ts.`parent_id` = 0) ";
3802					$select .= ',ts.`page_alias`';
3803				} elseif ($type == 'langOrphan') {
3804					$join_tables .= " left join `tiki_translated_objects` tro on (tro.`type` = 'wiki page' AND tro.`objId` = tp.`page_id`) ";
3805					$tmp_mid[] = "( (tro.`traId` IS NULL AND tp.`lang` != ?) OR tro.`traId` NOT IN(SELECT `traId` FROM `tiki_translated_objects` WHERE `lang` = ?))";
3806					$bindvars[] = $val;
3807					$bindvars[] = $val;
3808				} elseif ($type == 'structure_orphans') {
3809					$join_tables .= " left join `tiki_structures` as tss on (tss.`page_id` = tp.`page_id`) ";
3810					$tmp_mid[] = "(tss.`page_ref_id` is null)";
3811				} elseif ($type == 'translationOrphan') {
3812					$multilinguallib = TikiLib::lib('multilingual');
3813					$multilinguallib->sqlTranslationOrphan('wiki page', 'tp', 'page_id', $val, $join_tables, $midto, $bindvars);
3814					$tmp_mid[] = $midto;
3815				}
3816			}
3817			if (! empty($tmp_mid)) {
3818				$mid .= empty($mid) ? ' where (' : ' and (';
3819				$mid .= implode(' and ', $tmp_mid) . ')';
3820			}
3821		}
3822		if (! empty($initial)) {
3823			$mid .= empty($mid) ? ' where (' : ' and (';
3824			$tmp_mid = '';
3825			if (is_array($initial)) {
3826				foreach ($initial as $i) {
3827					if (! empty($tmp_mid)) {
3828						$tmp_mid .= ' or ';
3829					}
3830					$tmp_mid .= ' `pageName` like ? ';
3831					$bindvars[] = $i . '%';
3832				}
3833			} else {
3834				$tmp_mid = " `pageName` like ? ";
3835				$bindvars[] = $initial . '%';
3836			}
3837			$mid .= $tmp_mid . ')';
3838		}
3839
3840		if ($only_orphan_pages) {
3841			$join_tables .= ' left join `tiki_links` as tl on tp.`pageName` = tl.`toPage` left join `tiki_structures` as tsoo on tp.`page_id` = tsoo.`page_id`';
3842			$mid .= ( $mid == '' ) ? ' where ' : ' and ';
3843			$mid .= 'tl.`toPage` IS NULL and tsoo.`page_id` IS NULL';
3844		}
3845
3846		if ($prefs['rating_advanced'] == 'y') {
3847			$ratinglib = TikiLib::lib('rating');
3848			$join_tables .= $ratinglib->convert_rating_sort($sort_mode, 'wiki page', '`page_id`');
3849		}
3850
3851		if ($tiki_p_wiki_view_ratings === 'y' && $prefs['feature_polls'] == 'y' && $prefs['feature_wiki_ratings'] == 'y') {
3852			$select .= ', (select sum(`tiki_poll_options`.`title`*`tiki_poll_options`.`votes`) as rating ' . // Multiply the option's label (title) by the number of votes for that option. Titles can be numbers but even then, this computation has doubtful relevance. A page with 5 ratings of "1" (on a scale of 5) would obtain a higher rating than a page with 1 rating of "4". This is most particular and apparently undocumented. Chealer 2017-05-22
3853				'from `tiki_objects` as tobt, `tiki_poll_objects` as tpo, `tiki_poll_options` where tobt.`itemId`= tp.`pageName` and tobt.`type`=\'wiki page\' and tobt.`objectId`=tpo.`catObjectId` and `tiki_poll_options`.`pollId`=tpo.`pollId` group by `tiki_poll_options`.`pollId`) as rating';
3854		}
3855
3856		if (! empty($join_bindvars)) {
3857			$bindvars = empty($bindvars) ? $join_bindvars : array_merge($join_bindvars, $bindvars);
3858		}
3859
3860		$query = "select $distinct"
3861			. ( $onlyCant ? "tp.`pageName`" : "tp.* " . $select )
3862			. " from `tiki_pages` as tp $join_tables $mid order by " . $this->convertSortMode($sort_mode);
3863		$countquery = "select count($distinct tp.`pageName`) from `tiki_pages` as tp $join_tables $mid";
3864		$pageCount = $this->getOne($countquery, $bindvars);
3865
3866
3867		// HOTFIX (svn Rev. 22969 or near there)
3868		// Chunk loading. Because we cannot know what pages are visible, we load chunks of pages
3869		// and use Perms::filter to see what remains. Stop, if we have enough.
3870		$cant = 0;
3871		$n = -1;
3872		$ret = [];
3873		$raw = [];
3874
3875		$offset_tmp = 0;
3876		$haveEnough = false;
3877		$filterPerms = empty($ref) ? 'view' : ['view', 'wiki_view_ref'];
3878		while (! $haveEnough) {
3879			$rawTemp = $this->fetchAll($query, $bindvars, $maxRecords, $offset_tmp);
3880			$offset_tmp += $maxRecords; // next offset
3881
3882			if (count($rawTemp) == 0) {
3883				$haveEnough = true; // end of table
3884			}
3885
3886			$rawTemp = Perms::filter([ 'type' => 'wiki page' ], 'object', $rawTemp, ['object' => 'pageName', 'creator' => 'creator'], $filterPerms);
3887
3888			$raw = array_merge($raw, $rawTemp);
3889			if ((count($raw) >= $offset + $maxRecords) || $maxRecords == -1) {
3890				$haveEnough = true; // now we have enough records
3891			}
3892		} // prbably this brace has to include the next foreach??? I am unsure.
3893		// but if yes, the next lines have to be reviewed.
3894
3895
3896		$history = $this->table('tiki_history');
3897		$links = $this->table('tiki_links');
3898
3899		foreach ($raw as $res) {
3900			if ($initial) {
3901				$valid = false;
3902				$verified = self::take_away_accent($res['pageName']);
3903				foreach ((array) $initial as $candidate) {
3904					if (stripos($verified, $candidate) === 0) {
3905						$valid = true;
3906						break;
3907					}
3908				}
3909
3910				if (! $valid) {
3911					continue;
3912				}
3913			}
3914			//WYSIWYCA
3915			$res['perms'] = $this->get_perm_object($res['pageName'], 'wiki page', $res, false);
3916
3917			$n++;
3918			if (! $need_everything && $offset != -1 && $n < $offset) {
3919				continue;
3920			}
3921
3922			if (! $onlyCant && ( $need_everything || $maxRecords == -1 || $cant < $maxRecords )) {
3923				if ($onlyName) {
3924					$res = ['pageName' => $res['pageName']];
3925				} else {
3926					$page = $res['pageName'];
3927					$res['len'] = $res['page_size'];
3928					unset($res['page_size']);
3929					$res['flag'] = $res['flag'] == 'L' ? 'locked' : 'unlocked';
3930					if ($forListPages && $prefs['wiki_list_versions'] == 'y') {
3931						$res['versions'] = $history->fetchCount(['pageName' => $page]);
3932					}
3933					if ($forListPages && $prefs['wiki_list_links'] == 'y') {
3934						$res['links'] = $links->fetchCount(['fromPage' => $page]);
3935					}
3936					if ($forListPages && $prefs['wiki_list_backlinks'] == 'y') {
3937						$res['backlinks'] = $links->fetchCount(['toPage' => $page, 'fromPage' => $links->unlike('objectlink:%')]);
3938					}
3939					// backlinks do not include links from non-page objects TODO: full feature allowing this with options
3940				}
3941
3942				if ($loadCategories) {
3943					$cats = $categlib->get_object_categories('wiki page', $res['pageName']);
3944					$res['categpath'] = [];
3945					$res['categname'] = [];
3946					foreach ($cats as $cat) {
3947						$res['categpath'][] = $cp = $categlib->get_category_path_string($cat);
3948						if ($s = strrchr($cp, ':')) {
3949							$res['categname'][] = substr($s, 1);
3950						} else {
3951							$res['categname'][] = $cp;
3952						}
3953					}
3954				}
3955				$ret[] = $res;
3956			}
3957			$cant++;
3958		}
3959		if (! $need_everything) {
3960			$cant += $offset;
3961		}
3962
3963		// If sortmode is versions, links or backlinks sort using the ad-hoc function and reduce using old_offset and old_maxRecords
3964		if ($need_everything) {
3965			switch ($old_sort_mode) {
3966				case 'versions_asc':
3967					usort($ret, 'compare_versions');
3968					break;
3969				case 'versions_desc':
3970					usort($ret, 'r_compare_versions');
3971					break;
3972				case 'links_desc':
3973					usort($ret, 'compare_links');
3974					break;
3975				case 'links_asc':
3976					usort($ret, 'r_compare_links');
3977					break;
3978				case 'backlinks_desc':
3979					usort($ret, 'compare_backlinks');
3980					break;
3981				case 'backlinks_asc':
3982					usort($ret, 'r_compare_backlinks');
3983					break;
3984			}
3985		}
3986
3987		$retval = [];
3988		$retval['data'] = $ret;
3989		$retval['cant'] = $pageCount; // this is not exact. Workaround.
3990		return $retval;
3991	}
3992
3993
3994	// Function that checks for:
3995	// - tiki_p_admin
3996	// - the permission itself
3997	// - individual permission
3998	// - category permission
3999	// if O.K. this function shall replace similar constructs in list_pages and other functions above.
4000	// $categperm is the category permission that should grant $perm. if none, pass 0
4001	// If additional perm arguments are specified, the user must have all the perms to pass the test
4002	/**
4003	 * @param $usertocheck
4004	 * @param $object
4005	 * @param $objtype
4006	 * @param $perm1
4007	 * @param null $perm2
4008	 * @param null $perm3
4009	 * @return bool
4010	 */
4011	function user_has_perm_on_object($usertocheck, $object, $objtype, $perm1, $perm2 = null, $perm3 = null)
4012	{
4013		$accessor = $this->get_user_permission_accessor($usertocheck, $objtype, $object);
4014
4015		$chk1 = $perm1 != null ? $accessor->$perm1 : true;
4016		$chk2 = $perm2 != null ? $accessor->$perm2 : true;
4017		$chk3 = $perm3 != null ? $accessor->$perm3 : true;
4018
4019		return $chk1 && $chk2 && $chk3;
4020	}
4021
4022	function get_user_permission_accessor($usertocheck, $type = null, $object = null)
4023	{
4024		global $user;
4025		if ($type && $object) {
4026			$context = [ 'type' => $type, 'object' => $object ];
4027			$accessor = Perms::get($context);
4028		} else {
4029			$accessor = Perms::get();
4030		}
4031
4032		// Do not override perms for current users otherwise security tokens won't work
4033		if ($usertocheck != $user) {
4034			$groups = $this->get_user_groups($usertocheck);
4035			$accessor->setGroups($groups);
4036		}
4037
4038		return $accessor;
4039	}
4040
4041	/* get all the perm of an object either in a table or global+smarty set
4042	 * OPTIMISATION: better to test tiki_p_admin outside for global=false
4043	 * TODO: all the objectTypes
4044	 * TODO: replace switch with object
4045	 * global = true set the global perm and smarty var, otherwise return an array of perms
4046	 */
4047	/**
4048	 * @param $objectId
4049	 * @param $objectType
4050	 * @param string $info
4051	 * @param bool $global
4052	 * @return array|bool
4053	 */
4054	function get_perm_object($objectId, $objectType, $info = '', $global = true)
4055	{
4056		global $user;
4057		$smarty = TikiLib::lib('smarty');
4058		$userlib = TikiLib::lib('user');
4059
4060		$perms = Perms::get([ 'type' => $objectType, 'object' => $objectId ]);
4061		if (empty($perms->getGroups())) {
4062			$perms->setGroups($this->get_user_groups($user));
4063		}
4064		$permNames = $userlib->get_permission_names_for($this->get_permGroup_from_objectType($objectType));
4065
4066		$ret = [];
4067		foreach ($permNames as $perm) {
4068			$ret[$perm] = $perms->$perm ? 'y' : 'n';
4069
4070			if ($global) {
4071				$smarty->assign($perm, $ret[$perm]);
4072				$GLOBALS[ $perm ] = $ret[$perm];
4073			}
4074		}
4075
4076		// Skip those 'local' permissions for admin users and when global is not requested.
4077		if ($global && ! Perms::get()->admin) {
4078			$ret2 = $this->get_local_perms($user, $objectId, $objectType, $info, true);
4079			if ($ret2) {
4080				$ret = $ret2;
4081			}
4082		}
4083
4084		return $ret;
4085	}
4086
4087	/**
4088	 * @param $objectType
4089	 * @return string
4090	 */
4091	function get_permGroup_from_objectType($objectType)
4092	{
4093
4094		switch ($objectType) {
4095			case 'tracker':
4096			case 'trackeritem':
4097				return 'trackers';
4098			case 'image gallery':
4099			case 'image':
4100				return 'image galleries';
4101			case 'file gallery':
4102			case 'file':
4103				return 'file galleries';
4104			case 'article':
4105			case 'submission':
4106			case 'topic':
4107				return 'cms';
4108			case 'forum':
4109			case 'thread':
4110				return 'forums';
4111			case 'blog':
4112			case 'blog post':
4113				return 'blogs';
4114			case 'wiki page':
4115			case 'history':
4116				return 'wiki';
4117			case 'faq':
4118				return 'faqs';
4119			case 'survey':
4120				return 'surveys';
4121			case 'newsletter':
4122				return 'newsletters';
4123				/* TODO */
4124			default:
4125				return $objectType;
4126		}
4127	}
4128
4129	/**
4130	 * @param $objectType
4131	 * @return string
4132	 */
4133	function get_adminPerm_from_objectType($objectType)
4134	{
4135
4136		switch ($objectType) {
4137			case 'tracker':
4138				return 'tiki_p_admin_trackers';
4139			case 'image gallery':
4140			case 'image':
4141				return 'tiki_p_admin_galleries';
4142			case 'file gallery':
4143			case 'file':
4144				return 'tiki_p_admin_file_galleries';
4145			case 'article':
4146			case 'submission':
4147				return 'tiki_p_admin_cms';
4148			case 'forum':
4149				return 'tiki_p_admin_forum';
4150			case 'blog':
4151			case 'blog post':
4152				return 'tiki_p_blog_admin';
4153			case 'wiki page':
4154			case 'history':
4155				return 'tiki_p_admin_wiki';
4156			case 'faq':
4157				return 'tiki_p_admin_faqs';
4158			case 'survey':
4159				return 'tiki_p_admin_surveys';
4160			case 'newsletter':
4161				return 'tiki_p_admin_newsletters';
4162				/* TODO */
4163			default:
4164				return "tiki_p_admin_$objectType";
4165		}
4166	}
4167
4168	/* deal all the special perm */
4169	/**
4170	 * @param $user
4171	 * @param $objectId
4172	 * @param $objectType
4173	 * @param $info
4174	 * @param $global
4175	 * @return array|bool
4176	 */
4177	function get_local_perms($user, $objectId, $objectType, $info, $global)
4178	{
4179		global $prefs;
4180		$smarty = TikiLib::lib('smarty');
4181		$userlib = TikiLib::lib('user');
4182		$ret = [];
4183		switch ($objectType) {
4184			case 'wiki page':
4185			case 'wiki':
4186				if ($prefs['wiki_creator_admin'] == 'y' && ! empty($user) && ! empty($info) && $info['creator'] == $user) { //can admin his page
4187					$perms = $userlib->get_permission_names_for($this->get_permGroup_from_objectType($objectType));
4188					foreach ($perms as $perm) {
4189						$ret[$perm] = 'y';
4190						if ($global) {
4191							$GLOBALS[$perm] = 'y';
4192							$smarty->assign($perm, 'y');
4193						}
4194					}
4195					return $ret;
4196				}
4197				// Enabling userpage is not enough, the prefix must be present, otherwise, permissions will be messed-up on new page creation
4198				if ($prefs['feature_wiki_userpage'] == 'y' && ! empty($prefs['feature_wiki_userpage_prefix']) && ! empty($user) && strcasecmp($prefs['feature_wiki_userpage_prefix'], substr($objectId, 0, strlen($prefs['feature_wiki_userpage_prefix']))) == 0) {
4199					if (strcasecmp($objectId, $prefs['feature_wiki_userpage_prefix'] . $user) == 0) { //can edit his page
4200						if (! $global) {
4201							$perms = $userlib->get_permission_names_for($this->get_permGroup_from_objectType($objectType));
4202							foreach ($perms as $perm) {
4203								if ($perm == 'tiki_p_view' || $perm == 'tiki_p_edit') {
4204									$ret[$perm] = 'y';
4205								} else {
4206									$ret[$perm] = $GLOBALS[$perm];
4207								}
4208							}
4209						} else {
4210							global $tiki_p_edit, $tiki_p_view;
4211							$tiki_p_view = 'y';
4212							$smarty->assign('tiki_p_view', 'y');
4213							$tiki_p_edit = 'y';
4214							$smarty->assign('tiki_p_edit', 'y');
4215						}
4216					} else {
4217						if (! $global) {
4218							$ret['tiki_p_edit'] = 'n';
4219						} else {
4220								global $tiki_p_edit;
4221								$tiki_p_edit = 'n';
4222								$smarty->assign('tiki_p_edit', 'n');
4223						}
4224					}
4225					if (! $global) {
4226						$ret['tiki_p_rename'] = 'n';
4227						$ret['tiki_p_rollback'] = 'n';
4228						$ret['tiki_p_lock'] = 'n';
4229						$ret['tiki_p_assign_perm_wiki_page'] = 'n';
4230					} else {
4231						global $tiki_p_rename, $tiki_p_rollback, $tiki_p_lock, $tiki_p_assign_perm_wiki_page;
4232						$tiki_p_rename = $tiki_p_rollback = $tiki_p_lock = $tiki_p_assign_perm_wiki_page = 'n';
4233						$smarty->assign('tiki_p_rename', 'n');
4234						$smarty->assign('tiki_p_rollback', 'n');
4235						$smarty->assign('tiki_p_lock', 'n');
4236						$smarty->assign('tiki_p_assign_perm_wiki_page', 'n');
4237					}
4238				}
4239				break;
4240			case 'file gallery':
4241			case 'file':
4242				global $tiki_p_userfiles;
4243
4244				if ($objectType === 'file') {
4245					$gal_info = TikiLib::lib('filegal')->get_file_gallery_info($info['galleryId']);
4246					if ($gal_info['user'] === $user) {
4247						$info['type'] = 'user';			// show my files as mine
4248					} else {
4249						$info['type'] = '';
4250					}
4251				}
4252				if ($prefs['feature_use_fgal_for_user_files'] === 'y' &&
4253						$info['type'] === 'user' && $info['user'] === $user && $tiki_p_userfiles === 'y') {
4254					foreach (['tiki_p_download_files',
4255									'tiki_p_upload_files',
4256									'tiki_p_view_file_gallery',
4257									'tiki_p_remove_files',
4258									'tiki_p_create_file_galleries',
4259									'tiki_p_edit_gallery_file',
4260								] as $perm) {
4261						$GLOBALS[$perm] = 'y';
4262						$smarty->assign($perm, 'y');
4263						$ret[$perm] = 'y';
4264					}
4265					return $ret;
4266				}
4267				break;
4268			default:
4269				break;
4270		}
4271		return false;
4272	}
4273
4274	// Returns a string-indexed array of modified preferences (those with a value other than the default). Keys are preference names. Values are preference values.
4275	// NOTE: prefslib contains a similar method now called getModifiedPrefsForExport
4276	/**
4277	 * @return array
4278	 */
4279	function getModifiedPreferences()
4280	{
4281		$defaults = get_default_prefs();
4282		$modified = [];
4283
4284		$results = $this->table('tiki_preferences')->fetchAll(['name', 'value'], []);
4285
4286		foreach ($results as $result) {
4287			$name = $result['name'];
4288			$value = $result['value'];
4289
4290			$strDef = "";
4291			if (isset($defaults[$name]) && is_array($defaults[$name])) {
4292				$strDef = implode(" ", $defaults[$name]);
4293			} else {
4294				$strDef = isset($defaults[$name]) ? $defaults[$name] : "";
4295			}
4296			if (empty($strDef) || ($strDef != (string) $value)) {
4297				$modified[$name] = $value;
4298			}
4299		}
4300		return $modified;
4301	}
4302
4303	/**
4304	 * @param $names
4305	 * @param bool $exact_match
4306	 * @param bool $no_return
4307	 * @return array|bool
4308	 */
4309	function get_preferences($names, $exact_match = false, $no_return = false)
4310	{
4311		global $prefs;
4312
4313		$preferences = [];
4314		if ($exact_match) {
4315			if (is_array($names)) {
4316				$this->_get_values('tiki_preferences', 'name', $names, $prefs);
4317				if (! $no_return) {
4318					foreach ($names as $name) {
4319						$preferences[$name] = $prefs[$name];
4320					}
4321				}
4322			} else {
4323				$this->get_preference($names);
4324				if (! $no_return) {
4325					$preferences = [ $names => $prefs[$names] ];
4326				}
4327			}
4328		} else {
4329			if (is_array($names)) {
4330				//Only handle $filtername as array with exact_matches
4331				return false;
4332			} else {
4333				$tikiPreferences = $this->table('tiki_preferences');
4334				$preferences = $tikiPreferences->fetchMap('name', 'value', ['name' => $tikiPreferences->like($names)]);
4335			}
4336		}
4337		return $preferences;
4338	}
4339
4340	/**
4341	 * @param $name
4342	 * @param string $default
4343	 * @param bool $expectArray
4344	 * @return mixed|string
4345	 */
4346	function get_preference($name, $default = '', $expectArray = false)
4347	{
4348		global $prefs;
4349
4350		$value = isset($prefs[$name]) ? $prefs[$name] : $default;
4351
4352		if (empty($value)) {
4353			if ($expectArray) {
4354				return [];
4355			} else {
4356				return $value;
4357			}
4358		}
4359
4360		if ($expectArray && is_string($value)) {
4361			return unserialize($value);
4362		} else {
4363			return $value;
4364		}
4365	}
4366
4367	/**
4368	 * @param $name
4369	 */
4370	function delete_preference($name)
4371	{
4372		global $user_overrider_prefs, $user_preferences, $user, $prefs;
4373		$prefslib = TikiLib::lib('prefs');
4374
4375		$this->table('tiki_preferences')->delete(['name' => $name]);
4376		$cachelib = TikiLib::lib('cache');
4377		$cachelib->invalidate('global_preferences');
4378
4379		$definition = $prefslib->getPreference($name);
4380		$value = $definition['default'];
4381		if (isset($prefs)) {
4382			if (in_array($name, $user_overrider_prefs)) {
4383				$prefs['site_' . $name] = $value;
4384			} elseif (isset($user_preferences[$user][$name])) {
4385				$prefs[$name] = $user_preferences[$user][$name];
4386			} else {
4387				$prefs[$name] = $value;
4388			}
4389		}
4390	}
4391
4392	/**
4393	 * @param $name
4394	 * @param $value
4395	 * @return bool
4396	 */
4397	function set_preference($name, $value)
4398	{
4399		global $user_overrider_prefs, $user_preferences, $user, $prefs;
4400
4401		$prefslib = TikiLib::lib('prefs');
4402
4403		$definition = $prefslib->getPreference($name);
4404
4405		if ($definition && ! $definition['available']) {
4406			return false;
4407		}
4408
4409		$preferences = $this->table('tiki_preferences');
4410		$preferences->insertOrUpdate(['value' => is_array($value) ? serialize($value) : $value], ['name' => $name]);
4411
4412		if (isset($prefs)) {
4413			if (in_array($name, $user_overrider_prefs)) {
4414				$prefs['site_' . $name] = $value;
4415			} elseif (isset($user_preferences[$user][$name])) {
4416				$prefs[$name] = $user_preferences[$user][$name];
4417			} else {
4418				$prefs[$name] = $value;
4419			}
4420		}
4421
4422		// Invalidate cache only after writing to DB to avoid other processes to cache the old info
4423		$menulib = TikiLib::lib('menu');
4424		$menulib->empty_menu_cache();
4425
4426		$cachelib = TikiLib::lib('cache');
4427		$cachelib->invalidate('global_preferences');
4428
4429		return true;
4430	}
4431
4432	/**
4433	 * @param $table
4434	 * @param $field_name
4435	 * @param null $var_names
4436	 * @param $global_ref
4437	 * @param string $query_cond
4438	 * @param null $bindvars
4439	 * @return bool
4440	 */
4441	function _get_values($table, $field_name, $var_names = null, &$global_ref, $query_cond = '', $bindvars = null)
4442	{
4443		if (empty($table) || empty($field_name)) {
4444			return false;
4445		}
4446
4447		$needed = [];
4448		$defaults = null;
4449
4450		if (is_array($var_names)) {
4451			// Detect if var names are specified as keys (then values are considered as var defaults)
4452			//   by looking at the type of the first key
4453			$defaults = ! is_integer(key($var_names));
4454
4455			// Check if we need to get the value from DB by looking in the global $user_preferences array
4456			// (this is able to handle more than one var at a time)
4457			//   ... and store the default values as well, just in case we needs them later
4458			if ($defaults) {
4459				foreach ($var_names as $var => $default) {
4460					if (! isset($global_ref[$var])) {
4461						$needed[$var] = $default;
4462					}
4463				}
4464			} else {
4465				foreach ($var_names as $var) {
4466					if (! isset($global_ref[$var])) {
4467						$needed[$var] = null;
4468					}
4469				}
4470			}
4471		} elseif ($var_names !== null) {
4472			return false;
4473		}
4474
4475		$where = $query_cond;
4476		if (empty($where)) {
4477			$where = '1';
4478		}
4479		if (is_null($bindvars)) {
4480			$bindvars = [];
4481		}
4482		if (count($needed) > 0) {
4483			$where .= ' AND (0';
4484			foreach ($needed as $var => $def) {
4485				$where .= " or `$field_name`=?";
4486				$bindvars[] = $var;
4487			}
4488			$where .= ')';
4489		}
4490		$query = "select `$field_name`, `value` from `$table` where $where";
4491		$result = $this->fetchAll($query, $bindvars);
4492
4493		foreach ($result as $res) {
4494			// store the db value in the global array
4495			$global_ref[$res[$field_name]] = $res['value'];
4496			// remove vars that have a value in db from the $needed array to avoid affecting them a default value
4497			unset($needed[$res[$field_name]]);
4498		}
4499
4500		// set defaults values if needed and if there is no value in database and if it's default was not null
4501		if ($defaults) {
4502			foreach ($needed as $var => $def) {
4503				if (! is_null($def)) {
4504					$global_ref[$var] = $def;
4505				}
4506			}
4507		}
4508		return true;
4509	}
4510
4511	function clear_cache_user_preferences()
4512	{
4513		global $user_preferences;
4514		unset($user_preferences);
4515	}
4516
4517	/**
4518	 * @param $my_user
4519	 * @param null $names
4520	 * @return bool
4521	 */
4522	function get_user_preferences($my_user, $names = null)
4523	{
4524		global $user_preferences;
4525
4526		// $my_user must be specified
4527		if (! is_string($my_user) || $my_user == '') {
4528			return false;
4529		}
4530
4531		global $user_preferences;
4532		if (! is_array($user_preferences) || ! array_key_exists($my_user, $user_preferences)) {
4533			$user_preferences[$my_user] = [];
4534		}
4535		$global_ref =& $user_preferences[$my_user];
4536		$return = $this->_get_values('tiki_user_preferences', 'prefName', $names, $global_ref, '`user`=?', [$my_user]);
4537
4538		// Handle special display_timezone values
4539		if (isset($user_preferences[$my_user]['display_timezone']) && $user_preferences[$my_user]['display_timezone'] != 'Site' && $user_preferences[$my_user]['display_timezone'] != 'Local'
4540				&& ! TikiDate::TimezoneIsValidId($user_preferences[$my_user]['display_timezone'])
4541			 ) {
4542			unset($user_preferences[$my_user]['display_timezone']);
4543		}
4544		return $return;
4545	}
4546
4547	// Returns a boolean indicating whether the specified user (anonymous or not, the current user by default) has the specified preference set
4548	/**
4549	 * @param $preference
4550	 * @param bool $username
4551	 * @return bool
4552	 */
4553	function userHasPreference($preference, $username = false)
4554	{
4555		global $user, $user_preferences;
4556		if ($username === false) {
4557			$username = $user;
4558		}
4559		if ($username) {
4560			if (! isset($user_preferences[$username])) {
4561				$this->get_user_preferences($username);
4562			}
4563			return isset($user_preferences[$username][$preference]);
4564		} else { // If $username is empty, we must be Anonymous looking up one of our own preferences
4565			return isset($_SESSION['preferences'][$preference]);
4566		}
4567	}
4568
4569	/**
4570	 * @param $my_user
4571	 * @param $name
4572	 * @param null $default
4573	 * @return null
4574	 */
4575	function get_user_preference($my_user, $name, $default = null)
4576	{
4577		global $user_preferences, $user;
4578		if ($my_user) {
4579			if ($user != $my_user && ! isset($user_preferences[$my_user])) {
4580				$this->get_user_preferences($my_user);
4581			}
4582			if (isset($user_preferences) && isset($user_preferences[$my_user]) && isset($user_preferences[$my_user][$name])) {
4583				return $user_preferences[$my_user][$name];
4584			}
4585		} else { // If $my_user is empty, we must be Anonymous getting one of our own preferences
4586			if (isset($_SESSION['preferences'][$name])) {
4587				return $_SESSION['preferences'][$name];
4588			}
4589		}
4590		return $default;
4591	}
4592
4593	/**
4594	 * @param $my_user
4595	 * @param $name
4596	 * @param $value
4597	 *
4598	 * @return bool|TikiDb_Pdo_Result|TikiDb_Adodb_Result
4599	 * @throws Exception
4600	 */
4601	function set_user_preference($my_user, $name, $value)
4602	{
4603		global $user_preferences, $prefs, $user, $user_overrider_prefs;
4604
4605		if ($my_user) {
4606			$cachelib = TikiLib::lib('cache');
4607			$cachelib->invalidate('user_details_' . $my_user);
4608
4609			if ($name == "realName") {
4610				// attempt to invalidate userlink cache (does not cover all options - only the default)
4611				$cachelib->invalidate('userlink.' . $user . '.' . $my_user . '0');
4612				$cachelib->invalidate('userlink.' . $my_user . '0');
4613			}
4614
4615			$userPreferences = $this->table('tiki_user_preferences', false);
4616			$userPreferences->delete(['user' => $my_user, 'prefName' => $name]);
4617			$result = $userPreferences->insert(['user' => $my_user,	'prefName' => $name,	'value' => $value]);
4618
4619			$user_preferences[$my_user][$name] = $value;
4620
4621			if ($my_user == $user) {
4622				$prefs[$name] = $value;
4623				if ($name == 'theme' && $prefs['change_theme'] == 'y') {
4624					$prefs['users_prefs_theme'] = $value;
4625					if ($value == '') {
4626						$userPreferences->delete(['user' => $my_user, 'prefName' => $name]);
4627					}
4628				} elseif ($name == 'theme_option' && $prefs['change_theme'] == 'y') {
4629					$prefs['users_prefs_theme-option'] = $value;
4630					if ($value == '') {
4631						$userPreferences->delete(['user' => $my_user, 'prefName' => $name]);
4632					}
4633				} elseif ($value == '') {
4634					if (in_array($name, $user_overrider_prefs)) {
4635						$prefs[$name] = $prefs['site_' . $name];
4636						$userPreferences->delete(['user' => $my_user, 'prefName' => $name]);
4637					}
4638				}
4639				return $result;
4640			}
4641		} else { // If $my_user is empty, we must be Anonymous updating one of our own preferences
4642			if ($name == 'theme' && $prefs['change_theme'] == 'y') {
4643				$prefs['theme'] = $value;
4644				$_SESSION['preferences']['theme'] = $value;
4645				if ($value == '') {
4646					unset($_SESSION['preferences']['theme']);
4647					unset($_SESSION['preferences']['theme_option']);
4648				}
4649			} elseif ($name == 'theme_option' && $prefs['change_theme'] == 'y' && ! empty($_SESSION['preferences']['theme'])) {
4650				$prefs['theme_option'] = $value;
4651				$_SESSION['preferences']['theme_option'] = $value;
4652			} elseif ($value == '') {
4653				if (in_array($name, $user_overrider_prefs)) {
4654					$prefs[$name] = $prefs['site_' . $name];
4655					unset($_SESSION['preferences'][$name]);
4656				}
4657			} else {
4658				$prefs[$name] = $value;
4659				$_SESSION['preferences'][$name] = $value;
4660			}
4661			return true;
4662		}
4663	}
4664
4665	// similar to set_user_preference, but set all at once.
4666	/**
4667	 * @param $my_user
4668	 * @param $preferences
4669	 * @return bool
4670	 */
4671	function set_user_preferences($my_user, &$preferences)
4672	{
4673		global $user_preferences, $prefs, $user;
4674
4675		$cachelib = TikiLib::lib('cache');
4676		$cachelib->invalidate('user_details_' . $my_user);
4677
4678		$userPreferences = $this->table('tiki_user_preferences', false);
4679		$userPreferences->deleteMultiple(['user' => $my_user]);
4680
4681		foreach ($preferences as $prefName => $value) {
4682			$userPreferences->insert(['user' => $my_user, 'prefName' => $prefName, 'value' => $value]);
4683		}
4684		$user_preferences[$my_user] =& $preferences;
4685
4686		if ($my_user == $user) {
4687			$prefs = array_merge($prefs, $preferences);
4688			$_SESSION['s_prefs'] = array_merge($_SESSION['s_prefs'], $preferences);
4689		}
4690		return true;
4691	}
4692
4693	// This implements all the functions needed to use Tiki
4694	/*shared*/
4695	// Returns whether a page named $pageName exists. Unless $casesensitive is set to true, the check is case-insensitive.
4696	/**
4697	 * @param $pageName
4698	 * @param bool $casesensitive
4699	 * @return int
4700	 */
4701	function page_exists($pageName, $casesensitive = false)
4702	{
4703		$page_info = $this->get_page_info($pageName, false);
4704		return ( $page_info !== false && ( ! $casesensitive || $page_info['pageName'] == $pageName ) ) ? 1 : 0;
4705	}
4706
4707	/**
4708	 * @param $pageName
4709	 * @return mixed
4710	 */
4711	function page_exists_desc(&$pageName)
4712	{
4713
4714		$page_info = $this->get_page_info($pageName, false);
4715
4716		return empty($page_info['description']) ? $pageName : $page_info['description'];
4717	}
4718
4719	/**
4720	 * @param $pageName
4721	 * @return bool|int
4722	 */
4723	function page_exists_modtime($pageName)
4724	{
4725		$page_info = $this->get_page_info($pageName, false);
4726		if ($page_info === false) {
4727			return false;
4728		}
4729		return empty($page_info['lastModif']) ? 0 : $page_info['lastModif'];
4730	}
4731
4732	/**
4733	 * @param $pageName
4734	 * @return bool
4735	 */
4736	function add_hit($pageName)
4737	{
4738		global $prefs;
4739		if (StatsLib::is_stats_hit()) {
4740			$pages = $this->table('tiki_pages');
4741			$pages->update(['hits' => $pages->increment(1)], ['pageName' => $pageName]);
4742		}
4743		return true;
4744	}
4745
4746	/** Create a wiki page
4747		@param array $hash- lock_it,contributions, contributors
4748	 **/
4749	function create_page($name, $hits, $data, $lastModif, $comment, $user = 'admin', $ip = '0.0.0.0', $description = '', $lang = '', $is_html = false, $hash = null, $wysiwyg = null, $wiki_authors_style = '', $minor = 0, $created = '')
4750	{
4751		global $prefs, $tracer;
4752		$parserlib = TikiLib::lib('parser');
4753
4754		$tracer->trace('tikilib.create_page', "** invoked");
4755
4756		if (! $is_html) {
4757			$data = str_replace('<x>', '', $data);
4758		}
4759		$name = trim($name); // to avoid pb with trailing space http://dev.mysql.com/doc/refman/5.1/en/char.html
4760
4761		if (! $user) {
4762			$user = 'anonymous';
4763		}
4764		if (empty($wysiwyg)) {
4765			$wysiwyg = $prefs['wysiwyg_default'];
4766			if ($wysiwyg === 'y') {
4767				$is_html = $prefs['wysiwyg_htmltowiki'] !== 'y';
4768			}
4769		}
4770		// Collect pages before modifying data
4771		$pointedPages = $parserlib->get_pages($data, true);
4772
4773		if (! isset($_SERVER["SERVER_NAME"])) {
4774			$_SERVER["SERVER_NAME"] = $_SERVER["HTTP_HOST"] ?? '';
4775		}
4776
4777		if ($this->page_exists($name)) {
4778			Feedback::error(tr('TikiLib::create_page: Cannot create page "%0", it already exists.)', $name));
4779			return false;
4780		}
4781
4782		$tracer->trace('tikilib.create_page', "** TikiLib::lib...");
4783		$tracer->trace('tikilib.create_page', "** invoking process_save_plugins, \$parserlib=" . get_class($parserlib));
4784		$data = $parserlib->process_save_plugins(
4785			$data,
4786			[
4787				'type' => 'wiki page',
4788				'itemId' => $name,
4789				'user' => $user,
4790			]
4791		);
4792
4793		$html = $is_html ? 1 : 0;
4794		if ($html && $prefs['feature_purifier'] != 'n') {
4795			$noparsed = [];
4796			$parserlib->plugins_remove($data, $noparsed);
4797
4798			require_once('lib/htmlpurifier_tiki/HTMLPurifier.tiki.php');
4799			$data = HTMLPurifier($data);
4800
4801			$parserlib->plugins_replace($data, $noparsed, true);
4802		}
4803
4804		$insertData = [
4805			'pageName' => $name,
4806			'pageSlug' => TikiLib::lib('slugmanager')->generate($prefs['wiki_url_scheme'] ?: 'dash', $name, $prefs['url_only_ascii'] === 'y'),
4807			'hits' => (int) $hits,
4808			'data' => $data,
4809			'description' => $description,
4810			'lastModif' => (int) $lastModif,
4811			'comment' => $comment,
4812			'version' => 1,
4813			'version_minor' => $minor,
4814			'user' => $user,
4815			'ip' => $ip,
4816			'creator' => $user,
4817			'page_size' => strlen($data),
4818			'is_html' => $html,
4819			'created' => empty($created) ? $this->now : $created,
4820			'wysiwyg' => $wysiwyg,
4821			'wiki_authors_style' => $wiki_authors_style,
4822		];
4823		if ($lang) {
4824			$insertData['lang'] = $lang;
4825		}
4826		if (! empty($hash['lock_it']) && ($hash['lock_it'] == 'y' || $hash['lock_it'] == 'on')) {
4827			$insertData['flag'] = 'L';
4828			$insertData['lockedby'] = $user;
4829		} elseif (empty($hash['lock_it']) || $hash['lock_it'] == 'n') {
4830			$insertData['flag'] = '';
4831			$insertData['lockedby'] = '';
4832		}
4833		if ($prefs['wiki_comments_allow_per_page'] != 'n') {
4834			if (! empty($hash['comments_enabled']) && $hash['comments_enabled'] == 'y') {
4835				$insertData['comments_enabled'] = 'y';
4836			} elseif (empty($hash['comments_enabled']) || $hash['comments_enabled'] == 'n') {
4837				$insertData['comments_enabled'] = 'n';
4838			}
4839		}
4840		if (empty($hash['contributions'])) {
4841			$hash['contributions'] = '';
4842		}
4843		if (empty($hash['contributors'])) {
4844			$hash2 = '';
4845		} else {
4846			foreach ($hash['contributors'] as $c) {
4847				$hash3['contributor'] = $c;
4848				$hash2[] = $hash3;
4849			}
4850		}
4851		$pages = $this->table('tiki_pages');
4852		$page_id = $pages->insert($insertData);
4853
4854		//update status, page storage was updated in tiki 9 to be non html encoded
4855		$wikilib = TikiLib::lib('wiki');
4856		$converter = new convertToTiki9();
4857		$converter->saveObjectStatus($page_id, 'tiki_pages');
4858
4859		$this->replicate_page_to_history($name);
4860
4861		$this->clear_links($name);
4862
4863		// Pages are collected before adding slashes
4864		foreach ($pointedPages as $pointedPage => $types) {
4865			$this->replace_link($name, $pointedPage, $types);
4866		}
4867
4868		$wikilib->update_wikicontent_relations($data, 'wiki page', $name);
4869
4870		// Update the log
4871		if (strtolower($name) != 'sandbox') {
4872			$logslib = TikiLib::lib('logs');
4873			$logslib->add_action("Created", $name, 'wiki page', 'add=' . strlen($data), $user, '', '', $created, $hash['contributions'], $hash2);
4874			//get_strings tra("Created");
4875
4876			// Need to categorize new pages before sending mail notifications to make sure category permissions are considered
4877			if (! empty($_REQUEST['cat_categories']) && ! empty($_REQUEST["page"])) {
4878				// these variables are used in categorize.php
4879				$cat_type = 'wiki page';
4880				$cat_objid = $_REQUEST["page"];
4881				include_once("categorize.php");
4882			}
4883
4884			//  Deal with mail notifications.
4885			include_once(__DIR__ . '/notifications/notificationemaillib.php');
4886
4887			$foo = parse_url($_SERVER["REQUEST_URI"] ?? '');
4888			$machine = self::httpPrefix(true) . dirname($foo["path"]);
4889			sendWikiEmailNotification('wiki_page_created', $name, $user, $comment, 1, $data, $machine, '', false, $hash['contributions']);
4890			if ($prefs['feature_contribution'] == 'y') {
4891				$contributionlib = TikiLib::lib('contribution');
4892				$contributionlib->assign_contributions($hash['contributions'], $name, 'wiki page', $description, $name, "tiki-index.php?page=" . urlencode($name));
4893			}
4894		}
4895
4896		//if there are links to this page, clear cache to avoid linking to edition
4897		$toInvalidate = $this->table('tiki_links')->fetchColumn('fromPage', ['toPage' => $name]);
4898		foreach ($toInvalidate as $res) {
4899			$this->invalidate_cache($res);
4900		}
4901
4902		TikiLib::events()->trigger(
4903			'tiki.wiki.create',
4904			[
4905				'type' => 'wiki page',
4906				'object' => $name,
4907				'namespace' => $wikilib->get_namespace($name),
4908				'user' => $GLOBALS['user'],
4909				'page_id' => $page_id,
4910				'version' => 1,
4911				'data' => $data,
4912				'old_data' => '',
4913			]
4914		);
4915
4916		// Update links to the URL of the new page from HTML wiki pages (when wysiwyg is in use).
4917		// This is not an elegant fix but will do for now until the "use wiki syntax in WYSIWYG" feature is ready (if that ever replaces HTML-mode WYSIWYG completely).
4918		if ($prefs['feature_wysiwyg'] == 'y' && $prefs['wysiwyg_htmltowiki'] != 'y') {
4919			$wikilib = TikiLib::lib('wiki');
4920			$temppage = md5($this->now . $name);
4921			$wikilib->wiki_rename_page($name, $temppage, false, $user);
4922			$wikilib->wiki_rename_page($temppage, $name, false, $user);
4923		}
4924
4925		$tracer->trace('tikilib.create_page', "** Returning");
4926
4927		return true;
4928	}
4929
4930	/**
4931	 * @param $pageName
4932	 * @return bool|mixed
4933	 */
4934	protected function replicate_page_to_history($pageName)
4935	{
4936		if (strtolower($pageName) == 'sandbox') {
4937			return false;
4938		}
4939
4940		$query = "INSERT IGNORE INTO `tiki_history`(`pageName`, `version`, `version_minor`, `lastModif`, `user`, `ip`, `comment`, `data`, `description`,`is_html`)
4941			SELECT `pageName`, `version`, `version_minor`, `lastModif`, `user`, `ip`, `comment`, `data`, `description`,`is_html`
4942			FROM tiki_pages
4943			WHERE pageName = ?
4944			LIMIT 1";
4945
4946		$this->query($query, [$pageName]);
4947
4948		$id = $this->lastInsertId();
4949
4950		//update status, we don't want the page to be decoded later
4951		$wikilib = TikiLib::lib('wiki');
4952		$converter = new convertToTiki9();
4953		$converter->saveObjectStatus($id, 'tiki_history');
4954
4955		return $id;
4956	}
4957
4958	/**
4959	 * @param $pageName
4960	 * @return bool|mixed
4961	 */
4962	public function restore_page_from_history($pageName, $version = null)
4963	{
4964		if (strtolower($pageName) == 'sandbox') {
4965			return false;
4966		}
4967
4968		$query = "SELECT `version`, `version_minor`, `lastModif`, `user`, `ip`, `comment`, `data`, `description`,`is_html`
4969			FROM tiki_history
4970			WHERE pageName = ? ";
4971
4972		$bindvars = [$pageName];
4973
4974		if ($version === null) {
4975			$query .= "ORDER BY version DESC";
4976		} else {
4977			$query .= "AND `version`=?";
4978			$bindvars[] = $version;
4979		}
4980
4981		$result = $this->query($query, $bindvars, 1);
4982		if ($res = $result->fetchRow()) {
4983			$query = "UPDATE `tiki_pages`
4984			SET `version` = ?, `version_minor` = ?, `lastModif` = ?, `user` = ?, `ip` = ?, `comment` = ?, `data` = ?, `description` = ?,`is_html` = ?
4985			WHERE pageName = ?";
4986			$bindvars = [$res['version'], $res['version_minor'], $res['lastModif'], $res['user'], $res['ip'], $res['comment'], $res['data'], $res['description'], $res['is_html'], $pageName];
4987			$this->query($query, $bindvars);
4988		}
4989
4990		$bindvars = [$pageName];
4991		$query = "SELECT `page_id` from `tiki_pages` WHERE pageName = ?";
4992		$this->query($query, $bindvars);
4993
4994		if ($res = $result->fetchRow()) {
4995			$id = $res['version'];
4996		}
4997
4998		// FIXME: Are these lines necessary? If so, what is the proper status to use?
4999		//$converter = new convertToTiki9();
5000		//$converter->saveObjectStatus($id, 'tiki_pages', 'conv9.0');
5001
5002		return $id;
5003	}
5004
5005	/**
5006	 * @param $user
5007	 * @param $max
5008	 * @param string $who
5009	 * @return mixed
5010	 */
5011	function get_user_pages($user, $max, $who = 'user')
5012	{
5013		return $this->table('tiki_pages')->fetchAll(['pageName'], [$who => $user], $max);
5014	}
5015
5016	/**
5017	 * @param $user
5018	 * @param $max
5019	 * @return array
5020	 */
5021	function get_user_galleries($user, $max)
5022	{
5023		$query = "select `name` ,`galleryId`  from `tiki_galleries` where `user`=? order by `name` asc";
5024
5025		$result = $this->fetchAll($query, [$user], $max);
5026		$ret = [];
5027
5028		foreach ($result as $res) {
5029			//FIXME Perm::filter ?
5030			if ($this->user_has_perm_on_object($user, $res['galleryId'], 'image gallery', 'tiki_p_view_image_gallery')) {
5031				$ret[] = $res;
5032			}
5033		}
5034		return $ret;
5035	}
5036
5037	/**
5038	 * @param $pageName
5039	 * @return bool
5040	 */
5041	function get_page_print_info($pageName)
5042	{
5043		$query = "SELECT `pageName`, `data` as `parsed`, `is_html` FROM `tiki_pages` WHERE `pageName`=?";
5044		$result = $this->query($query, [$pageName]);
5045		if (! $result->numRows()) {
5046			return false;
5047		} else {
5048			$page_info = $result->fetchRow();
5049			$page_info['parsed'] = TikiLib::lib('parser')->parse_data($page_info['parsed'], ['is_html' => $page_info['is_html'], 'print' => 'y', 'page' => $pageName]);
5050			$page_info['h'] = 1;
5051		}
5052		return $page_info;
5053	}
5054
5055	/**
5056	 * @param $pageName
5057	 * @param bool $retrieve_datas
5058	 * @param bool $skipCache
5059	 * @return bool
5060	 */
5061	function get_page_info($pageName, $retrieve_datas = true, $skipCache = false)
5062	{
5063		global $prefs;
5064		$pageNameEncode = urlencode($pageName);
5065		if (! $skipCache && isset($this->cache_page_info[$pageNameEncode])
5066			&& ( ! $retrieve_datas || isset($this->cache_page_info[$pageNameEncode]['data']) )
5067		) {
5068			return $this->cache_page_info[$pageNameEncode];
5069		}
5070
5071		if ($retrieve_datas) {
5072			$query = "SELECT * FROM `tiki_pages` WHERE `pageName`=?";
5073		} else {
5074			$query = "SELECT `page_id`, `pageName`, `hits`, `description`, `lastModif`, `comment`, `version`, `version_minor`, `user`, `ip`, `flag`, `points`, `votes`, `wiki_cache`, `cache_timestamp`, `pageRank`, `creator`, `page_size`, `lang`, `lockedby`, `is_html`, `created`, `wysiwyg`, `wiki_authors_style`, `comments_enabled` FROM `tiki_pages` WHERE `pageName`=?";
5075		}
5076		$result = $this->query($query, [$pageName]);
5077
5078		if (! $result->numRows()) {
5079			return false;
5080		} else {
5081			$row = $result->fetchRow();
5082			$row['baseName'] = TikiLib::lib('wiki')->get_without_namespace($row['pageName']);
5083			$row['prettyName'] = TikiLib::lib('wiki')->get_readable($row['pageName']);
5084			$row['namespace'] = TikiLib::lib('wiki')->get_namespace($row['pageName']);
5085			$row['namespace_parts'] = TikiLib::lib('wiki')->get_namespace_parts($row['pageName']);
5086
5087			// Be sure to have the correct character case (because DB is caseinsensitive)
5088			$pageNameEncode = urlencode($row['pageName']);
5089
5090			// Limit memory usage of the page cache.  No
5091			// intelligence is attempted here whatsoever.  This was
5092			// done because a few thousand ((page)) links would blow
5093			// up memory, even with the limit at 128MiB.
5094			// Information on 128 pages really should be plenty.
5095			while (count($this->cache_page_info) >= 128) {
5096				// Need to delete something; pick at random
5097				$keys = array_keys($this->cache_page_info);
5098				$num = rand(0, count($keys));
5099				if (isset($keys[$num])) {
5100					unset($this->cache_page_info[$keys[$num]]);
5101				}
5102			}
5103			$row['outputType'] = '';	// TODO remove as redundant?
5104
5105			$this->cache_page_info[$pageNameEncode] = $row;
5106			return $this->cache_page_info[$pageNameEncode];
5107		}
5108	}
5109
5110	/**
5111	 * @param $page_id
5112	 * @return mixed
5113	 */
5114	function get_page_info_from_id($page_id)
5115	{
5116		return $this->table('tiki_pages')->fetchFullRow(['page_id' => $page_id]);
5117	}
5118
5119
5120	/**
5121	 * @param $page_id
5122	 * @return mixed
5123	 */
5124	function get_page_name_from_id($page_id)
5125	{
5126		return $this->table('tiki_pages')->fetchOne('pageName', ['page_id' => $page_id]);
5127	}
5128
5129	/**
5130	 * @param $page
5131	 * @return mixed
5132	 */
5133	function get_page_id_from_name($page)
5134	{
5135		return $this->table('tiki_pages')->fetchOne('page_id', ['pageName' => $page]);
5136	}
5137
5138	/**
5139	 * @param $str
5140	 * @param $car
5141	 * @return int
5142	 */
5143	static function how_many_at_start($str, $car)
5144	{
5145		$cant = 0;
5146		$i = 0;
5147		while (($i < strlen($str)) && (isset($str{$i})) && ($str{$i} == $car)) {
5148			$i++;
5149			$cant++;
5150		}
5151		return $cant;
5152	}
5153
5154	/**
5155	 * @param $name
5156	 * @param $domain
5157	 * @param string $sep
5158	 * @return string
5159	 */
5160	static function protect_email($name, $domain, $sep = '@')
5161	{
5162		TikiLib::lib('header')->add_jq_onready(
5163			'$(".convert-mailto").removeClass("convert-mailto").each(function () {
5164				var address = $(this).data("encode-name") + "@" + $(this).data("encode-domain");
5165				$(this).attr("href", "mailto:" + address).text(address);
5166			});'
5167		);
5168		return "<a class=\"convert-mailto\" href=\"mailto:nospam@example.com\" data-encode-name=\"$name\" data-encode-domain=\"$domain\">$name " . tra("at", "", true) . " $domain</a>";
5169	}
5170
5171	//Updates a dynamic variable found in some object
5172	/*Shared*/
5173	/**
5174	 * @param $name
5175	 * @param $value
5176	 * @param null $lang
5177	 * @return bool
5178	 */
5179	function update_dynamic_variable($name, $value, $lang = null)
5180	{
5181		$dynamicVariables = $this->table('tiki_dynamic_variables');
5182		$dynamicVariables->delete(['name' => $name, 'lang' => $lang]);
5183		$dynamicVariables->insert(['name' => $name, 'data' => $value, 'lang' => $lang]);
5184		return true;
5185	}
5186
5187	/**
5188	 * @param $page
5189	 */
5190	function clear_links($page)
5191	{
5192		$this->table('tiki_links')->deleteMultiple(['fromPage' => $page]);
5193
5194		$objectRelations = $this->table('tiki_object_relations');
5195		$objectRelations->deleteMultiple(
5196			[
5197				'source_type' => 'wiki page',
5198				'source_itemId' => $page,
5199				'target_type' => 'wiki page',
5200				'relation' => $objectRelations->like('tiki.link.%'),
5201			]
5202		);
5203	}
5204
5205	/**
5206	 * @param $pageFrom
5207	 * @param $pageTo
5208	 * @param array $types
5209	 */
5210	function replace_link($pageFrom, $pageTo, $types = [])
5211	{
5212		global $prefs;
5213		if ($prefs['namespace_enabled'] == 'y' && $prefs['namespace_force_links'] == 'y'
5214			&& TikiLib::lib('wiki')->get_namespace($pageFrom)
5215			&& ! TikiLib::lib('wiki')->get_namespace($pageTo) ) {
5216				$namespace = TikiLib::lib('wiki')->get_namespace($pageFrom);
5217				$pageTo = $namespace . $prefs['namespace_separator'] . $pageTo;
5218		}
5219		// The max pagename length is 160 characters ( tiki_pages.pageName varchar(160) ).
5220		//	However, wiki_rename_page stores a page in the format: $tmpName = "~".$newName."~";
5221		//	So, actual max page name length is 160 - 2 = 158
5222		//	Strip excess characters (silently) and proceed.
5223		$pageTo = substr($pageTo, 0, 158);
5224
5225		$links = $this->table('tiki_links');
5226		$links->insert(['fromPage' => $pageFrom, 'toPage' => $pageTo], true);
5227
5228		$relationlib = TikiLib::lib('relation');
5229		foreach ($types as $type) {
5230			$relationlib->add_relation("tiki.link.$type", 'wiki page', $pageFrom, 'wiki page', $pageTo);
5231		}
5232	}
5233
5234	/**
5235	 * @param $page
5236	 */
5237	function invalidate_cache($page)
5238	{
5239		unset($this->cache_page_info[urlencode($page)]);
5240		$this->table('tiki_pages')->update(['cache_timestamp' => 0], ['pageName' => $page]);
5241
5242		$pageCache = Tiki_PageCache::create()
5243			->checkMeta('wiki-page-output-meta-timestamp', ['page' => $page ])
5244			->invalidate();
5245	}
5246
5247	/** Update a wiki page
5248		@param array $hash- lock_it,contributions, contributors
5249		@param int $saveLastModif - modification time - pass null for now, unless importing a Wiki page
5250	 **/
5251	function update_page($pageName, $edit_data, $edit_comment, $edit_user, $edit_ip, $edit_description = null, $edit_minor = 0, $lang = '', $is_html = null, $hash = null, $saveLastModif = null, $wysiwyg = '', $wiki_authors_style = '')
5252	{
5253		global $prefs;
5254		$histlib = TikiLib::lib('hist');
5255		$parserlib = TikiLib::lib('parser');
5256
5257		if (! $edit_user) {
5258			$edit_user = 'anonymous';
5259		}
5260
5261		$this->invalidate_cache($pageName);
5262		// Collect pages before modifying edit_data (see update of links below)
5263		$pages = $parserlib->get_pages($edit_data, true);
5264
5265		if (! $this->page_exists($pageName)) {
5266			return false;
5267		}
5268
5269		// Get this page information
5270		$info = $this->get_page_info($pageName);
5271
5272		if ($edit_description === null) {
5273			$edit_description = $info['description'];
5274		}
5275
5276		$user = $info["user"] ? $info["user"] : 'anonymous';
5277		$data = $info["data"];
5278		$willDoHistory = ($prefs['feature_wiki_history_full'] == 'y' || $data != $edit_data || $info['description'] != $edit_description || $info["comment"] != $edit_comment );
5279		$version = $histlib->get_page_next_version($pageName, $willDoHistory);
5280		$old_version = $version - 1;	// this doesn't really make sense but is needed to make diff links work properly - regression from r65651
5281
5282		if ($is_html === null) {
5283			$html = $info['is_html'];
5284		} else {
5285			$html = $is_html ? 1 : 0;
5286		}
5287		if ($wysiwyg == '') {
5288			$wysiwyg = $info['wysiwyg'];
5289		}
5290
5291		if ($wysiwyg == 'y' && $html != 1 && $prefs['wysiwyg_htmltowiki'] != 'y') {	// correct for html only wysiwyg
5292			$html = 1;
5293		}
5294
5295		$edit_data = $parserlib->process_save_plugins(
5296			$edit_data,
5297			[
5298				'type' => 'wiki page',
5299				'itemId' => $pageName,
5300				'user' => $user,
5301			]
5302		);
5303
5304		if ($html == 1 && $prefs['feature_purifier'] != 'n') {
5305			$noparsed = [];
5306			$parserlib->plugins_remove($edit_data, $noparsed);
5307
5308			require_once('lib/htmlpurifier_tiki/HTMLPurifier.tiki.php');
5309			$edit_data = HTMLPurifier($edit_data);
5310
5311			$parserlib->plugins_replace($edit_data, $noparsed, true);
5312		}
5313
5314		if (is_null($saveLastModif)) {
5315			$saveLastModif = $this->now;
5316		}
5317
5318		if (empty($lang)) {
5319			$lang = $info['lang'];
5320		}
5321
5322		$queryData = [
5323			'description' => $edit_description,
5324			'data' => $edit_data,
5325			'comment' => $edit_comment,
5326			'lastModif' => (int) $saveLastModif,
5327			'version' => $version,
5328			'version_minor' => $edit_minor,
5329			'user' => $edit_user,
5330			'ip' => $edit_ip,
5331			'page_size' => strlen($edit_data),
5332			'is_html' => $html,
5333			'wysiwyg' => $wysiwyg,
5334			'wiki_authors_style' => $wiki_authors_style,
5335			'lang' => $lang,
5336		];
5337
5338		if ($hash !== null) {
5339			if (! empty($hash['lock_it']) && ($hash['lock_it'] == 'y' || $hash['lock_it'] == 'on')) {
5340				$queryData['flag'] = 'L';
5341				$queryData['lockedby'] = $user;
5342			} elseif (empty($hash['lock_it']) || $hash['lock_it'] == 'n') {
5343				$queryData['flag'] = '';
5344				$queryData['lockedby'] = '';
5345			}
5346		}
5347		if ($prefs['wiki_comments_allow_per_page'] != 'n') {
5348			if (! empty($hash['comments_enabled']) && $hash['comments_enabled'] == 'y') {
5349				$queryData['comments_enabled'] = 'y';
5350			} elseif (empty($hash['comments_enabled']) || $hash['comments_enabled'] == 'n') {
5351				$queryData['comments_enabled'] = 'n';
5352			}
5353		}
5354		if (empty($hash['contributions'])) {
5355			$hash['contributions'] = '';
5356		}
5357		if (empty($hash['contributors'])) {
5358			$hash2 = '';
5359		} else {
5360			foreach ($hash['contributors'] as $c) {
5361				$hash3['contributor'] = $c;
5362				$hash2[] = $hash3;
5363			}
5364		}
5365
5366		$this->table('tiki_pages')->update($queryData, ['pageName' => $pageName]);
5367
5368		// Synchronize object comment
5369		if ($prefs['feature_wiki_description'] == 'y') {
5370			$query = 'update `tiki_objects` set `description`=? where `itemId`=? and `type`=?';
5371			$this->query($query, [ $edit_description, $pageName, 'wiki page']);
5372		}
5373
5374		//update status, page storage was updated in tiki 9 to be non html encoded
5375		$wikilib = TikiLib::lib('wiki');
5376		$converter = new convertToTiki9();
5377		$converter->saveObjectStatus($this->getOne("SELECT page_id FROM tiki_pages WHERE pageName = ?", [$pageName]), 'tiki_pages');
5378
5379		// Parse edit_data updating the list of links from this page
5380		$this->clear_links($pageName);
5381
5382		// Pages collected above
5383		foreach ($pages as $page => $types) {
5384			$this->replace_link($pageName, $page, $types);
5385		}
5386
5387		$wikilib->update_wikicontent_relations($edit_data, 'wiki page', $pageName);
5388
5389		if (strtolower($pageName) != 'sandbox' && ! $edit_minor) {
5390			$maxversions = $prefs['maxVersions'];
5391
5392			if ($maxversions && ($nb = $histlib->get_nb_history($pageName)) > $maxversions) {
5393				// Select only versions older than keep_versions days
5394				$keep = $prefs['keep_versions'];
5395
5396				$oktodel = $saveLastModif - ($keep * 24 * 3600) + 1;
5397
5398				$history = $this->table('tiki_history');
5399				$result = $history->fetchColumn(
5400					'version',
5401					['pageName' => $pageName,'lastModif' => $history->lesserThan($oktodel)],
5402					$nb - $maxversions,
5403					0,
5404					['lastModif' => 'ASC']
5405				);
5406				foreach ($result as $toRemove) {
5407					$histlib->remove_version($pageName, $toRemove);
5408				}
5409			}
5410		}
5411
5412		// This if no longer checks for minor-ness of the change; sendWikiEmailNotification does that.
5413		if ($willDoHistory) {
5414			$this->replicate_page_to_history($pageName);
5415			if (strtolower($pageName) != 'sandbox') {
5416				if ($prefs['feature_contribution'] == 'y') {// transfer page contributions to the history
5417					$contributionlib = TikiLib::lib('contribution');
5418					$history = $this->table('tiki_history');
5419					$historyId = $history->fetchOne($history->max('historyId'), ['pageName' => $pageName, 'version' => (int) $old_version]);
5420					$contributionlib->change_assigned_contributions($pageName, 'wiki page', $historyId, 'history', '', $pageName . '/' . $old_version, "tiki-pagehistory.php?page=$pageName&preview=$old_version");
5421				}
5422			}
5423			include_once('lib/diff/difflib.php');
5424			if (strtolower($pageName) != 'sandbox') {
5425				$logslib = TikiLib::lib('logs');
5426				$bytes = diff2($data, $edit_data, 'bytes');
5427				$logslib->add_action('Updated', $pageName, 'wiki page', $bytes, $edit_user, $edit_ip, '', $saveLastModif, $hash['contributions'], $hash2);
5428				if ($prefs['feature_contribution'] == 'y') {
5429					$contributionlib = TikiLib::lib('contribution');
5430					$contributionlib->assign_contributions($hash['contributions'], $pageName, 'wiki page', $edit_description, $pageName, "tiki-index.php?page=" . urlencode($pageName));
5431				}
5432			}
5433
5434			if ($prefs['feature_multilingual'] == 'y' && $lang) {
5435				// Need to update the translated objects table when an object's language changes.
5436				$this->table('tiki_translated_objects')->update(['lang' => $lang], ['type' => 'wiki page', 'objId' => $info['page_id']]);
5437			}
5438
5439			if ($prefs['wiki_watch_minor'] != 'n' || ! $edit_minor) {
5440				//  Deal with mail notifications.
5441				include_once(__DIR__ . '/notifications/notificationemaillib.php');
5442				$histlib = TikiLib::lib('hist');
5443				$old = $histlib->get_version($pageName, $old_version);
5444				$foo = parse_url($_SERVER["REQUEST_URI"]);
5445				$machine = self::httpPrefix(true) . dirname($foo["path"]);
5446				$diff = diff2($old["data"], $edit_data, "unidiff"); // TODO: Only compute if we have at least one notification to send
5447				sendWikiEmailNotification('wiki_page_changed', $pageName, $edit_user, $edit_comment, $old_version, $edit_data, $machine, $diff, $edit_minor, $hash['contributions'], 0, 0, $lang);
5448			}
5449		}
5450
5451		$tx = $this->begin();
5452
5453		TikiLib::events()->trigger(
5454			'tiki.wiki.update',
5455			[
5456				'type' => 'wiki page',
5457				'object' => $pageName,
5458				'namespace' => $wikilib->get_namespace($pageName),
5459				'reply_action' => 'comment',
5460				'user' => $GLOBALS['user'],
5461				'page_id' => $info['page_id'],
5462				'version' => $version,
5463				'old_version' => $old_version,
5464				'data' => $edit_data,
5465				'old_data' => $info['data'],
5466				'edit_comment' => $edit_comment,
5467			]
5468		);
5469
5470		$tx->commit();
5471	}
5472
5473	/**
5474	 * @param $context
5475	 * @param $data
5476	 */
5477	function object_post_save($context, $data)
5478	{
5479		global $prefs;
5480
5481		if (is_array($data)) {
5482			if (isset($data['content']) && $prefs['feature_file_galleries'] == 'y') {
5483				$filegallib = TikiLib::lib('filegal');
5484				$filegallib->syncFileBacklinks($data['content'], $context);
5485			}
5486
5487			if (isset($data['content'])) {
5488				$this->plugin_post_save_actions($context, $data);
5489			}
5490		} else {
5491			if (isset($context['content']) && $prefs['feature_file_galleries'] == 'y') {
5492				$filegallib = TikiLib::lib('filegal');
5493				$filegallib->syncFileBacklinks($context['content'], $context);
5494			}
5495
5496			$this->plugin_post_save_actions($context);
5497		}
5498	}
5499
5500	/**
5501	 * Foreach plugin used in a object content call its save handler,
5502	 * if one exist, and send email notifications when it has pending
5503	 * status, if preference is enabled.
5504	 *
5505	 * A plugin save handler is a function defined on the plugin file
5506	 * with the following format: wikiplugin_$pluginName_save()
5507	 *
5508	 * This function is called from object_post_save. Do not call directly.
5509	 *
5510	 * @param array $context object type and id
5511	 * @param array $data
5512	 * @return void
5513	 */
5514	private function plugin_post_save_actions($context, $data = null)
5515	{
5516		global $prefs;
5517		$parserlib = TikiLib::lib('parser');
5518
5519		if (is_null($data)) {
5520			$content = [];
5521			if (isset($context['values'])) {
5522				$content = $context['values'];
5523			}
5524			if (isset($context['data'])) {
5525				$content[] = $context['data'];
5526			}
5527			$data['content'] = implode(' ', $content);
5528		}
5529
5530		$argumentParser = new WikiParser_PluginArgumentParser;
5531
5532		$matches = WikiParser_PluginMatcher::match($data['content']);
5533
5534		foreach ($matches as $match) {
5535			$plugin_name = $match->getName();
5536			$body = $match->getBody();
5537			$arguments = $argumentParser->parse($match->getArguments());
5538
5539			$dummy_output = '';
5540			if ($parserlib->plugin_enabled($plugin_name, $dummy_output)) {
5541				$status = $parserlib->plugin_can_execute($plugin_name, $body, $arguments, true);
5542
5543				// when plugin status is pending, $status equals plugin fingerprint
5544				if ($prefs['wikipluginprefs_pending_notification'] == 'y' && $status !== true && $status != 'rejected') {
5545					$this->plugin_pending_notification($plugin_name, $body, $arguments, $context);
5546				}
5547
5548				WikiPlugin_Negotiator_Wiki_Alias::findImplementation($plugin_name, $body, $arguments);
5549
5550				$func_name = 'wikiplugin_' . $plugin_name . '_save';
5551
5552				if (function_exists($func_name)) {
5553					$func_name($context, $body, $arguments);
5554				}
5555			}
5556		}
5557	}
5558
5559	/**
5560	 * Send notification by email that a plugin is waiting to be
5561	 * approved to everyone with permission to approve it.
5562	 *
5563	 * @param string $plugin_name
5564	 * @param string $body plugin body
5565	 * @param array $arguments plugin arguments
5566	 * @param array $context object type and id
5567	 * @return void
5568	 */
5569	private function plugin_pending_notification($plugin_name, $body, $arguments, $context)
5570	{
5571		require_once('lib/webmail/tikimaillib.php');
5572		global $prefs, $base_url;
5573		$mail = new TikiMail(null, $prefs['sender_email']);
5574		$mail->setSubject(tr("Plugin %0 pending approval", $plugin_name));
5575
5576		$smarty = TikiLib::lib('smarty');
5577		$smarty->assign('plugin_name', $plugin_name);
5578		$smarty->assign('type', $context['type']);
5579		$smarty->assign('objectId', $context['object']);
5580		$smarty->assign('arguments', $arguments);
5581		$smarty->assign('body', $body);
5582
5583		$mail->setHtml(nl2br($smarty->fetch('mail/plugin_pending_notification.tpl')));
5584
5585		$recipients = $this->plugin_get_email_users_with_perm();
5586
5587		$mail->setBcc($recipients);
5588
5589		if (! empty($prefs['sender_email'])) {
5590			$mail->send([$prefs['sender_email']]);
5591		} elseif ($admin_email = TikiLib::lib('user')->get_user_email('admin')) {
5592			$recipients = array_diff($recipients, [$admin_email]);
5593			$mail->setBcc($recipients);
5594			$mail->send([$admin_email]);
5595		}
5596	}
5597
5598	/**
5599	 * Return a list of e-mails from the users with permission
5600	 * to approve a plugin.
5601	 *
5602	 * @return array
5603	 */
5604	private function plugin_get_email_users_with_perm()
5605	{
5606		global $prefs;
5607		$userlib = TikiLib::lib('user');
5608
5609		$allGroups = $userlib->get_groups();
5610		$accessor = Perms::get();
5611
5612		// list of groups with permission to approve plugin on this object
5613		$groups = [];
5614
5615		foreach ($allGroups['data'] as $group) {
5616			$accessor->setGroups([$group['groupName']]);
5617			if ($accessor->plugin_approve) {
5618				$groups[] = $group['groupName'];
5619			}
5620		}
5621
5622		$recipients = [];
5623
5624		foreach ($groups as $group) {
5625			$recipients = array_merge($recipients, $userlib->get_group_users($group, 0, -1, 'email'));
5626		}
5627
5628		$recipients = array_filter($recipients);
5629		$recipients = array_unique($recipients);
5630
5631		if ($prefs['user_plugin_approval_watch_editor'] === 'y') {
5632			# Do not self-notify, therefore, remove user's email from the list of recipients
5633			$useremail = TikiLib::lib('user')->get_user_email($user);
5634			$recipients = array_diff($recipients, array($useremail));
5635		}
5636
5637		return $recipients;
5638	}
5639
5640	/**
5641	 * @param bool $_user
5642	 * @return null|string
5643	 */
5644	function get_display_timezone($_user = false)
5645	{
5646		global $prefs, $user;
5647
5648		if ($_user === false || $_user == $user) {
5649			// If the requested timezone is the current user timezone
5650			$tz = $prefs['display_timezone'];
5651		} elseif ($_user) {
5652			// ... else, get the user timezone preferences from DB
5653			$tz = $this->get_user_preference($_user, 'display_timezone');
5654		}
5655		if (! TikiDate::TimezoneIsValidId($tz)) {
5656			$tz = $prefs['server_timezone'];
5657		}
5658		if (! TikiDate::TimezoneIsValidId($tz)) {
5659			$tz = 'UTC';
5660		}
5661		return $tz;
5662	}
5663
5664	function set_display_timezone($user)
5665	{
5666		global $prefs, $user_preferences;
5667
5668		if ($prefs['users_prefs_display_timezone'] == 'Site' ||
5669			(isset($user_preferences[$user]['display_timezone']) && $user_preferences[$user]['display_timezone'] == 'Site')) {
5670			// Stay in the time zone of the server
5671			$prefs['display_timezone'] = $prefs['server_timezone'];
5672		} elseif (empty($user_preferences[$user]['display_timezone']) || $user_preferences[$user]['display_timezone'] == 'Local') {
5673			// If the display timezone is not known ...
5674			if (isset($_COOKIE['local_tz'])) {
5675				//   ... we try to use the timezone detected by javascript and stored in cookies
5676				if (TikiDate::TimezoneIsValidId($_COOKIE['local_tz'])) {
5677					$prefs['display_timezone'] = $_COOKIE['local_tz'];
5678				} elseif (in_array(strtolower($_COOKIE['local_tz']), TikiDate::getTimezoneAbbreviations())) {	// abbreviation like BST or CEST
5679					// timezone_offset in seconds
5680					$prefs['timezone_offset'] = isset($_COOKIE['local_tzoffset']) ? (int) $_COOKIE['local_tzoffset'] * 60 * 60 : -1;
5681					$tzname = timezone_name_from_abbr($_COOKIE['local_tz'], $prefs['timezone_offset']);
5682
5683					if (TikiDate::TimezoneIsValidId($tzname)) {	// double check
5684						$prefs['display_timezone'] = $tzname;
5685					} else {
5686						$prefs['display_timezone'] = $_COOKIE['local_tz'];
5687					}
5688				} elseif ($_COOKIE['local_tz'] == 'HAEC') {
5689					// HAEC, returned by Safari on Mac, is not recognized as a DST timezone (with daylightsavings)
5690					//  ... So use one equivalent timezone name
5691					$prefs['display_timezone'] = 'Europe/Paris';
5692				} else {
5693					$prefs['display_timezone'] = $prefs['server_timezone'];
5694				}
5695			} else {
5696				// ... and we fallback to the server timezone if the cookie value is not available
5697				$prefs['display_timezone'] = $prefs['server_timezone'];
5698			}
5699		}
5700	}
5701
5702	function get_long_date_format()
5703	{
5704		global $prefs;
5705		return $prefs['long_date_format'];
5706	}
5707
5708	function get_short_date_format()
5709	{
5710		global $prefs;
5711		return $prefs['short_date_format'];
5712	}
5713
5714	function get_long_time_format()
5715	{
5716		global $prefs;
5717		return $prefs['long_time_format'];
5718	}
5719
5720	function get_short_time_format()
5721	{
5722		global $prefs;
5723		return $prefs['short_time_format'];
5724	}
5725
5726	/**
5727	 * @return string
5728	 */
5729	function get_long_datetime_format()
5730	{
5731		static $long_datetime_format = false;
5732
5733		if (! $long_datetime_format) {
5734			$t = trim($this->get_long_time_format());
5735			if (! empty($t)) {
5736				$t = ' ' . $t;
5737			}
5738			$long_datetime_format = $this->get_long_date_format() . $t;
5739		}
5740
5741		return $long_datetime_format;
5742	}
5743
5744	/**
5745	 * @return string
5746	 */
5747	function get_short_datetime_format()
5748	{
5749		static $short_datetime_format = false;
5750
5751		if (! $short_datetime_format) {
5752			$t = trim($this->get_short_time_format());
5753			if (! empty($t)) {
5754				$t = ' ' . $t;
5755			}
5756			$short_datetime_format = $this->get_short_date_format() . $t;
5757		}
5758
5759		return $short_datetime_format;
5760	}
5761
5762	/**
5763	 * @param $format
5764	 * @param bool $timestamp
5765	 * @param bool $_user
5766	 * @param int $input_format
5767	 * @return string
5768	 */
5769	static function date_format2($format, $timestamp = false, $_user = false, $input_format = 5/*DATE_FORMAT_UNIXTIME*/)
5770	{
5771		return TikiLib::date_format($format, $timestamp, $_user, $input_format, false);
5772	}
5773
5774	/**
5775	 * @param $format
5776	 * @param bool $timestamp
5777	 * @param bool $_user
5778	 * @param int $input_format
5779	 * @param bool $is_strftime_format
5780	 * @return string
5781	 */
5782	static function date_format($format, $timestamp = false, $_user = false, $input_format = 5/*DATE_FORMAT_UNIXTIME*/, $is_strftime_format = true)
5783	{
5784		$tikilib = TikiLib::lib('tiki');
5785		static $currentUserDateByFormat = [];
5786
5787		if (! $timestamp) {
5788			$timestamp = $tikilib->now;
5789		}
5790
5791		if ($_user === false && $is_strftime_format && $timestamp == $tikilib->now && isset($currentUserDateByFormat[ $format . $timestamp ])) {
5792			return $currentUserDateByFormat[ $format . $timestamp ];
5793		}
5794
5795		$tikidate = TikiLib::lib('tikidate');
5796		try {
5797			$tikidate->setDate($timestamp, 'UTC');
5798		} catch (Exception $e) {
5799			return $e->getMessage();
5800		}
5801
5802		$tz = $tikilib->get_display_timezone($_user);
5803
5804		// If user timezone is not also in UTC, convert the date
5805		if ($tz != 'UTC') {
5806			$tikidate->setTZbyID($tz);
5807		}
5808
5809		$return = $tikidate->format($format, $is_strftime_format);
5810		if ($is_strftime_format) {
5811			$currentUserDateByFormat[ $format . $timestamp ] = $return;
5812		}
5813		return $return;
5814	}
5815
5816	/**
5817	 * @param $hour
5818	 * @param $minute
5819	 * @param $second
5820	 * @param $month
5821	 * @param $day
5822	 * @param $year
5823	 * @return int
5824	 */
5825	function make_time($hour, $minute, $second, $month, $day, $year)
5826	{
5827		$tikilib = TikiLib::lib('tiki');
5828		$tikidate = TikiLib::lib('tikidate');
5829		$display_tz = $tikilib->get_display_timezone();
5830		if ($display_tz == '') {
5831			$display_tz = 'UTC';
5832		}
5833		$tikidate->setTZbyID($display_tz);
5834		$tikidate->setLocalTime($day, $month, $year, $hour, $minute, $second, 0);
5835		return $tikidate->getTime();
5836	}
5837
5838	/**
5839	 * @param $timestamp
5840	 * @param bool $user
5841	 * @return string
5842	 */
5843	function get_long_date($timestamp, $user = false)
5844	{
5845		return $this->date_format($this->get_long_date_format(), $timestamp, $user);
5846	}
5847
5848	/**
5849	 * @param $timestamp
5850	 * @param bool $user
5851	 * @return string
5852	 */
5853	function get_short_date($timestamp, $user = false)
5854	{
5855		return $this->date_format($this->get_short_date_format(), (int) $timestamp, $user);
5856	}
5857
5858	/**
5859	 * @param $timestamp
5860	 * @param bool $user
5861	 * @return string
5862	 */
5863	function get_long_time($timestamp, $user = false)
5864	{
5865		return $this->date_format($this->get_long_time_format(), $timestamp, $user);
5866	}
5867
5868	/**
5869	 * @param $timestamp
5870	 * @param bool $user
5871	 * @return string
5872	 */
5873	function get_short_time($timestamp, $user = false)
5874	{
5875		return $this->date_format($this->get_short_time_format(), $timestamp, $user);
5876	}
5877
5878	/**
5879	 * @param $timestamp
5880	 * @param bool $user
5881	 * @return string
5882	 */
5883	function get_long_datetime($timestamp, $user = false)
5884	{
5885		return $this->date_format($this->get_long_datetime_format(), $timestamp, $user);
5886	}
5887
5888	/**
5889	 * @param $timestamp
5890	 * @param bool $user
5891	 * @return string
5892	 */
5893	function get_short_datetime($timestamp, $user = false)
5894	{
5895		return $this->date_format($this->get_short_datetime_format(), $timestamp, $user);
5896	}
5897
5898	/**
5899		Per http://www.w3.org/TR/NOTE-datetime
5900	 */
5901	function get_iso8601_datetime($timestamp, $user = false)
5902	{
5903		return $this->date_format('%Y-%m-%dT%H:%M:%S%O', $timestamp, $user);
5904	}
5905
5906	/**
5907	 * @param $timestamp
5908	 * @param bool $user
5909	 * @return string
5910	 */
5911	function get_compact_iso8601_datetime($timestamp, $user = false)
5912	{
5913		// no dashes and no tz info - latter should be fixed
5914		return $this->date_format('%Y%m%dT%H%M%S', $timestamp, $user);
5915	}
5916
5917	/**
5918	 * @return  array of css files in the style dir
5919	 */
5920	function list_styles()
5921	{
5922		global $tikidomain;
5923		$csslib = TikiLib::lib('css');
5924
5925		$sty = [];
5926		$style_base_path = $this->get_style_path();	// knows about $tikidomain
5927
5928		if ($style_base_path) {
5929			$sty = $csslib->list_css($style_base_path);
5930		}
5931
5932		if ($tikidomain) {
5933			$sty = array_unique(array_merge($sty, $csslib->list_css('styles')));
5934		}
5935		foreach ($sty as &$s) {
5936			if (in_array($s, ['mobile', '960_gs'])) {
5937				$s = '';
5938			} elseif (substr($s, -4) == '-rtl' || substr($s, -6) == '-print') {
5939				$s = '';
5940			} else {
5941				$s .= '.css';	// add the .css back onto the end of the style names
5942			}
5943		}
5944		$sty = array_filter($sty);
5945		sort($sty);
5946		return $sty;
5947
5948		/* What is this $tikidomain section?
5949		 * Some files that call this method used to list styles without considering
5950		 * $tikidomain, now they do. They're listed below:
5951		 *
5952		 *  tiki-theme_control.php
5953		 *  tiki-theme_control_objects.php
5954		 *  tiki-theme_control_sections.php
5955		 *  tiki-my_tiki.php
5956		 *  modules/mod-switch_theme.php
5957		 *
5958		 *  lfagundes
5959		 *
5960		 *  Tiki 3.0 - now handled by get_style_path()
5961		 *  jonnybradley
5962		 */
5963	}
5964
5965	/**
5966	 * @param $a_style - main style (e.g. "thenews.css")
5967	 * @return array of css files in the style options dir
5968	 */
5969	function list_style_options($a_style = '')
5970	{
5971		global $prefs;
5972		$csslib = TikiLib::lib('css');
5973
5974		if (empty($a_style)) {
5975			$a_style = $prefs['style'];
5976		}
5977
5978		$sty = [];
5979		$option_base_path = $this->get_style_path($a_style) . 'options/';
5980
5981		if (is_dir($option_base_path)) {
5982			$sty = $csslib->list_css($option_base_path);
5983		}
5984
5985		if (count($sty)) {
5986			foreach ($sty as &$s) {	// add .css back as above
5987				$s .= '.css';
5988			}
5989			sort($sty);
5990			return $sty;
5991		} else {
5992			return false;
5993		}
5994	}
5995
5996	/**
5997	 * @param $stl - main style (e.g. "thenews.css")
5998	 * @return string - style passed in up to - | or . char (e.g. "thenews")
5999	 */
6000	function get_style_base($stl)
6001	{
6002		$parts = preg_split('/[\-\.]/', $stl);
6003		if (count($parts) > 0) {
6004			return $parts[0];
6005		} else {
6006			return '';
6007		}
6008	}
6009
6010	/**
6011	 * @param $stl - main style (e.g. "thenews.css" - can be empty to return main styles dir)
6012	 * @param $opt - optional option file name (e.g. "purple.css")
6013	 * @param $filename - optional filename to look for (e.g. "purple.png")
6014	 * @return path to dir or file if found or empty if not - e.g. "styles/mydomain.tld/thenews/options/purple/"
6015	 */
6016	function get_style_path($stl = '', $opt = '', $filename = '')
6017	{
6018		global $tikidomain;
6019
6020		$path = '';
6021		$dbase = '';
6022		if ($tikidomain && is_dir("styles/$tikidomain")) {
6023			$dbase = $tikidomain . '/';
6024		}
6025
6026		$sbase = '';
6027		if (! empty($stl)) {
6028			$sbase = $this->get_style_base($stl) . '/';
6029		}
6030
6031		$obase = '';
6032		if (! empty($opt)) {
6033			$obase = 'options/';
6034			if ($opt != $filename) {	// exception for getting option.css as it doesn't live in it's own dir
6035				$obase .= substr($opt, 0, strlen($opt) - 4) . '/';
6036			}
6037		}
6038
6039		if (empty($filename)) {
6040			if (is_dir('styles/' . $dbase . $sbase . $obase)) {
6041				$path = 'styles/' . $dbase . $sbase . $obase;
6042			} elseif (is_dir('styles/' . $dbase . $sbase)) {
6043				$path = 'styles/' . $dbase . $sbase;	// try "parent" style dir if no option one
6044			} elseif (is_dir('styles/' . $sbase . $obase)) {
6045				$path = 'styles/' . $sbase . $obase;	// try root style dir if no domain one
6046			} else {
6047				$path = 'styles/' . $sbase;			// fall back to "parent" style dir if no option one
6048			}
6049		} else {
6050			if (is_file('styles/' . $dbase . $sbase . $obase . $filename)) {
6051				$path = 'styles/' . $dbase . $sbase . $obase . $filename;
6052			} elseif (is_file('styles/' . $dbase . $sbase . $filename)) {	// try "parent" style dir if no option one
6053				$path = 'styles/' . $dbase . $sbase . $filename;
6054			} elseif (is_file('styles/' . $sbase . $obase . $filename)) {	// try non-tikidomain dirs if not found
6055				$path = 'styles/' . $sbase . $obase . $filename;
6056			} elseif (is_file('styles/' . $sbase . $filename)) {
6057				$path = 'styles/' . $sbase . $filename;				// fall back to "parent" style dir if no option
6058			} elseif (is_file('styles/' . $dbase . $filename)) {
6059				$path = 'styles/' . $dbase . $filename;				// tikidomain root style dir?
6060			} elseif (is_file('styles/' . $dbase . $filename)) {
6061				$path = 'styles/' . $filename;					// root style dir?
6062			}
6063		}
6064
6065		return $path;
6066	}
6067
6068	/**
6069	 * @param bool $user
6070	 * @return null
6071	 */
6072	function get_language($user = false)
6073	{
6074		global $prefs;
6075		static $language = false;
6076
6077		if (! $language) {
6078			if ($user) {
6079				$language = $this->get_user_preference($user, 'language', 'default');
6080				if (! $language || $language == 'default') {
6081					$language = $prefs['language'];
6082				}
6083			} else {
6084				$language = $prefs['language'];
6085			}
6086		}
6087		return $language;
6088	}
6089
6090	/**
6091	 * @param $text
6092	 * @return string
6093	 */
6094	function read_raw($text, $preserve=false)
6095	{
6096		$file = explode("\n", $text);
6097		$back = [];
6098		// When the fieldID is not preserved, ensure uniqueness of the $var key even if the fieldID is duplicated in the input
6099		$i = 0;
6100		foreach ($file as $line) {
6101			$r = $s = '';
6102			if (substr($line, 0, 1) != "#") {
6103				if (preg_match("/^\[([A-Z0-9]+)\]/", $line, $r)) {
6104					if ($preserve) {
6105						$var = strtolower($r[1]);
6106					} else {
6107						$i++;
6108						$var = 'id' . $i . strtolower($r[1]);
6109					}
6110				}
6111				if (isset($var) and (preg_match("/^([-_\/ a-zA-Z0-9]+)[ \t]+[:=][ \t]+(.*)/", $line, $s))) {
6112					$back[$var][trim($s[1])] = trim($s[2]);
6113				}
6114			}
6115		}
6116		return $back;
6117	}
6118
6119
6120	/**
6121	 * Get URL Scheme (http / https)
6122	 * Considers the use of a reverse proxy / ssl offloader. I.e If request is https -> ssl offloader -> http tiki, then it will correctly return https
6123	 * @return string http | https
6124	 */
6125	static function httpScheme()
6126	{
6127		global $url_scheme;
6128		return $url_scheme;
6129	}
6130
6131	/**
6132	 * @param bool $isUserSpecific
6133	 * @return string
6134	 */
6135	static function httpPrefix($isUserSpecific = false)
6136	{
6137		global $url_scheme, $url_host, $url_port, $prefs;
6138
6139		if ($isUserSpecific && $prefs['https_login'] != 'disabled' && $prefs['https_external_links_for_users'] == 'y') {
6140			$scheme = 'https';
6141		} else {
6142			$scheme = $url_scheme;
6143		}
6144
6145		return $scheme . '://' . $url_host . (($url_port != '') ? ":$url_port" : '');
6146	}
6147
6148	/**
6149	 * Includes the full tiki path in the links for external link generation.
6150	 * @param string $relative
6151	 * @param array $args
6152	 * @return string
6153	 */
6154	static function tikiUrl($relative = "", $args = [])
6155	{
6156		global $tikiroot;
6157
6158		if (preg_match('/^http(s?):/', $relative)) {
6159			$base = $relative;
6160		} else {
6161			$base = self::httpPrefix() . $tikiroot . $relative;
6162		}
6163
6164		if (count($args)) {
6165			$base .= '?';
6166			$base .= http_build_query($args, '', '&');
6167		}
6168
6169		return $base;
6170	}
6171
6172	/**
6173	 * Include the full tiki path if requested in an external context.
6174	 * Otherwise, leave as-is.
6175	 *
6176	 * @param string $relative
6177	 * @param array $args
6178	 * @return string
6179	 */
6180	static function tikiUrlOpt($relative)
6181	{
6182		if (self::$isExternalContext) {
6183			return self::tikiUrl($relative);
6184		} else {
6185			return $relative;
6186		}
6187	}
6188
6189	static function setExternalContext($isExternal)
6190	{
6191		$oldValue = self::$isExternalContext;
6192
6193		self::$isExternalContext = (bool) $isExternal;
6194
6195		return $oldValue;
6196	}
6197
6198	static function contextualizeKey($key, $param1 = null, $param2 = null)
6199	{
6200		global $prefs;
6201
6202		$args = func_get_args();
6203		array_shift($args);
6204
6205		foreach ($args as $arg) {
6206			if ($arg == 'language') {
6207				$language = isset($prefs['language']) ? $prefs['language'] : 'en';
6208				$key .= "_{$language}";
6209			} elseif ($arg == 'external') {
6210				$key .= (int) self::$isExternalContext;
6211			}
6212		}
6213
6214		return $key;
6215	}
6216
6217	/**
6218	 * Removes the protocol, host and path from a URL if they match
6219	 *
6220	 * @param string $url		URL to be converted
6221	 * @return string			relative URL if possible
6222	 */
6223	static function makeAbsoluteLinkRelative($url)
6224	{
6225		global $base_url;
6226
6227		if (strpos($url, $base_url) !== false) {
6228			$out = substr($url, strlen($base_url));
6229		} else {
6230			$out = $url;
6231		}
6232		return $out;
6233	}
6234
6235	/**
6236	 * @param $lat1
6237	 * @param $lon1
6238	 * @param $lat2
6239	 * @param $lon2
6240	 * @return int
6241	 */
6242	function distance($lat1, $lon1, $lat2, $lon2)
6243	{
6244		// This function uses a pure spherical model
6245		// it could be improved to use the WGS84 Datum
6246		// Franck Martin
6247		$lat1rad = deg2rad($lat1);
6248		$lon1rad = deg2rad($lon1);
6249		$lat2rad = deg2rad($lat2);
6250		$lon2rad = deg2rad($lon2);
6251		$distance = 6367 * acos(sin($lat1rad) * sin($lat2rad) + cos($lat1rad) * cos($lat2rad) * cos($lon1rad - $lon2rad));
6252		return($distance);
6253	}
6254
6255	/**
6256	 * returns a list of usergroups where the user is a member and the group has the right perm
6257	 * sir-b
6258	 **/
6259	function get_groups_to_user_with_permissions($user, $perm)
6260	{
6261		$userid = $this->get_user_id($user);
6262		$query = "SELECT DISTINCT `users_usergroups`.`groupName` AS `groupName`";
6263		$query .= "FROM  `users_grouppermissions`, `users_usergroups` ";
6264		$query .= "WHERE `users_usergroups`.`userId` = ? AND ";
6265		$query .= "`users_grouppermissions`.`groupName` = `users_usergroups`.`groupName` AND ";
6266		$query .= "`users_grouppermissions`.`permName` = ? ";
6267		$query .= "ORDER BY `groupName`";
6268		return $this->fetchAll($query, [(int)$userid, $perm]);
6269	}
6270
6271	/**
6272	 * @param $tab
6273	 * @param $valField1
6274	 * @param $field1
6275	 * @param $field2
6276	 * @return mixed
6277	 */
6278	function other_value_in_tab_line($tab, $valField1, $field1, $field2)
6279	{
6280		foreach ($tab as $line) {
6281			if ($line[$field1] == $valField1) {
6282				return $line[$field2];
6283			}
6284		}
6285	}
6286
6287	/**
6288	 * @param $file_name
6289	 * @return string
6290	 */
6291	function get_attach_hash_file_name($file_name)
6292	{
6293		global $prefs;
6294		do {
6295			$fhash = md5($file_name . date('U') . rand());
6296		} while (file_exists($prefs['w_use_dir'] . $fhash));
6297		return $fhash;
6298	}
6299
6300	/**
6301	 * @param $file_name
6302	 * @param $file_tmp_name
6303	 * @param $store_type
6304	 * @return array
6305	 */
6306	function attach_file($file_name, $file_tmp_name, $store_type)
6307	{
6308		global $prefs;
6309		$tmp_dest = $prefs['tmpDir'] . "/" . $file_name . ".tmp";
6310		if (! move_uploaded_file($file_tmp_name, $tmp_dest)) {
6311			return ["ok" => false, "error" => tra('Errors detected')];
6312		}
6313		try {
6314			$filegallib = TikiLib::lib('filegal');
6315			$filegallib->assertUploadedFileIsSafe($tmp_dest, $file_name);
6316		} catch (Exception $e) {
6317			return ['ok' => false, 'error' => $e->getMessage()];
6318		}
6319		$fp = fopen($tmp_dest, "rb");
6320		$data = '';
6321		$fhash = '';
6322		$chunk = '';
6323		if ($store_type == 'dir') {
6324			$fhash = $this->get_attach_hash_file_name($file_name);
6325			$fw = fopen($prefs['w_use_dir'] . $fhash, "wb");
6326			if (! $fw) {
6327				return ["ok" => false, "error" => tra('Cannot write to this file:') . $prefs['w_use_dir'] . $fhash];
6328			}
6329		}
6330		while (! feof($fp)) {
6331			$chunk = fread($fp, 8192 * 16);
6332
6333			if ($store_type == 'dir') {
6334				fwrite($fw, $chunk);
6335			}
6336			$data .= $chunk;
6337		}
6338		fclose($fp);
6339		unlink($tmp_dest);
6340		if ($store_type == 'dir') {
6341			fclose($fw);
6342			$data = "";
6343		}
6344		return ["ok" => true, "data" => $data, "fhash" => $fhash];
6345	}
6346
6347	/* to get the length of a data without the quoted part (very
6348		 approximative)  */
6349	/**
6350	 * @param $data
6351	 * @return int
6352	 */
6353	function strlen_quoted($data)
6354	{
6355		global $prefs;
6356		if ($prefs['feature_use_quoteplugin'] != 'y') {
6357			$data = preg_replace('/^>.*\\n?/m', '', $data);
6358		} else {
6359			$data = preg_replace('/{QUOTE\([^\)]*\)}.*{QUOTE}/Ui', '', $data);
6360		}
6361		return strlen($data);
6362	}
6363
6364	/**
6365	 * @param $id
6366	 * @param int $offset
6367	 * @param $maxRecords
6368	 * @param string $sort_mode
6369	 * @param string $find
6370	 * @param string $table
6371	 * @param string $column
6372	 * @param string $from
6373	 * @param string $to
6374	 * @return array
6375	 */
6376	function list_votes($id, $offset = 0, $maxRecords = -1, $sort_mode = 'user_asc', $find = '', $table = '', $column = '', $from = '', $to = '')
6377	{
6378		$mid = 'where  `id`=?';
6379		$bindvars[] = $id;
6380		$select = '';
6381		$join = '';
6382		if (! empty($find)) {
6383			$mid .= ' and (`user` like ? or `title` like ? or `ip` like ?)';
6384			$bindvars[] = '%' . $find . '%';
6385			$bindvars[] = '%' . $find . '%';
6386			$bindvars[] = '%' . $find . '%';
6387		}
6388		if (! empty($from) && ! empty($to)) {
6389			$mid .= ' and ((time >= ? and time <= ?) or time = ?)';
6390			$bindvars[] = $from;
6391			$bindvars[] = $to;
6392			$bindvars[] = 0;
6393		}
6394		if (! empty($table) && ! empty($column)) {
6395			$select = ", `$table`.`$column` as title";
6396			$join = "left join `$table` on (`tiki_user_votings`.`optionId` = `$table`.`optionId`)";
6397		}
6398		$query = "select * $select from `tiki_user_votings` $join $mid order by " . $this->convertSortMode($sort_mode);
6399		$query_cant = "select count(*) from `tiki_user_votings` $join $mid";
6400		$ret = $this->fetchAll($query, $bindvars, $maxRecords, $offset);
6401		$cant = $this->getOne($query_cant, $bindvars);
6402		$retval = [];
6403		$retval["data"] = $ret;
6404		$retval["cant"] = $cant;
6405		return $retval;
6406	}
6407
6408	/**
6409	  *  Returns explicit message on upload problem
6410	  *
6411	  *	@params: $iError: php status of the file uploading (documented in http://uk2.php.net/manual/en/features.file-upload.errors.php )
6412	  *
6413	  */
6414	function uploaded_file_error($iError)
6415	{
6416		switch ($iError) {
6417			case UPLOAD_ERR_OK:
6418				return tra('The file was successfully uploaded.');
6419			case UPLOAD_ERR_INI_SIZE:
6420				return tra('The uploaded file exceeds the upload_max_filesize directive in php.ini.');
6421			case UPLOAD_ERR_FORM_SIZE:
6422				return tra('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.');
6423			case UPLOAD_ERR_PARTIAL:
6424				return tra('The file was only partially uploaded.');
6425			case UPLOAD_ERR_NO_FILE:
6426				return tra('No file was uploaded. Was a file selected ?');
6427			case UPLOAD_ERR_NO_TMP_DIR:
6428				return tra('A temporary folder is missing.');
6429			case UPLOAD_ERR_CANT_WRITE:
6430				return tra('Failed to write file to disk.');
6431			case UPLOAD_ERR_EXTENSION:
6432				return tra('File upload stopped by extension.');
6433
6434			default:
6435				return tra('Unknown error.');
6436		}
6437	}
6438
6439	// from PHP manual (ini-get function example)
6440	/**
6441	 * @param string $val		php.ini key returning memory string i.e. 32M
6442	 * @return int				size in bytes
6443	 */
6444	function return_bytes($val)
6445	{
6446		$val = trim($val);
6447		$bytes = (int) $val;
6448		$lastCharacter = strtolower($val{strlen($val) - 1});
6449		$units = ['k' => 1, 'm' => 2, 'g' => 3];
6450		if (array_key_exists($lastCharacter, $units)) {
6451			$bytes = $bytes * (1024 ** $units[$lastCharacter]);
6452		}
6453		return $bytes;
6454	}
6455
6456	/**
6457	 * @return int	bytes of memory available for PHP
6458	 */
6459	function get_memory_avail()
6460	{
6461		return $this->get_memory_limit() - memory_get_usage(true);
6462	}
6463
6464	/**
6465	 * @return int
6466	 */
6467	function get_memory_limit()
6468	{
6469		return $this->return_bytes(ini_get('memory_limit'));
6470	}
6471
6472	/**
6473	 * @param bool $with_names
6474	 * @param bool $translate
6475	 * @param bool $sort_names
6476	 * @return array|mixed
6477	 */
6478	function get_flags($with_names = false, $translate = false, $sort_names = false, $langsort = false)
6479	{
6480		global $prefs;
6481
6482		$cachelib = TikiLib::lib('cache');
6483		$args = func_get_args();
6484		$cacheKey = serialize($args) . $prefs['language'];
6485
6486		if ($data = $cachelib->getSerialized($cacheKey, 'flags')) {
6487			return $data;
6488		}
6489
6490		$flags = [];
6491		$h = opendir("img/flags/");
6492		while ($file = readdir($h)) {
6493			if (strstr($file, ".png")) {
6494				$parts = explode('.', $file);
6495				$flags[] = $parts[0];
6496			}
6497		}
6498		closedir($h);
6499		if ($langsort) {
6500			foreach ($flags as $flagname => $flagtra) {
6501				unset($flags[$flagname]);
6502				$flags[$flagtra] = tra($flagtra);
6503			}
6504		}
6505		natcasesort($flags);
6506
6507		if ($with_names) {
6508			$ret = [];
6509			$names = [];
6510			foreach ($flags as $f) {
6511				$ret[$f] = strtr($f, '_', ' ');
6512				if ($translate) {
6513					$ret[$f] = tra($ret[$f]);
6514				}
6515				if ($sort_names) {
6516					$names[$f] = strtolower($this->take_away_accent($ret[$f]));
6517				}
6518			}
6519			if ($sort_names) {
6520				array_multisort($names, $ret);
6521			}
6522
6523			$flags = $ret;
6524		}
6525
6526		$cachelib->cacheItem($cacheKey, serialize($flags), 'flags');
6527
6528		return $flags;
6529	}
6530
6531
6532	/**
6533	 * @param {string} $data
6534	 * @return string
6535	 */
6536	function strip_tags($data)
6537	{
6538		$result = preg_replace('/[<]style[^>]*?[>](.|\n|\r)*?[<][\/]style[>]/', '', $data);
6539		$result = strip_tags($result);
6540		return $result;
6541	}
6542	/**
6543	 * @param $data
6544	 * @param string $outputType
6545	 * @param boolean $is_html
6546	 * @param string $highlight
6547	 * @param int $length
6548	 * @param string $start
6549	 * @param string $end
6550	 * @return string
6551	 */
6552	function get_snippet($data, $outputType = '', $is_html = false, $highlight = '', $length = 240, $start = '', $end = '')
6553	{
6554		global $prefs;
6555		if ($prefs['search_parsed_snippet'] == 'y') {
6556			$data = preg_replace('/{(:?make)?toc[^}]*}/', '', $data);
6557
6558			$_REQUEST['redirectpage'] = 'y'; //do not interpret redirect
6559			$data = TikiLib::lib('parser')->parse_data($data, ['is_html' => $is_html, 'stripplugins' => true, 'parsetoc' => true]);
6560		}
6561
6562
6563		$data = strip_tags($data);
6564		if ($length > 0) {
6565			if (function_exists('mb_substr')) {
6566				return mb_substr($data, 0, $length);
6567			} else {
6568				return substr($data, 0, $length);
6569			}
6570		}
6571		if (! empty($start) && ($i = strpos($data, $start))) {
6572			$data = substr($data, $i + strlen($start));
6573		}
6574		if (! empty($end) && ($i = strpos($data, $end))) {
6575			$data = substr($data, 0, $i);
6576		}
6577		return $data;
6578	}
6579
6580	/**
6581	 * @param $string
6582	 * @param int $quote_style
6583	 * @param int $translation_table
6584	 * @return string
6585	 */
6586	static function htmldecode($string, $quote_style = ENT_COMPAT, $translation_table = HTML_ENTITIES)
6587	{
6588		if ($translation_table == HTML_ENTITIES) {
6589			$string = html_entity_decode($string, $quote_style, 'utf-8');
6590		} elseif ($translation_table === HTML_SPECIALCHARS) {
6591			$string = htmlspecialchars_decode($string, $quote_style);
6592		}
6593
6594		return $string;
6595	}
6596
6597	/**
6598	 * * Unaccent the input string string. An example string like `ÀØėÿᾜὨζὅБю`
6599	 * will be translated to `AOeyIOzoBY`
6600	 * @param $str
6601	 * @return string unaccented string
6602	 */
6603	static function take_away_accent($str)
6604	{
6605		$transliteration = array(
6606			'IJ' => 'I', 'Ö' => 'O','Œ' => 'O','Ü' => 'U','ä' => 'a','æ' => 'a',
6607			'ij' => 'i','ö' => 'o','œ' => 'o','ü' => 'u','ß' => 's','ſ' => 's',
6608			'À' => 'A','Á' => 'A','Â' => 'A','Ã' => 'A','Ä' => 'A','Å' => 'A',
6609			'Æ' => 'A','Ā' => 'A','Ą' => 'A','Ă' => 'A','Ç' => 'C','Ć' => 'C',
6610			'Č' => 'C','Ĉ' => 'C','Ċ' => 'C','Ď' => 'D','Đ' => 'D','È' => 'E',
6611			'É' => 'E','Ê' => 'E','Ë' => 'E','Ē' => 'E','Ę' => 'E','Ě' => 'E',
6612			'Ĕ' => 'E','Ė' => 'E','Ĝ' => 'G','Ğ' => 'G','Ġ' => 'G','Ģ' => 'G',
6613			'Ĥ' => 'H','Ħ' => 'H','Ì' => 'I','Í' => 'I','Î' => 'I','Ï' => 'I',
6614			'Ī' => 'I','Ĩ' => 'I','Ĭ' => 'I','Į' => 'I','İ' => 'I','Ĵ' => 'J',
6615			'Ķ' => 'K','Ľ' => 'K','Ĺ' => 'K','Ļ' => 'K','Ŀ' => 'K','Ł' => 'L',
6616			'Ñ' => 'N','Ń' => 'N','Ň' => 'N','Ņ' => 'N','Ŋ' => 'N','Ò' => 'O',
6617			'Ó' => 'O','Ô' => 'O','Õ' => 'O','Ø' => 'O','Ō' => 'O','Ő' => 'O',
6618			'Ŏ' => 'O','Ŕ' => 'R','Ř' => 'R','Ŗ' => 'R','Ś' => 'S','Ş' => 'S',
6619			'Ŝ' => 'S','Ș' => 'S','Š' => 'S','Ť' => 'T','Ţ' => 'T','Ŧ' => 'T',
6620			'Ț' => 'T','Ù' => 'U','Ú' => 'U','Û' => 'U','Ū' => 'U','Ů' => 'U',
6621			'Ű' => 'U','Ŭ' => 'U','Ũ' => 'U','Ų' => 'U','Ŵ' => 'W','Ŷ' => 'Y',
6622			'Ÿ' => 'Y','Ý' => 'Y','Ź' => 'Z','Ż' => 'Z','Ž' => 'Z','à' => 'a',
6623			'á' => 'a','â' => 'a','ã' => 'a','ā' => 'a','ą' => 'a','ă' => 'a',
6624			'å' => 'a','ç' => 'c','ć' => 'c','č' => 'c','ĉ' => 'c','ċ' => 'c',
6625			'ď' => 'd','đ' => 'd','è' => 'e','é' => 'e','ê' => 'e','ë' => 'e',
6626			'ē' => 'e','ę' => 'e','ě' => 'e','ĕ' => 'e','ė' => 'e','ƒ' => 'f',
6627			'ĝ' => 'g','ğ' => 'g','ġ' => 'g','ģ' => 'g','ĥ' => 'h','ħ' => 'h',
6628			'ì' => 'i','í' => 'i','î' => 'i','ï' => 'i','ī' => 'i','ĩ' => 'i',
6629			'ĭ' => 'i','į' => 'i','ı' => 'i','ĵ' => 'j','ķ' => 'k','ĸ' => 'k',
6630			'ł' => 'l','ľ' => 'l','ĺ' => 'l','ļ' => 'l','ŀ' => 'l','ñ' => 'n',
6631			'ń' => 'n','ň' => 'n','ņ' => 'n','ʼn' => 'n','ŋ' => 'n','ò' => 'o',
6632			'ó' => 'o','ô' => 'o','õ' => 'o','ø' => 'o','ō' => 'o','ő' => 'o',
6633			'ŏ' => 'o','ŕ' => 'r','ř' => 'r','ŗ' => 'r','ś' => 's','š' => 's',
6634			'ť' => 't','ù' => 'u','ú' => 'u','û' => 'u','ū' => 'u','ů' => 'u',
6635			'ű' => 'u','ŭ' => 'u','ũ' => 'u','ų' => 'u','ŵ' => 'w','ÿ' => 'y',
6636			'ý' => 'y','ŷ' => 'y','ż' => 'z','ź' => 'z','ž' => 'z','Α' => 'A',
6637			'Ά' => 'A','Ἀ' => 'A','Ἁ' => 'A','Ἂ' => 'A','Ἃ' => 'A','Ἄ' => 'A',
6638			'Ἅ' => 'A','Ἆ' => 'A','Ἇ' => 'A','ᾈ' => 'A','ᾉ' => 'A','ᾊ' => 'A',
6639			'ᾋ' => 'A','ᾌ' => 'A','ᾍ' => 'A','ᾎ' => 'A','ᾏ' => 'A','Ᾰ' => 'A',
6640			'Ᾱ' => 'A','Ὰ' => 'A','ᾼ' => 'A','Β' => 'B','Γ' => 'G','Δ' => 'D',
6641			'Ε' => 'E','Έ' => 'E','Ἐ' => 'E','Ἑ' => 'E','Ἒ' => 'E','Ἓ' => 'E',
6642			'Ἔ' => 'E','Ἕ' => 'E','Ὲ' => 'E','Ζ' => 'Z','Η' => 'I','Ή' => 'I',
6643			'Ἠ' => 'I','Ἡ' => 'I','Ἢ' => 'I','Ἣ' => 'I','Ἤ' => 'I','Ἥ' => 'I',
6644			'Ἦ' => 'I','Ἧ' => 'I','ᾘ' => 'I','ᾙ' => 'I','ᾚ' => 'I','ᾛ' => 'I',
6645			'ᾜ' => 'I','ᾝ' => 'I','ᾞ' => 'I','ᾟ' => 'I','Ὴ' => 'I','ῌ' => 'I',
6646			'Θ' => 'T','Ι' => 'I','Ί' => 'I','Ϊ' => 'I','Ἰ' => 'I','Ἱ' => 'I',
6647			'Ἲ' => 'I','Ἳ' => 'I','Ἴ' => 'I','Ἵ' => 'I','Ἶ' => 'I','Ἷ' => 'I',
6648			'Ῐ' => 'I','Ῑ' => 'I','Ὶ' => 'I','Κ' => 'K','Λ' => 'L','Μ' => 'M',
6649			'Ν' => 'N','Ξ' => 'K','Ο' => 'O','Ό' => 'O','Ὀ' => 'O','Ὁ' => 'O',
6650			'Ὂ' => 'O','Ὃ' => 'O','Ὄ' => 'O','Ὅ' => 'O','Ὸ' => 'O','Π' => 'P',
6651			'Ρ' => 'R','Ῥ' => 'R','Σ' => 'S','Τ' => 'T','Υ' => 'Y','Ύ' => 'Y',
6652			'Ϋ' => 'Y','Ὑ' => 'Y','Ὓ' => 'Y','Ὕ' => 'Y','Ὗ' => 'Y','Ῠ' => 'Y',
6653			'Ῡ' => 'Y','Ὺ' => 'Y','Φ' => 'F','Χ' => 'X','Ψ' => 'P','Ω' => 'O',
6654			'Ώ' => 'O','Ὠ' => 'O','Ὡ' => 'O','Ὢ' => 'O','Ὣ' => 'O','Ὤ' => 'O',
6655			'Ὥ' => 'O','Ὦ' => 'O','Ὧ' => 'O','ᾨ' => 'O','ᾩ' => 'O','ᾪ' => 'O',
6656			'ᾫ' => 'O','ᾬ' => 'O','ᾭ' => 'O','ᾮ' => 'O','ᾯ' => 'O','Ὼ' => 'O',
6657			'ῼ' => 'O','α' => 'a','ά' => 'a','ἀ' => 'a','ἁ' => 'a','ἂ' => 'a',
6658			'ἃ' => 'a','ἄ' => 'a','ἅ' => 'a','ἆ' => 'a','ἇ' => 'a','ᾀ' => 'a',
6659			'ᾁ' => 'a','ᾂ' => 'a','ᾃ' => 'a','ᾄ' => 'a','ᾅ' => 'a','ᾆ' => 'a',
6660			'ᾇ' => 'a','ὰ' => 'a','ᾰ' => 'a','ᾱ' => 'a','ᾲ' => 'a','ᾳ' => 'a',
6661			'ᾴ' => 'a','ᾶ' => 'a','ᾷ' => 'a','β' => 'b','γ' => 'g','δ' => 'd',
6662			'ε' => 'e','έ' => 'e','ἐ' => 'e','ἑ' => 'e','ἒ' => 'e','ἓ' => 'e',
6663			'ἔ' => 'e','ἕ' => 'e','ὲ' => 'e','ζ' => 'z','η' => 'i','ή' => 'i',
6664			'ἠ' => 'i','ἡ' => 'i','ἢ' => 'i','ἣ' => 'i','ἤ' => 'i','ἥ' => 'i',
6665			'ἦ' => 'i','ἧ' => 'i','ᾐ' => 'i','ᾑ' => 'i','ᾒ' => 'i','ᾓ' => 'i',
6666			'ᾔ' => 'i','ᾕ' => 'i','ᾖ' => 'i','ᾗ' => 'i','ὴ' => 'i','ῂ' => 'i',
6667			'ῃ' => 'i','ῄ' => 'i','ῆ' => 'i','ῇ' => 'i','θ' => 't','ι' => 'i',
6668			'ί' => 'i','ϊ' => 'i','ΐ' => 'i','ἰ' => 'i','ἱ' => 'i','ἲ' => 'i',
6669			'ἳ' => 'i','ἴ' => 'i','ἵ' => 'i','ἶ' => 'i','ἷ' => 'i','ὶ' => 'i',
6670			'ῐ' => 'i','ῑ' => 'i','ῒ' => 'i','ῖ' => 'i','ῗ' => 'i','κ' => 'k',
6671			'λ' => 'l','μ' => 'm','ν' => 'n','ξ' => 'k','ο' => 'o','ό' => 'o',
6672			'ὀ' => 'o','ὁ' => 'o','ὂ' => 'o','ὃ' => 'o','ὄ' => 'o','ὅ' => 'o',
6673			'ὸ' => 'o','π' => 'p','ρ' => 'r','ῤ' => 'r','ῥ' => 'r','σ' => 's',
6674			'ς' => 's','τ' => 't','υ' => 'y','ύ' => 'y','ϋ' => 'y','ΰ' => 'y',
6675			'ὐ' => 'y','ὑ' => 'y','ὒ' => 'y','ὓ' => 'y','ὔ' => 'y','ὕ' => 'y',
6676			'ὖ' => 'y','ὗ' => 'y','ὺ' => 'y','ῠ' => 'y','ῡ' => 'y','ῢ' => 'y',
6677			'ῦ' => 'y','ῧ' => 'y','φ' => 'f','χ' => 'x','ψ' => 'p','ω' => 'o',
6678			'ώ' => 'o','ὠ' => 'o','ὡ' => 'o','ὢ' => 'o','ὣ' => 'o','ὤ' => 'o',
6679			'ὥ' => 'o','ὦ' => 'o','ὧ' => 'o','ᾠ' => 'o','ᾡ' => 'o','ᾢ' => 'o',
6680			'ᾣ' => 'o','ᾤ' => 'o','ᾥ' => 'o','ᾦ' => 'o','ᾧ' => 'o','ὼ' => 'o',
6681			'ῲ' => 'o','ῳ' => 'o','ῴ' => 'o','ῶ' => 'o','ῷ' => 'o','А' => 'A',
6682			'Б' => 'B','В' => 'V','Г' => 'G','Д' => 'D','Е' => 'E','Ё' => 'E',
6683			'Ж' => 'Z','З' => 'Z','И' => 'I','Й' => 'I','К' => 'K','Л' => 'L',
6684			'М' => 'M','Н' => 'N','О' => 'O','П' => 'P','Р' => 'R','С' => 'S',
6685			'Т' => 'T','У' => 'U','Ф' => 'F','Х' => 'K','Ц' => 'T','Ч' => 'C',
6686			'Ш' => 'S','Щ' => 'S','Ы' => 'Y','Э' => 'E','Ю' => 'Y','Я' => 'Y',
6687			'а' => 'A','б' => 'B','в' => 'V','г' => 'G','д' => 'D','е' => 'E',
6688			'ё' => 'E','ж' => 'Z','з' => 'Z','и' => 'I','й' => 'I','к' => 'K',
6689			'л' => 'L','м' => 'M','н' => 'N','о' => 'O','п' => 'P','р' => 'R',
6690			'с' => 'S','т' => 'T','у' => 'U','ф' => 'F','х' => 'K','ц' => 'T',
6691			'ч' => 'C','ш' => 'S','щ' => 'S','ы' => 'Y','э' => 'E','ю' => 'Y',
6692			'я' => 'Y','ð' => 'd','Ð' => 'D','þ' => 't','Þ' => 'T','ა' => 'a',
6693			'ბ' => 'b','გ' => 'g','დ' => 'd','ე' => 'e','ვ' => 'v','ზ' => 'z',
6694			'თ' => 't','ი' => 'i','კ' => 'k','ლ' => 'l','მ' => 'm','ნ' => 'n',
6695			'ო' => 'o','პ' => 'p','ჟ' => 'z','რ' => 'r','ს' => 's','ტ' => 't',
6696			'უ' => 'u','ფ' => 'p','ქ' => 'k','ღ' => 'g','ყ' => 'q','შ' => 's',
6697			'ჩ' => 'c','ც' => 't','ძ' => 'd','წ' => 't','ჭ' => 'c','ხ' => 'k',
6698			'ჯ' => 'j','ჰ' => 'h'
6699			);
6700		$str = str_replace( array_keys( $transliteration ),array_values( $transliteration ),$str);
6701		return $str;
6702	}
6703
6704	/**
6705	 * @param $str
6706	 * @return mixed
6707	 */
6708	static function substituteSeparators($str)
6709	{
6710		$subst = explode(' ', '+ \' : ;');
6711		$convs = explode(' ', '_ _ _ _');
6712		$ret = str_replace($subst, $convs, $str);
6713		$ret = str_replace(' ', '_', $ret);
6714		return $ret;
6715	}
6716
6717	/**
6718	 * @param $str
6719	 * @return mixed
6720	 */
6721	function urlencode_accent($str)
6722	{
6723		$convs = [];
6724		preg_match_all('/[\x80-\xFF| ]/', $str, $matches);
6725		$accents = $matches[0];
6726		foreach ($accents as $a) {
6727			$convs[] = rawurlencode($a);
6728		}
6729		return str_replace($accents, $convs, $str);
6730	}
6731
6732	/**
6733	 * Remove all "non-word" characters and accents from a string
6734	 * Can be used for DOM elements and preferences etc
6735	 *
6736	 * @static
6737	 * @param string $str
6738	 * @return string cleaned
6739	 */
6740
6741	static function remove_non_word_characters_and_accents($str)
6742	{
6743		return preg_replace('/\W+/', '_', TikiLib::take_away_accent($str));
6744	}
6745
6746	/* return the positions in data where the hdr-nth header is find
6747	 */
6748	/**
6749	 * @param $data
6750	 * @param $hdr
6751	 * @return array
6752	 */
6753	function get_wiki_section($data, $hdr)
6754	{
6755		$start = 0;
6756		$end = strlen($data);
6757		$lines = explode("\n", $data);
6758		$header = 0;
6759		$pp_level = 0;
6760		$np_level = 0;
6761		for ($i = 0, $count_lines = count($lines); $i < $count_lines; ++$i) {
6762			$pp_level += preg_match('/~pp~/', $lines[$i]);
6763			$pp_level -= preg_match('/~\/pp~/', $lines[$i]);
6764			$np_level += preg_match('/~np~/', $lines[$i]);
6765			$np_level -= preg_match('/~\/np~/', $lines[$i]);
6766			// We test if we are inside nonparsed or pre section to ignore !*
6767			if ($pp_level % 2 == 0 and $np_level % 2 == 0) {
6768				if (substr($lines[$i], 0, 1) == '!') {
6769					++$header;
6770					if ($header == $hdr) { // we are on it - now find the next header at same or lower level
6771						$level = $this->how_many_at_start($lines[$i], '!');
6772						$end = strlen($lines[$i]) + 1;
6773						for (++$i; $i < $count_lines; ++$i) {
6774							if (substr($lines[$i], 0, 1) == '!' && $level >= $this->how_many_at_start($lines[$i], '!')) {
6775								return ([$start, $end]);
6776							}
6777							$end += strlen($lines[$i]) + 1;
6778						}
6779						break;
6780					}
6781				}
6782			}
6783			$start += strlen($lines[$i]) + 1;
6784		}
6785		return ([$start, $end]);
6786	}
6787
6788	/**
6789	 * \brief Function to embed a flash object (using JS method by default when JS in user's browser is detected)
6790	 *
6791	 * So far it's being called from wikiplugin_flash.php and tiki-edit_banner.php
6792	 *
6793	 * @param javascript = y or n to force to generate a version with javascript or not, ='' user prefs
6794	 */
6795	function embed_flash($params, $javascript = '', $flashvars = false)
6796	{
6797		global $prefs;
6798		$headerlib = TikiLib::lib('header');
6799		if (! isset($params['movie'])) {
6800			return false;
6801		}
6802		$defaults = [
6803						  'width' => 425,
6804						  'height' => 350,
6805						  'quality' => 'high',
6806						  'version' => '9.0.0',
6807						  'wmode' => 'transparent',
6808						  ];
6809		$params = array_merge($defaults, $params);
6810		if (preg_match('/^(\/|https?:)/', $params['movie'])) {
6811			$params['allowscriptaccess'] = 'always';
6812		}
6813
6814		if (((empty($javascript) && $prefs['javascript_enabled'] == 'y') || $javascript == 'y')) {
6815			$myId = (! empty($params['id'])) ? ($params['id']) : 'wp-flash-' . uniqid();
6816			$movie = '"' . $params['movie'] . '"';
6817			$div = json_encode($myId);
6818			$width = (int) $params['width'];
6819			$height = (int) $params['height'];
6820			$version = json_encode($params['version']);
6821			if (! empty($params['altimg'])) {
6822				$alt = '<img src="' . $params['altimg'] . '" width="' . $width . '" height="' . $height . '" alt=\"\" />';
6823			} else {
6824				$alt = ''; // Must be blank otherwise for a split second before Flash loads you can see any text that is set
6825			}
6826			unset($params['movie'], $params['width'], $params['height'], $params['version'], $params['altimg']);
6827			$params = json_encode($params);
6828
6829			if (! $flashvars) {
6830				$flashvars = '{}';
6831			} else {
6832				$flashvars = json_encode($flashvars);
6833				$flashvars = str_replace('\\/', '/', $flashvars);
6834			}
6835			$js = <<<JS
6836swfobject.embedSWF( $movie, $div, $width, $height, $version, 'vendor_bundled/vendor/bower-asset/swfobject/swfobject/expressInstall.swf', $flashvars, $params, {} );
6837JS;
6838			$headerlib->add_js($js);
6839			return "<div id=\"$myId\">" . $alt . "</div>";
6840		} else { // link on the movie will not work with IE6
6841			$asetup = "<object classid=\"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000\" codebase=\"http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,29,0\" width=\"{$params['width']}\" height=\"{$params['height']}\">";
6842			$asetup .= "<param name=\"movie\" value=\"{$params['movie']}\" />";
6843			$asetup .= "<param name=\"quality\" value=\"{$params['quality']}\" />";
6844			$asetup .= "<param name=\"wmode\" value=\"transparent\" />";
6845			if (! empty($params['allowscriptaccess'])) {
6846				$asetup .= "<param name=\"allowscriptaccess\" value=\"always\" />";
6847			}
6848			if (! empty($params['allowFullScreen'])) {
6849				$asetup .= '<param name="allowFullScreen" value="' . $params['allowFullScreen'] . '"></param>';
6850			}
6851			if (! empty($params['altimg'])) {
6852				$asetup .= '<img src="' . $params['altimg'] . '" width="' . $params['width'] . '" height="' . $params['height'] . '" alt=\"\" />';
6853			}
6854			$asetup .= "<embed src=\"{$params['movie']}\" quality=\"{$params['quality']}\" pluginspage=\"http://www.macromedia.com/go/getflashplayer\"" .
6855				" type=\"application/x-shockwave-flash\" width=\"{$params['width']}\" height=\"{$params['height']}\" wmode=\"transparent\"></embed></object>";
6856			return $asetup;
6857		}
6858	}
6859
6860	/**
6861	 * @param bool $descendants The default is to get all descendents of the jailed categories, but for unified search
6862	 *                          we only need the "root" jailed categories as the search does a deep_categories search on them
6863	 * @return array
6864	 */
6865	function get_jail($descendants = true)
6866	{
6867		global $prefs;
6868		// if jail is zero, we should allow non-categorized objects to be seen as well, i.e. consider as no jail
6869		if (! empty($prefs['feature_categories']) &&  $prefs['feature_categories'] == 'y' &&
6870				! empty($prefs['category_jail']) && $prefs['category_jail'] != [0 => 0] ) {
6871			$expanded = [];
6872			if ($descendants) {
6873				$categlib = TikiLib::lib('categ');
6874				foreach ($prefs['category_jail'] as $categId) {
6875					$expanded = array_merge($expanded, $categlib->get_category_descendants($categId));
6876				}
6877			} else {
6878				$expanded = $prefs['category_jail'];
6879			}
6880			return $expanded;
6881		} else {
6882			return [];
6883		}
6884	}
6885
6886	/**
6887	 * @param $type
6888	 * @param $old
6889	 * @param $new
6890	 */
6891	protected function rename_object($type, $old, $new, $user = '')
6892	{
6893		global $prefs;
6894
6895		// comments
6896		$this->table('tiki_comments')->updateMultiple(['object' => $new], ['object' => $old, 'objectType' => $type]);
6897
6898		// Move email notifications
6899		$oldId = str_replace($type, ' ', '') . $old;
6900		$newId = str_replace($type, ' ', '') . $new;
6901		$this->table('tiki_user_watches')->updateMultiple(['object' => $newId], ['object' => $oldId]);
6902		$this->table('tiki_group_watches')->updateMultiple(['object' => $newId], ['object' => $oldId]);
6903
6904		// theme_control_objects(objId,name)
6905		$oldId = md5($type . $old);
6906		$newId = md5($type . $new);
6907		$this->table('tiki_theme_control_objects')->updateMultiple(['objId' => $newId, 'name' => $new], ['objId' => $oldId]);
6908
6909		// polls
6910		if ($prefs['feature_polls'] == 'y') {
6911			$query = "update `tiki_polls` tp inner join `tiki_poll_objects` tpo on tp.`pollId` = tpo.`pollId` inner join `tiki_objects` tob on tpo.`catObjectId` = tob.`objectId` set tp.`title`=? where tp.`title`=? and tob.`type` = ?";
6912			$this->query($query, [ $new, $old, $type ]);
6913		}
6914
6915		// Move custom permissions
6916		$oldId = md5($type . TikiLib::strtolower($old));
6917		$newId = md5($type . TikiLib::strtolower($new));
6918		$this->table('users_objectpermissions')->updateMultiple(['objectId' => $newId], ['objectId' => $oldId, 'objectType' => $type]);
6919
6920		// Logs
6921		if ($prefs['feature_actionlog'] == 'y') {
6922			$logslib = TikiLib::lib('logs');
6923			$logslib->add_action('Renamed', $new, 'wiki page', 'old=' . $old . '&new=' . $new, $user, '', '', '', '', [['rename' => $old]]);
6924			$logslib->rename($type, $old, $new);
6925		}
6926
6927		// Attributes
6928		$this->table('tiki_object_attributes')->updateMultiple(['itemId' => $new], ['itemId' => $old, 'type' => $type]);
6929		$this->table('tiki_object_relations')->updateMultiple(['source_itemId' => $new], ['source_itemId' => $old, 'source_type' => $type]);
6930		$this->table('tiki_object_relations')->updateMultiple(['target_itemId' => $new], ['target_itemId' => $old, 'target_type' => $type]);
6931
6932		$menulib = TikiLib::lib('menu');
6933		$menulib->rename_wiki_page($old, $new);
6934	}
6935
6936	/**
6937	 * @param $delimiters
6938	 * @param $string
6939	 * @return array
6940	 */
6941	function multi_explode($delimiters, $string)
6942	{
6943		global $prefs;
6944
6945		if (is_array($delimiters) == false) {
6946			$delimiters = [$delimiters];
6947		}
6948
6949		$delimiter = array_shift($delimiters);
6950		$temp = explode($delimiter, $string);
6951
6952		$array = [];
6953		$keep = false;
6954
6955		$ignore_chars = array_unique(str_split($prefs['namespace_separator']));
6956
6957		foreach ($temp as $v) {
6958			$filtered = str_replace($ignore_chars, '', $v);
6959			if ($filtered == '' && $v != '') {
6960				if (! $keep) {
6961					$array[count($array) - 1] .= $delimiter;
6962				}
6963
6964				$array[count($array) - 1] .= $v . $delimiter;
6965				$keep = true;
6966			} elseif ($keep) {
6967				$array[count($array) - 1] .= $v;
6968				$keep = false;
6969			} else {
6970				$array[] = $v;
6971			}
6972		}
6973
6974		if ($delimiters != null) {
6975			foreach ($array as $key => $val) {
6976				 $array[$key] = $this->multi_explode($delimiters, $val);
6977			}
6978		}
6979
6980		return $array;
6981	}
6982
6983	/**
6984	 * @param $delimiters
6985	 * @param $string
6986	 * @return string
6987	 */
6988	function multi_implode($delimiters, $array)
6989	{
6990		$delimiters = (array) $delimiters;
6991		$delimiter = array_shift($delimiters);
6992
6993		if (count($delimiters)) {
6994			$self = $this;
6995			$array = array_map(
6996				function ($value) use ($delimiters, $self) {
6997					return $self->multi_implode($delimiters, $value);
6998				},
6999				$array
7000			);
7001		}
7002
7003		return implode($delimiter, $array);
7004	}
7005
7006	/**
7007	 * @param $vals
7008	 * @param $filter
7009	 * @return string
7010	 */
7011	function array_apply_filter($vals, $filter)
7012	{
7013		if (is_array($vals) == true) {
7014			foreach ($vals as $key => $val) {
7015				$vals[$key] = $this->array_apply_filter($val, $filter);
7016			}
7017			return $vals;
7018		} else {
7019			return trim($filter->filter($vals));
7020		}
7021	}
7022
7023	/**
7024	 * @param $type
7025	 * @param $object
7026	 * @param bool $process
7027	 * @return bool
7028	 */
7029	function refresh_index($type, $object, $process = true)
7030	{
7031		require_once __DIR__ . '/search/refresh-functions.php';
7032		return refresh_index($type, $object, $process);
7033	}
7034
7035	/**
7036	 * Possibly enhanced version of strtolower(), using multi-byte if mbstring is available
7037	 *
7038	 * Since Tiki 17, mb_strtolower() can be used directly instead since Tiki indirectly depends on the symfony/polyfill-mbstring compatibility library.
7039	 *
7040	 * @param $string
7041	 * @return string
7042	 */
7043	public static function strtolower($string)
7044	{
7045		if (function_exists('mb_strtolower')) {
7046			return mb_strtolower($string, 'UTF-8');
7047		} else {
7048			return strtolower($string);
7049		}
7050	}
7051
7052	/**
7053	 * Possibly enhanced version of strtoupper(), using multi-byte if mbstring is available
7054	 *
7055	 * Since Tiki 17, mb_strtoupper() can be used directly instead since Tiki indirectly depends on the symfony/polyfill-mbstring compatibility library.
7056	 *
7057	 * @param $string
7058	 * @return string
7059	 */
7060	public static function strtoupper($string)
7061	{
7062		if (function_exists('mb_strtoupper')) {
7063			return mb_strtoupper($string, 'UTF-8');
7064		} else {
7065			return strtoupper($string);
7066		}
7067	}
7068
7069	/**
7070	 * @param $string
7071	 * @return string UTF-8
7072	 */
7073	public static function urldecode($string)
7074	{
7075		return TikiInit::to_utf8(urldecode($string));
7076	}
7077
7078	/**
7079	 * @param $string
7080	 * @return string UTF-8
7081	 */
7082	public static function rawurldecode($string)
7083	{
7084		return TikiInit::to_utf8(rawurldecode($string));
7085	}
7086
7087	/**
7088	 * Unparse an array of url parts, e.g. the result of parse_url()
7089	 * Thanks to http://php.net/manual/en/function.parse-url.php#106731
7090	 *
7091	 * @param $parsed_url
7092	 * @return string
7093	 */
7094	public static function unparse_url($parsed_url)
7095	{
7096		$scheme   = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : '//';
7097		$host     = isset($parsed_url['host']) ? $parsed_url['host'] : '';
7098		$port     = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '';
7099		$user     = isset($parsed_url['user']) ? $parsed_url['user'] : '';
7100		$pass     = isset($parsed_url['pass']) ? ':' . $parsed_url['pass'] : '';
7101		$pass     = ($user || $pass) ? "$pass@" : '';
7102		$path     = isset($parsed_url['path']) ? $parsed_url['path'] : '';
7103		$query    = isset($parsed_url['query']) ? '?' . $parsed_url['query'] : '';
7104		$fragment = isset($parsed_url['fragment']) ? '#' . $parsed_url['fragment'] : '';
7105
7106		return "$scheme$user$pass$host$port$path$query$fragment";
7107	}
7108
7109
7110
7111	/**
7112	*	Return the request URI.
7113	*	Assumes http or https is used. Non-standard ports are taken into account
7114	*	@return Full URL to the current page
7115	  * \static
7116	*/
7117	// Note: this is unused as of r37658, but quite generic.
7118	static function curPageURL()
7119	{
7120		$pageURL = 'http';
7121		if (isset($_SERVER["HTTPS"]) && ($_SERVER["HTTPS"] == "on")) {
7122			$pageURL .= 's';
7123		}
7124		$pageURL .= '://';
7125		if ($_SERVER['SERVER_PORT'] != '80') {
7126			$pageURL .= $_SERVER['SERVER_NAME'] . ":" . $_SERVER['SERVER_PORT'] . $_SERVER['REQUEST_URI'];
7127		} else {
7128			$pageURL .= $_SERVER['SERVER_NAME'] . $_SERVER['REQUEST_URI'];
7129		}
7130		return $pageURL;
7131	}
7132
7133	/**
7134	 * @param array $data
7135	 * @return array
7136	 */
7137	public static function array_flat(array $data)
7138	{
7139		$out = [];
7140		foreach ($data as $entry) {
7141			if (is_array($entry)) {
7142				$out = array_merge($out, self::array_flat($entry));
7143			} else {
7144				$out[] = $entry;
7145			}
7146		}
7147		return $out;
7148	}
7149
7150	/**
7151	 * This checks the modifier array and scans the template directory for templates
7152	 * that match the modifiers.
7153	 * Example: if we are looking at modifier "blog" for the articles.tpl, this function
7154	 * looks for the existence of articles--blog.tpl to use before using the standard articles.tpl
7155	 *
7156	 * @param $basetpl
7157	 * @param $modifier_arr
7158	 * @return string
7159	 * @throws Exception
7160	 */
7161	public static function custom_template($basetpl, $modifier_arr)
7162	{
7163		//if it's an item passed and not an array, put the item in an array
7164		if (! is_array($modifier_arr)) {
7165			$modifier_arr = [$modifier_arr];
7166		}
7167		//strip the .tpl
7168		$temp = explode('.', $basetpl);
7169		$ext  = array_pop($temp);
7170		$base = implode('.', $temp);
7171
7172		$smarty = TikiLib::lib('smarty');
7173		foreach ($modifier_arr as $modifier) {
7174			if ($smarty->templateExists("$base--$modifier.tpl")) {
7175				return "$base--$modifier.tpl";
7176			}
7177		}
7178		return "$base.tpl";
7179	}
7180
7181	/**
7182	 * @param $page
7183	 * @return mixed
7184	 */
7185	public function removePageReference($page)
7186	{
7187		$page_id = $this->get_page_id_from_name($page);
7188		$query = "DELETE FROM `tiki_page_references` WHERE `page_id`=?";
7189		$result = $this->query($query, [$page_id]);
7190		return $result;
7191	}
7192
7193	/**
7194	 * @param array $new_toolbars
7195	 * @param string $section
7196	 * @param string $action
7197	 */
7198	public function saveEditorToolbars($new_toolbars = [], $section = 'global', $action = 'add')
7199	{
7200		global $prefs;
7201		$prefName = 'toolbar_' . $section;
7202		$toolbars = explode(',', $prefs[$prefName]);
7203		if ($action == 'add') {
7204			foreach ($new_toolbars as $key => $value) {
7205				if (! in_array($value, $toolbars)) {
7206					$toolbars[] = $value;
7207				}
7208			}
7209		} else {//remove the toolbars
7210			$toolbars = array_diff($toolbars, $new_toolbars);
7211		}
7212		$toolbars = implode(',', $toolbars);
7213		$this->set_preference($prefName, $toolbars);
7214	}
7215
7216	/**
7217	 * @param $haystack
7218	 * @param $needle
7219	 * @return bool
7220	 */
7221	static function startsWith($haystack, $needle)
7222	{
7223		$length = strlen($needle);
7224		return (substr($haystack, 0, $length) === $needle);
7225	}
7226
7227	/**
7228	 * @param $haystack
7229	 * @param $needle
7230	 * @return bool
7231	 */
7232	static function endsWith($haystack, $needle)
7233	{
7234		$length = strlen($needle);
7235		if ($length == 0) {
7236			return true;
7237		}
7238
7239		$start  = $length * -1; //negative
7240		return (substr($haystack, $start) === $needle);
7241	}
7242
7243	/**
7244	 * Checks if all link aliases contained in a page are valid, it automatically flashes the error in case there are invalid aliases
7245	 * @param String $edit  Contains page edit content
7246	 * @param String $page  Page name
7247	 * @return bool returns false if there is at least one invalid alias
7248	 * @throws Exception
7249	 */
7250	function check_duplicate_alias($edit, $page)
7251	{
7252		$errors = [];
7253
7254		$parserlib = TikiLib::lib('parser');
7255		$table = $this->table('tiki_object_relations');
7256
7257		$smarty = TikiLib::lib('smarty');
7258		$smarty->loadPlugin('smarty_modifier_sefurl');
7259
7260		foreach ($parserlib->get_pages($edit, true) as $pointedPage => $types) {
7261			if (empty($types[0]) || $types[0] != 'alias') {
7262				continue;
7263			}
7264
7265			$conflictPages = $table->fetchColumn('source_itemId', [
7266				'target_itemId' => $pointedPage,
7267				'source_itemId' => $table->not($page),
7268				'relation' => $table->like('%alias%')
7269			]);
7270
7271			if (empty($conflictPages)) {
7272				continue;
7273			}
7274
7275			$url = [];
7276			foreach ($conflictPages as $pageName) {
7277				$url[] = sprintf('<a href="%s">%s</a>', smarty_modifier_sefurl($pageName, 'wiki'), $pageName);
7278			}
7279
7280			$errors[] = tr('Alias <b>%0</b> link already present in %1 page(s)', $pointedPage, implode(', ', $url));
7281		}
7282
7283		if (! empty($errors)) {
7284			Feedback::error(implode('<br>', $errors));
7285		}
7286
7287		return empty($errors);
7288	}
7289
7290	/**
7291	 * @param $arr - array of data to convert to csv
7292	 * @return string - csv formatted string
7293	 */
7294	function str_putcsv($arr)
7295	{
7296		$fh = fopen('php://temp', 'rw');
7297		fputcsv($fh, $arr);
7298		rewind($fh);
7299		$csv = stream_get_contents($fh);
7300		fclose($fh);
7301		return trim($csv);
7302	}
7303
7304	/**
7305	 * Find a text inside string range
7306	 *
7307	 * @param string $text
7308	 * @param string $string
7309	 * @param int $from
7310	 * @param int $to
7311	 * @return mixed
7312	 */
7313	public function findText($text, $string, $from, $to)
7314	{
7315		if ($from >= strlen($text)) {
7316			return false;
7317		}
7318
7319		$pos = strpos($text, $string, $from);
7320
7321		if ($pos === false || $pos + strlen($string) > $to) {
7322			return false;
7323		}
7324
7325		return $pos;
7326	}
7327
7328	/**
7329	 * Return wiki markers
7330	 *
7331	 * @return array
7332	 */
7333	public function getWikiMarkers()
7334	{
7335		$listMarkers = [
7336			['~np~', '~/np~'],
7337			['-+', '+-'],
7338			['~pp~', '~/pp~'],
7339			['~pre~', '~/pre~'],
7340			['-=', '=-'],
7341		];
7342
7343		return $listMarkers;
7344	}
7345}
7346// end of class ------------------------------------------------------
7347
7348// function to check if a file or directory is in the path
7349// returns FALSE if incorrect
7350// returns the canonicalized absolute pathname otherwise
7351/**
7352 * @param $file
7353 * @param $dir
7354 * @return bool|string
7355 */
7356function inpath($file, $dir)
7357{
7358	$realfile = realpath($file);
7359	$realdir = realpath($dir);
7360	if (! $realfile) {
7361		return (false);
7362	}
7363	if (! $realdir) {
7364		return (false);
7365	}
7366	if (substr($realfile, 0, strlen($realdir)) != $realdir) {
7367		return(false);
7368	} else {
7369		return($realfile);
7370	}
7371}
7372
7373/**
7374 * @param $ar1
7375 * @param $ar2
7376 * @return mixed
7377 */
7378function compare_links($ar1, $ar2)
7379{
7380	return $ar1["links"] - $ar2["links"];
7381}
7382
7383/**
7384 * @param $ar1
7385 * @param $ar2
7386 * @return mixed
7387 */
7388function compare_backlinks($ar1, $ar2)
7389{
7390	return $ar1["backlinks"] - $ar2["backlinks"];
7391}
7392
7393/**
7394 * @param $ar1
7395 * @param $ar2
7396 * @return mixed
7397 */
7398function r_compare_links($ar1, $ar2)
7399{
7400	return $ar2["links"] - $ar1["links"];
7401}
7402
7403/**
7404 * @param $ar1
7405 * @param $ar2
7406 * @return mixed
7407 */
7408function r_compare_backlinks($ar1, $ar2)
7409{
7410	return $ar2["backlinks"] - $ar1["backlinks"];
7411}
7412
7413/**
7414 * @param $ar1
7415 * @param $ar2
7416 * @return mixed
7417 */
7418function compare_images($ar1, $ar2)
7419{
7420	return $ar1["images"] - $ar2["images"];
7421}
7422
7423/**
7424 * @param $ar1
7425 * @param $ar2
7426 * @return mixed
7427 */
7428function r_compare_images($ar1, $ar2)
7429{
7430	return $ar2["images"] - $ar1["images"];
7431}
7432
7433/**
7434 * @param $ar1
7435 * @param $ar2
7436 * @return mixed
7437 */
7438function compare_versions($ar1, $ar2)
7439{
7440	return $ar1["versions"] - $ar2["versions"];
7441}
7442
7443/**
7444 * @param $ar1
7445 * @param $ar2
7446 * @return mixed
7447 */
7448function r_compare_versions($ar1, $ar2)
7449{
7450	return $ar2["versions"] - $ar1["versions"];
7451}
7452
7453/**
7454 * @param $ar1
7455 * @param $ar2
7456 * @return mixed
7457 */
7458function compare_changed($ar1, $ar2)
7459{
7460	return $ar1["lastChanged"] - $ar2["lastChanged"];
7461}
7462
7463/**
7464 * @param $ar1
7465 * @param $ar2
7466 * @return mixed
7467 */
7468function r_compare_changed($ar1, $ar2)
7469{
7470	return $ar2["lastChanged"] - $ar1["lastChanged"];
7471}
7472
7473/**
7474 * @param $ar1
7475 * @param $ar2
7476 * @return int
7477 */
7478function compare_names($ar1, $ar2)
7479{
7480	return strcasecmp(tra($ar1["name"]), tra($ar2["name"]));
7481}
7482
7483function chkgd2()
7484{
7485	return function_exists('imagecreatetruecolor');
7486}
7487
7488
7489/**
7490 * @return string
7491 */
7492function detect_browser_language()
7493{
7494	global $prefs;
7495	// Get supported languages
7496	if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
7497		$supported = preg_split('/\s*,\s*/', preg_replace('/;q=[0-9.]+/', '', $_SERVER['HTTP_ACCEPT_LANGUAGE']));
7498	} else {
7499		return '';
7500	}
7501
7502	// Get available languages
7503	$available = [];
7504	$available_aprox = [];
7505
7506	if (is_dir("lang")) {
7507		$dh = opendir("lang");
7508		while ($lang = readdir($dh)) {
7509			if (! strpos($lang, '.') and is_dir("lang/$lang") and file_exists("lang/$lang/language.php") and ($prefs['restrict_language'] === 'n' || empty($prefs['available_languages']) || in_array($lang, $prefs['available_languages']))) {
7510				$available[strtolower($lang)] = $lang;
7511				$available_aprox[substr(strtolower($lang), 0, 2)] = $lang;
7512			}
7513		}
7514	}
7515
7516	// Check better language
7517	// Priority has been changed in 2.0 to that defined in RFC 4647
7518	$aproximate_lang = '';
7519	foreach ($supported as $supported_lang) {
7520		$lang = strtolower($supported_lang);
7521		if (in_array($lang, array_keys($available))) {
7522			// exact match is always good
7523			return $available[$lang];
7524		} elseif (in_array($lang, array_keys($available_aprox))) {
7525			// otherwise if supported language matches any available dialect, ok also
7526			return $available_aprox[$lang];
7527		} elseif ($aproximate_lang == '') {
7528			// otherwise if supported dialect matches language, store as possible fallback
7529			$lang = substr($lang, 0, 2);
7530			if (in_array($lang, array_keys($available_aprox))) {
7531				$aproximate_lang = $available_aprox[$lang];
7532			}
7533		}
7534	}
7535
7536	return $aproximate_lang;
7537}
7538
7539/**
7540 * Validates an email address, using a domain check if $validate == 'y'
7541 *
7542 * @param string $email email to validate
7543 * @param string $validate n|y|d (d = deep) defaults to pref validateEmail
7544 * @return bool
7545 */
7546function validate_email($email, $validate = null)
7547{
7548	global $prefs;
7549
7550	if (empty($validate)) {
7551		$validate = $prefs['validateEmail'];
7552	}
7553
7554	$options = ['allow' => Zend\Validator\Hostname::ALLOW_ALL,];
7555
7556	if ($validate === 'n') {
7557		return true;
7558	} else {
7559		$options['useDomainCheck'] = true;	// both y and d
7560	}
7561
7562	if ($validate === 'd') {				// deep mx check
7563		$options['useMxCheck'] = true;
7564		$options['useDeepMxCheck'] = true;
7565	}
7566	$validator = new Zend\Validator\EmailAddress($options);
7567	return $validator->isValid($email);
7568}
7569
7570/**
7571 * @param $val
7572 * @param $default
7573 * @return string
7574 */
7575function makeBool($val, $default)
7576{
7577	// Warning: This function is meant to return a string 'true' or 'false' to be used in JS, not a real boolean value
7578	if (isset($val) && ! empty($val)) {
7579		$val = ($val == 'y' ? true : false);
7580	} else {
7581		$val = $default;
7582	}
7583	return ($val ? 'true' : 'false');
7584}
7585/* Editor configuration
7586	 Local Variables:
7587	 tab-width: 4
7588	 c-basic-offset: 4
7589End:
7590 * vim: fdm=marker tabstop=4 shiftwidth=4 noet:
7591 */
7592
7593
7594/**
7595 *
7596 * Writes a temporary directory and/or file in a cryptographically secure way.
7597 *
7598 * @param string|null $data Data to be written to file, null if we are creating directories only.
7599 * @param string      $directory Directory for the file to be created in. using the string 'random' will generate a random directory. Sending NULL will create a directory only.
7600 * @param bool        $system If files should be stored in the system directory (outside the web root), will fall back to tiki /temp directory upon failure.
7601 * @param string      $prefix A string to add to the beginning of the file name.
7602 * @param string      $append A string to append the file name, such as an extension.
7603 *
7604 * @return string            The path and filename of the file written.
7605 * @throws exception        If a file can not be created, an exception will be thrown.
7606 */
7607
7608function writeTempFile(?string $data, string $directory = '', bool $system = true, string $prefix = '', string $append = ''): ?string {
7609	global $prefs;
7610	$fileName = '';
7611
7612	if ($directory === 'random') {
7613		if (is_callable('random_bytes')) {
7614			$directory = bin2hex(random_bytes(16)) . '/';
7615		} else {
7616			$directory = dechex(rand(0, 2 ** 62)) . dechex(rand(0, 2 ** 62)) . '/';
7617		}
7618	}
7619
7620	if (strlen($prefix) + strlen($append) > 223) {
7621		throw new Exception('File name must be under 255 characters.');
7622	}
7623
7624	if ($system) {
7625		$tmpDir = $prefs['tmpDir'];
7626		if (substr($tmpDir, -1) !== '/'){
7627			$tmpDir = $tmpDir . '/';
7628		}
7629		if (file_exists($tmpDir . $directory)) {
7630			$dirName = $tmpDir . $directory;
7631		} elseif (@mkdir($tmpDir . $directory)) {
7632			$dirName = $tmpDir . $directory;
7633		}
7634		// if the system directory is not writable, then fall back to Tiki tmp directory.
7635		if (!is_writable($tmpDir . $directory)){
7636			unset($dirName);
7637		}
7638	}
7639
7640	if (! isset($dirName)) {
7641		if (file_exists('temp/' . $directory)) {
7642			$dirName = 'temp/' . $directory;
7643		} elseif (@mkdir('temp/' . $directory)) {
7644			$dirName = 'temp/' . $directory;
7645			@file_put_contents('temp/' . $directory . 'index.php', '');
7646		} else {
7647			throw new Exception ("Can not create temp/$directory directory.");
7648		}
7649	}
7650
7651	if (! is_null($data)) {
7652
7653		do {
7654			if (is_callable('random_bytes')) {
7655				$fileName = $prefix . bin2hex(random_bytes(16)) . $append;
7656			} else {
7657				$fileName = $prefix . dechex(rand(0, 2 ** 62)) . dechex(rand(0, 2 ** 62)) . $append;
7658			}
7659		} while (file_exists($dirName . $fileName));
7660
7661
7662		if (@file_put_contents($dirName . $fileName, $data) === false) {
7663			throw new exception ("Can not write to $dirName$fileName file.");
7664		}
7665	}
7666	return $dirName . $fileName;
7667}
7668