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
8/**
9 * Parser Library
10 *
11 * \wiki syntax parser for tiki
12 *
13 * NB: Needs to be kept in utf-8
14 *
15 * @package		Tiki
16 * @subpackage		Parser
17 * @author		Robert Plummer
18 * @copyright (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
19 * 			See copyright.txt for details and a complete list of authors.
20 * @licence Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
21 * @version		SVN $Rev$
22 * @filesource
23 * @link		http://dev.tiki.org/Parser
24 * @since		8
25 * @see WikiParser_Parsable
26 */
27class ParserLib extends TikiDb_Bridge
28{
29	// private $makeTocCount = 0; Unused since Tiki 12 or earlier
30
31	//This var is used in both protectSpecialChars and unprotectSpecialChars to simplify the html ouput process. Can be replaced with a const starting with PHP 7
32	public $specialChars = [
33		'≤REAL_LT≥' => [
34			'html' => '<',
35			'nonHtml' => '&lt;'
36		],
37		'≤REAL_GT≥' => [
38			'html' => '>',
39			'nonHtml' => '&gt;'
40		],
41		'≤REAL_NBSP≥' => [
42			'html' => '&nbsp;',
43			'nonHtml' => '&nbsp;'
44		],
45		/*on post back the page is parsed, which turns & into &amp;
46		this is done to prevent that from happening, we are just
47		protecting some chars from letting the parser nab them*/
48		'≤REAL_AMP≥' => [
49			'html' => '& ',
50			'nonHtml' => '& '
51		],
52	];
53
54	/*
55	 * Parsing "options". Some of these are real parsing parameters, such as protect_email and security options.
56	 * Others (like is_html) define the markup's semantic.
57	 *
58	 * TODO: Separate real parsing parameters from properties of the parsable markup
59	 * TO DO: To ease usage tracking, it may be best to replace $option with individual properties.
60	 * Or replace setOptions() with individual setters?
61	 */
62	public $option = []; // An associative array of (most) parameters (despite the singular)
63
64	function setOptions($option = [])
65	{
66		global $page, $prefs;
67
68		$this->option = array_merge(
69			[
70				'is_html' => false,
71
72				/* Determines if "Tiki syntax" is parsed in some circumstances.
73				Currently, when is_html is true, but that is probably wrong.
74				Overriden by the HTML plugin to force wiki parsing */
75				'parse_wiki' => ! isset($prefs['wysiwyg_wiki_parsed']) || $prefs['wysiwyg_wiki_parsed'] === 'y',
76
77				'absolute_links' => false,
78				'language' => '',
79				'noparseplugins' => false,
80				'stripplugins' => false,
81				'noheaderinc' => false,
82				'page' => $page,
83				'print' => false,
84				'parseimgonly' => false,
85				'preview_mode' => false,
86				'suppress_icons' => false,
87				'parsetoc' => true,
88				'inside_pretty' => false,
89				'process_wiki_paragraphs' => true,
90				'min_one_paragraph' => false,
91				'skipvalidation' => false,
92				'ck_editor' => false,
93				'namespace' => false,
94				'protect_email' => true,
95				'exclude_plugins' => [],
96				'exclude_all_plugins' => false,
97				'include_plugins' => [],
98				'typography' => true,
99			],
100			empty($option) ? [] : (array) $this->option,
101			(array)$option
102		);
103		$this->option['include_plugins'] = array_map('strtolower', $this->option['include_plugins']);
104		$this->option['exclude_plugins'] = array_map('strtolower', $this->option['exclude_plugins']);
105	}
106
107	function __construct()
108	{
109		$this->setOptions();
110	}
111	//*
112	function parse_data_raw($data)
113	{
114		$data = $this->parse_data($data);
115		$data = str_replace("tiki-index", "tiki-index_raw", $data);
116		return $data;
117	}
118
119	// This function handles wiki codes for those special HTML characters
120	// that textarea won't leave alone.
121	//*
122	protected function parse_htmlchar(&$data)
123	{
124		// cleaning some user input
125		// ckeditor parses several times and messes things up, we should only let it parse once
126		if (! $this->option['ck_editor']) {
127			$data = str_replace('&', '&amp;', $data);
128		}
129
130		// oft-used characters (case insensitive)
131		$data = preg_replace("/~bs~/i", "&#92;", $data);
132		$data = preg_replace("/~hs~/i", "&nbsp;", $data);
133		$data = preg_replace("/~amp~/i", "&amp;", $data);
134		$data = preg_replace("/~ldq~/i", "&ldquo;", $data);
135		$data = preg_replace("/~rdq~/i", "&rdquo;", $data);
136		$data = preg_replace("/~lsq~/i", "&lsquo;", $data);
137		$data = preg_replace("/~rsq~/i", "&rsquo;", $data);
138		$data = preg_replace("/~c~/i", "&copy;", $data);
139		$data = preg_replace("/~--~/", "&mdash;", $data);
140		$data = preg_replace("/ -- /", " &mdash; ", $data);
141		$data = preg_replace("/~lt~/i", "&lt;", $data);
142		$data = preg_replace("/~gt~/i", "&gt;", $data);
143
144		// HTML numeric character entities
145		$data = preg_replace("/~([0-9]+)~/", "&#$1;", $data);
146	}
147
148	// This function handles the protection of html entities so that they are not mangled when
149	// parse_htmlchar runs, and as well so they can be properly seen, be it html or non-html
150	function protectSpecialChars($data, $is_html = false)
151	{
152		if ((isset($this->option['is_html']) && $this->option['is_html'] != true) || ! empty($this->option['ck_editor'])) {
153			foreach ($this->specialChars as $key => $specialChar) {
154				$data = str_replace($specialChar['html'], $key, $data);
155			}
156		}
157		return $data;
158	}
159
160	// This function removed the protection of html entities so that they are rendered as expected by the viewer
161	function unprotectSpecialChars($data, $is_html = false)
162	{
163		if (( $is_html != false || ( isset($this->option['is_html']) && $this->option['is_html']))
164			|| $this->option['ck_editor']) {
165			foreach ($this->specialChars as $key => $specialChar) {
166				$data = str_replace($key, $specialChar['html'], $data);
167			}
168		} else {
169			foreach ($this->specialChars as $key => $specialChar) {
170				$data = str_replace($key, $specialChar['nonHtml'], $data);
171			}
172		}
173
174		return $data;
175	}
176
177	// Reverses parse_first.
178	//*
179	function replace_preparse(&$data, &$preparsed, &$noparsed, $is_html = false)
180	{
181		$data1 = $data;
182		$data2 = "";
183
184		// Cook until done.  Handles nested cases.
185		while ($data1 != $data2) {
186			$data1 = $data;
187			if (isset($noparsed["key"]) and count($noparsed["key"]) and count($noparsed["key"]) == count($noparsed["data"])) {
188				$data = str_replace($noparsed["key"], $noparsed["data"], $data);
189			}
190
191			if (isset($preparsed["key"]) and count($preparsed["key"]) and count($preparsed["key"]) == count($preparsed["data"])) {
192				$data = str_replace($preparsed["key"], $preparsed["data"], $data);
193			}
194			$data2 = $data;
195		}
196
197		$data = $this->unprotectSpecialChars($data, $is_html);
198	}
199
200	/**
201	 * Replace plugins with guid keys and store them in an array
202	 *
203	 * @param $data	string		data to be cleaned of plugins
204	 * @param $noparsed array	output array
205	 * @see parserLib::plugins_replace()
206	 */
207	function plugins_remove(&$data, &$noparsed, $removeCb = null)
208	{
209		$tikilib = TikiLib::lib('tiki');
210		if (isset($removeCb) && ! is_callable($removeCb)) {
211			throw new Exception('Invalid callback');
212		}
213		$matches = WikiParser_PluginMatcher::match($data);		// find the plugins
214		foreach ($matches as $match) {							// each plugin
215			if (isset($removeCb) && ! $removeCb($match)) {
216				continue;
217			}
218			$plugin = (string) $match;
219			$key = '§' . md5($tikilib->genPass()) . '§';				// by replace whole plugin with a guid
220
221			$noparsed['key'][] = $key;
222			$noparsed['data'][] = $plugin;
223		}
224		$data = isset($noparsed['data']) ? str_replace($noparsed['data'], $noparsed['key'], $data) : $data;
225	}
226
227	/**
228	 * Restore plugins from array
229	 *
230	 * @param $data	string		data previously processed with plugins_remove()
231	 * @param $noparsed array	input array
232	 */
233
234	function plugins_replace(&$data, $noparsed, $is_html = false)
235	{
236		$preparsed = [];	// unused
237		$noparsed['data'] = isset($noparsed['data']) ? str_replace('<x>', '', $noparsed['data']) : '';
238		$this->replace_preparse($data, $preparsed, $noparsed, $is_html);
239	}
240
241	//*
242	private function plugin_match(&$data, &$plugins)
243	{
244		global $pluginskiplist;
245		if (! is_array($pluginskiplist)) {
246			$pluginskiplist = [];
247		}
248
249		$matcher_fake = ["~pp~","~np~","&lt;pre&gt;"];
250		$matcher = "/\{([A-Z0-9_]+) *\(|\{([a-z]+)(\s|\})|~pp~|~np~|&lt;[pP][rR][eE]&gt;/";
251
252		$plugins = [];
253		preg_match_all($matcher, $data, $tmp, PREG_SET_ORDER);
254		foreach ($tmp as $p) {
255			if (in_array(TikiLib::strtolower($p[0]), $matcher_fake)
256				|| ( isset($p[1]) && ( in_array($p[1], $matcher_fake) || $this->plugin_exists($p[1]) ) )
257				|| ( isset($p[2]) && ( in_array($p[2], $matcher_fake) || $this->plugin_exists($p[2]) ) )
258			) {
259				$plugins = $p;
260				break;
261			}
262		}
263
264		// Check to make sure there was a match.
265		if (count($plugins) > 0 && strlen($plugins[0]) > 0) {
266			$pos = 0;
267			while (in_array($plugins[0], $pluginskiplist)) {
268				$pos = strpos($data, $plugins[0], $pos) + 1;
269				if (! preg_match($matcher, substr($data, $pos), $plugins)) {
270					return;
271				}
272			}
273
274			// If it is a true plugin
275			if ($plugins[0]{0} == "{") {
276				$pos = strpos($data, $plugins[0]); // where plugin starts
277				$pos_end = $pos + strlen($plugins[0]); // where character after ( is
278
279				// Here we're going to look for the end of the arguments for the plugin.
280
281				$i = $pos_end;
282				$last_data = strlen($data);
283
284				// We start with one open curly brace, and one open paren.
285				$curlies = 1;
286
287				// If model with (
288				if (strlen($plugins[1])) {
289					$parens = 1;
290					$plugins['type'] = 'long';
291				} else {
292					$parens = 0;
293					$plugins[1] = $plugins[2];
294					unset($plugins[3]);
295					$plugins['type'] = 'short';
296				}
297
298				// While we're not at the end of the string, and we still haven't found both closers
299				while ($i < $last_data) {
300					$char = substr($data, $i, 1);
301					//print "<pre>Data char: $i, $char, $curlies, $parens\n.</pre>\n";
302					if ($char == "{") {
303						$curlies++;
304					} elseif ($char == "(" && $plugins['type'] == 'long') {
305						$parens++;
306					} elseif ($char == "}") {
307						$curlies--;
308						if ($plugins['type'] == 'short') {
309							$lastParens = $i;
310						}
311					} elseif ($char == ")"  && $plugins['type'] == 'long') {
312						$parens--;
313						$lastParens = $i;
314					}
315
316					// If we found the end of the match...
317					if ($curlies == 0 && $parens == 0) {
318						break;
319					}
320
321					$i++;
322				}
323
324				if ($curlies == 0 && $parens == 0) {
325					$plugins[2] = (string) substr($data, $pos_end, $lastParens - $pos_end);
326					$plugins[0] = $plugins[0] . (string) substr($data, $pos_end, $i - $pos_end + 1);
327					/*
328						 print "<pre>Match found: ";
329						 print( $plugins[2] );
330						 print "</pre>";
331					 */
332				}
333
334				$plugins['arguments'] = isset($plugins[2]) ? $this->plugin_split_args($plugins[2]) : [];
335			} else {
336				$plugins[1] = $plugins[0];
337				$plugins[2] = "";
338			}
339		}
340
341		/*
342			 print "<pre>Plugin match end:";
343			 print_r( $plugins );
344			 print "</pre>";
345		 */
346	}
347
348	//*
349	function plugin_split_args($params_string)
350	{
351		$parser = new WikiParser_PluginArgumentParser;
352
353		return $parser->parse($params_string);
354	}
355
356	// get all the plugins of a text- can be limitted only to some
357	//*
358	function getPlugins($data, $only = null)
359	{
360		$plugins = [];
361		for (;;) {
362			$this->plugin_match($data, $plugin);
363			if (empty($plugin)) {
364				break;
365			}
366			if (empty($only) || in_array($plugin[1], $only) || in_array(TikiLib::strtoupper($plugin[1]), $only) || in_array(TikiLib::strtolower($plugin[1]), $only)) {
367				$plugins[] = $plugin;
368			}
369			$pos = strpos($data, $plugin[0]);
370			$data = substr_replace($data, '', $pos, strlen($plugin[0]));
371		}
372		return $plugins;
373	}
374
375	// Transitional wrapper over WikiParser_Parsable::parse_first()
376	// This recursive function handles pre- and no-parse sections and plugins
377	function parse_first(&$data, &$preparsed, &$noparsed, $real_start_diff = '0')
378	{
379		return (new WikiParser_Parsable(''))->parse_first($data, $preparsed, $noparsed, $real_start_diff);
380	}
381
382	protected function strip_unparsed_block(& $data, & $noparsed, $protect = false)
383	{
384		$tikilib = TikiLib::lib('tiki');
385
386		$start = -1;
387		while (false !== $start = strpos($data, '~np~', $start + 1)) {
388			if (false !== $end = strpos($data, '~/np~', $start)) {
389				$content = substr($data, $start + 4, $end - $start - 4);
390				if ($protect) {
391					$content = $this->protectSpecialChars($content, $this->option['is_html']);
392				}
393				// ~pp~ type "plugins"
394				$key = "§" . md5($tikilib->genPass()) . "§";
395				$noparsed["key"][] = preg_quote($key);
396				$noparsed["data"][] = $content;
397
398				$data = substr($data, 0, $start) . $key . substr($data, $end + 5);
399			}
400		}
401	}
402
403	//
404	// Call 'wikiplugin_.*_description()' from given file
405	//
406	public function get_plugin_description($name, &$enabled, $area_id = 'editwiki')
407	{
408		if (( ! $info = $this->plugin_info($name) ) && $this->plugin_exists($name, true)) {
409			$enabled = true;
410
411			$func_name = "wikiplugin_{$name}_help";
412			if (! function_exists($func_name)) {
413				return false;
414			}
415
416			$ret = $func_name();
417			return $this->parse_data($ret);
418		} else {
419			$smarty = TikiLib::lib('smarty');
420			$enabled = true;
421
422			$ret = $info;
423
424			if (isset($ret['prefs'])) {
425				global $prefs;
426
427				// If the plugin defines required preferences, they should all be to 'y'
428				foreach ($ret['prefs'] as $pref) {
429					if (! isset($prefs[$pref]) || $prefs[$pref] != 'y') {
430						$enabled = false;
431						return;
432					}
433				}
434			}
435
436			if (isset($ret['documentation']) && ctype_alnum($ret['documentation'])) {
437				$ret['documentation'] = "http://doc.tiki.org/{$ret['documentation']}";
438			}
439
440			$smarty->assign('area_id', $area_id);
441			$smarty->assign('plugin', $ret);
442			$smarty->assign('plugin_name', TikiLib::strtoupper($name));
443			return $smarty->fetch('tiki-plugin_help.tpl');
444		}
445	}
446
447	//*
448	function plugin_get_list($includeReal = true, $includeAlias = true)
449	{
450		return WikiPlugin_Negotiator_Wiki::getList($includeReal, $includeAlias);
451	}
452
453	//*
454	function plugin_exists($name, $include = false)
455	{
456		$php_name = 'lib/wiki-plugins/wikiplugin_';
457		$php_name .= TikiLib::strtolower($name) . '.php';
458
459		$exists = file_exists($php_name);
460
461		if ($include && $exists) {
462			include_once $php_name;
463		}
464
465		if ($exists) {
466			return true;
467		} elseif ($info = WikiPlugin_Negotiator_Wiki_Alias::info($name)) {
468			// Make sure the underlying implementation exists
469
470			return $this->plugin_exists($info['implementation'], $include);
471		}
472		return false;
473	}
474
475	//*
476	function plugin_info($name, $args = [])
477	{
478		static $known = [];
479
480		if (isset($known[$name]) && $name != 'package') {
481			return $known[$name];
482		}
483
484		if (! $this->plugin_exists($name, true)) {
485			return $known[$name] = false;
486		}
487
488		$func_name_info = "wikiplugin_{$name}_info";
489
490		if (! function_exists($func_name_info)) {
491			if ($info = WikiPlugin_Negotiator_Wiki_Alias::info($name)) {
492				return $known[$name] = $info['description'];
493			} else {
494				return $known[$name] = false;
495			}
496		}
497
498		// Support Tiki Packages param overrides for Package plugin
499		if ($name == 'package' && ! empty($args['package']) && ! empty($args['plugin'])) {
500			$info = $func_name_info();
501
502			$parts = explode('/', $args['package']);
503			if ($extensionPackage = \Tiki\Package\ExtensionManager::get($args['package'])) {
504				$path = $extensionPackage->getPath() . '/lib/wiki-plugins/' . $args['plugin'] . '.php';
505			} else {
506				$path = '';
507			}
508
509			if (! file_exists($path)) {
510				return $known[$name] = $info;
511			}
512
513			require_once($path);
514
515			$namespace = $extensionPackage->getBaseNamespace();
516			if (!empty($namespace)) {
517				$namespace .= '\\PackagePlugins\\';
518			}
519			$functionname = $namespace . $args['plugin'] . "_info";
520
521			if (! function_exists($functionname)) {
522				return $known[$name] = $info;
523			}
524
525			$viewinfo = $functionname();
526			if (isset($viewinfo['params'])) {
527				$combinedparams = $viewinfo['params'] + $info['params'];
528			} else {
529				$combinedparams = $info['params'];
530			}
531
532			$info = $viewinfo + $info;
533			$info['params'] = $combinedparams;
534
535			return $known[$name] = $info;
536		}
537
538		return $known[$name] = $func_name_info();
539	}
540
541	//*
542	function plugin_alias_info($name)
543	{
544		return WikiPlugin_Negotiator_Wiki_Alias::info($name);
545	}
546
547	//*
548	function plugin_alias_store($name, $data)
549	{
550		return WikiPlugin_Negotiator_Wiki_Alias::store($name, $data);
551	}
552
553	//*
554	function plugin_alias_delete($name)
555	{
556		return WikiPlugin_Negotiator_Wiki_Alias::delete($name);
557	}
558
559	//*
560	function plugin_enabled($name, & $output)
561	{
562		if (! $meta = $this->plugin_info($name)) {
563			return true; // Legacy plugins always execute
564		}
565
566		global $prefs;
567
568		$missing = [];
569
570		if (isset($meta['prefs'])) {
571			foreach ($meta['prefs'] as $pref) {
572				if ($prefs[$pref] != 'y') {
573					$missing[] = $pref;
574				}
575			}
576		}
577
578		if (count($missing) > 0) {
579			$output = WikiParser_PluginOutput::disabled($name, $missing);
580			return false;
581		}
582
583		return true;
584	}
585
586	//*
587	function plugin_is_inline($name)
588	{
589		if (! $meta = $this->plugin_info($name)) {
590			return true; // Legacy plugins always inline
591		}
592
593		global $prefs;
594
595		$inline = false;
596		if (isset($meta['inline']) && $meta['inline']) {
597			return true;
598		}
599
600		$inline_pref = 'wikiplugininline_' . $name;
601		if (isset($prefs[ $inline_pref ]) && $prefs[ $inline_pref ] == 'y') {
602			return true;
603		}
604
605		return false;
606	}
607
608	/**
609	 * Check if possible to execute a plugin
610	 *
611	 * @param string $name
612	 * @param string $data
613	 * @param array $args
614	 * @param bool $dont_modify
615	 * @return bool|string Boolean true if can execute, string 'rejected' if can't execute and plugin fingerprint if pending
616	 */
617	//*
618	function plugin_can_execute($name, $data = '', $args = [], $dont_modify = false)
619	{
620		global $prefs;
621
622		// If validation is disabled, anything can execute
623		if ($prefs['wiki_validate_plugin'] != 'y') {
624			return true;
625		}
626
627		$meta = $this->plugin_info($name, $args);
628
629		if (! isset($meta['validate'])) {
630			return true;
631		}
632
633		$fingerprint = $this->plugin_fingerprint($name, $meta, $data, $args);
634
635		if ($fingerprint === '') {		// only args or body were being validated and they're empty or safe
636			return true;
637		}
638
639		$val = $this->plugin_fingerprint_check($fingerprint, $dont_modify);
640		if (strpos($val, 'accept') === 0) {
641			return true;
642		} elseif (strpos($val, 'reject') === 0) {
643			return 'rejected';
644		} else {
645			global $tiki_p_plugin_approve, $tiki_p_plugin_preview, $user;
646			if (isset($_SERVER['REQUEST_METHOD'])
647				&& $_SERVER['REQUEST_METHOD'] == 'POST'
648				&& isset($_POST['plugin_fingerprint'])
649				&& $_POST['plugin_fingerprint'] == $fingerprint
650			) {
651				if ($tiki_p_plugin_approve == 'y') {
652					if (isset($_POST['plugin_accept'])) {
653						$tikilib = TikiLib::lib('tiki');
654						$this->plugin_fingerprint_store($fingerprint, 'accept');
655						$tikilib->invalidate_cache($this->option['page']);
656						return true;
657					} elseif (isset($_POST['plugin_reject'])) {
658						$tikilib = TikiLib::lib('tiki');
659						$this->plugin_fingerprint_store($fingerprint, 'reject');
660						$tikilib->invalidate_cache($this->option['page']);
661						return 'rejected';
662					}
663				}
664
665				if ($tiki_p_plugin_preview == 'y'
666					&& isset($_POST['plugin_preview']) ) {
667					return true;
668				}
669			}
670
671			return $fingerprint;
672		}
673	}
674
675	//*
676	function plugin_fingerprint_check($fp, $dont_modify = false)
677	{
678		global $user;
679		$tikilib = TikiLib::lib('tiki');
680		$limit = date('Y-m-d H:i:s', time() - 15 * 24 * 3600);
681		$result = $this->query("SELECT `status`, if (`status`='pending' AND `last_update` < ?, 'old', '') flag FROM `tiki_plugin_security` WHERE `fingerprint` = ?", [ $limit, $fp ]);
682
683		$needUpdate = false;
684
685		if ($row = $result->fetchRow()) {
686			$status = $row['status'];
687			$flag = $row['flag'];
688
689			if ($status == 'accept' || $status == 'reject') {
690				return $status;
691			}
692
693			if ($flag == 'old') {
694				$needUpdate = true;
695			}
696		} else {
697			$needUpdate = true;
698		}
699
700		if ($needUpdate && ! $dont_modify) {
701			if ($this->option['page']) {
702				$objectType = 'wiki page';
703				$objectId = $this->option['page'];
704			} else {
705				$objectType = '';
706				$objectId = '';
707			}
708
709			if (! $user) {
710				$user = tra('Anonymous');
711			}
712
713			$pluginSecurity = $tikilib->table('tiki_plugin_security');
714			$pluginSecurity->delete(['fingerprint' => $fp]);
715			$pluginSecurity->insert(
716				['fingerprint' => $fp, 'status' => 'pending',	'added_by' => $user,	'last_objectType' => $objectType, 'last_objectId' => $objectId]
717			);
718		}
719
720		return '';
721	}
722
723	//*
724	function plugin_fingerprint_store($fp, $type)
725	{
726		global $prefs, $user;
727		$tikilib = TikiLib::lib('tiki');
728		if ($this->option['page']) {
729			$objectType = 'wiki page';
730			$objectId = $this->option['page'];
731		} else {
732			$objectType = '';
733			$objectId = '';
734		}
735
736		$pluginSecurity = $tikilib->table('tiki_plugin_security');
737		$pluginSecurity->delete(['fingerprint' => $fp]);
738		$pluginSecurity->insert(
739			['fingerprint' => $fp,'status' => $type,'added_by' => $user,'last_objectType' => $objectType,'last_objectId' => $objectId]
740		);
741	}
742
743	//*
744	function plugin_clear_fingerprint($fp)
745	{
746		$tikilib = TikiLib::lib('tiki');
747		$pluginSecurity = $tikilib->table('tiki_plugin_security');
748		$pluginSecurity->delete(['fingerprint' => $fp]);
749	}
750
751	//*
752	function list_plugins_pending_approval()
753	{
754		$tikilib = TikiLib::lib('tiki');
755		return $tikilib->fetchAll("SELECT `fingerprint`, `added_by`, `last_update`, `last_objectType`, `last_objectId` FROM `tiki_plugin_security` WHERE `status` = 'pending' ORDER BY `last_update` DESC");
756	}
757
758	/**
759	 * Return a list of plugins by status
760	 *
761	 * @param string|array $statuses
762	 * @return array
763	 */
764	public function listPluginsByStatus($statuses)
765	{
766		if (! empty($statuses) && ! is_array($statuses)) {
767			$statuses = [$statuses];
768		}
769
770		$tikiLib = TikiLib::lib('tiki');
771		$pluginSecurity = $tikiLib->table('tiki_plugin_security');
772		return $pluginSecurity->fetchAll(
773			['fingerprint', 'added_by', 'last_update', 'last_objectType', 'last_objectId', 'status'],
774			['status' => $pluginSecurity->in($statuses)],
775			-1,
776			-1,
777			['last_update' => 'DESC']
778		);
779	}
780
781	//*
782	function approve_all_pending_plugins()
783	{
784		global $user;
785		$tikilib = TikiLib::lib('tiki');
786
787		$pluginSecurity = $tikilib->table('tiki_plugin_security');
788		$pluginSecurity->updateMultiple(['status' => 'accept', 'approval_by' => $user], ['status' => 'pending',]);
789	}
790
791	//*
792	function approve_selected_pending_plugings($fp)
793	{
794		global $user;
795		$tikilib = TikiLib::lib('tiki');
796
797		$pluginSecurity = $tikilib->table('tiki_plugin_security');
798		$pluginSecurity->update(['status' => 'accept', 'approval_by' => $user], ['fingerprint' => $fp]);
799	}
800
801	//*
802	function plugin_fingerprint($name, $meta, $data, $args)
803	{
804		$validate = (isset($meta['validate']) ? $meta['validate'] : '');
805
806		$data = $this->unprotectSpecialChars($data, true);
807
808		if ($validate == 'all' || $validate == 'body') {
809			// Tiki 6 and ulterior may insert sequences in plugin body to break XSS exploits. The replacement works around removing them to keep fingerprints identical for upgrades from previous versions.
810			$validateBody = str_replace('<x>', '', $data);
811		} else {
812			$validateBody = '';
813		}
814
815		if ($validate === 'body' && empty($validateBody)) {
816			return '';
817		}
818
819		if ($validate == 'all' || $validate == 'arguments') {
820			$validateArgs = $args;
821
822			// Remove arguments marked as safe from the fingerprint
823			foreach ($meta['params'] as $key => $info) {
824				if (isset($validateArgs[$key])
825					&& isset($info['safe'])
826					&& $info['safe']
827				) {
828					unset($validateArgs[$key]);
829				}
830			}
831			// Parameter order needs to be stable
832			ksort($validateArgs);
833
834			if (empty($validateArgs)) {
835				if ($validate === 'arguments') {
836					return '';
837				}
838				$validateArgs = [ '' => '' ];	// maintain compatibility with pre-Tiki 7 fingerprints
839			}
840		} else {
841			$validateArgs = [];
842		}
843
844		$bodyLen = str_pad(strlen($validateBody), 6, '0', STR_PAD_RIGHT);
845		$serialized = serialize($validateArgs);
846		$argsLen = str_pad(strlen($serialized), 6, '0', STR_PAD_RIGHT);
847
848		$bodyHash = md5($validateBody);
849		$argsHash = md5($serialized);
850
851		return "$name-$bodyHash-$argsHash-$bodyLen-$argsLen";
852	}
853
854	// Transitional wrapper over WikiParser_Parsable::plugin_execute()
855	function plugin_execute($name, $data = '', $args = [], $offset = 0, $validationPerformed = false, $option = [])
856	{
857		return (new WikiParser_Parsable(''))->plugin_execute($name, $data, $args, $offset, $validationPerformed, $option);
858	}
859
860	//*
861	protected function convert_plugin_for_ckeditor($name, $args, $plugin_result, $data, $info = [])
862	{
863		$ck_editor_plugin = '{' . (empty($data) ? $name : TikiLib::strtoupper($name) . '(') . ' ';
864		$arg_str = '';		// not using http_build_query() as it converts spaces into +
865		if (! empty($args)) {
866			foreach ($args as $argKey => $argValue) {
867				if (is_array($argValue)) {
868					if (isset($info['params'][$argKey]['separator'])) {
869						$sep = $info['params'][$argKey]['separator'];
870					} else {
871						 $sep = ',';
872					}
873					$ck_editor_plugin .= $argKey . '="' . implode($sep, $argValue) . '" ';	// process array
874					$arg_str .= $argKey . '=' . implode($sep, $argValue) . '&';
875				} else {
876					// even though args are now decoded we still need to escape double quotes
877					$argValue = addcslashes($argValue, '"');
878
879					$ck_editor_plugin .= $argKey . '="' . $argValue . '" ';
880					$arg_str .= $argKey . '=' . $argValue . '&';
881				}
882			}
883		}
884		if (substr($ck_editor_plugin, -1) === ' ') {
885			$ck_editor_plugin = substr($ck_editor_plugin, 0, -1);
886		}
887		if (! empty($data)) {
888			$ck_editor_plugin .= ')}' . $data . '{' . TikiLib::strtoupper($name) . '}';
889		} else {
890			$ck_editor_plugin .= '}';
891		}
892		// work out if I'm a nested plugin and return empty if so
893		$stack = debug_backtrace();
894		$plugin_nest_level = 0;
895		foreach ($stack as $st) {
896			if ($st['function'] === 'parse_first') {
897				$plugin_nest_level ++;
898				if ($plugin_nest_level > 1) {
899					return '';
900				}
901			}
902		}
903		$arg_str = rtrim($arg_str, '&');
904		$icon = isset($info['icon']) ? $info['icon'] : 'img/icons/wiki_plugin_edit.png';
905
906		// some plugins are just too fragile to do wysiwyg, so show the "source" for them ;(
907		$excluded = ['tracker', 'trackerlist', 'trackerfilter', 'kaltura', 'toc', 'freetagged', 'draw', 'googlemap',
908			'include', 'module', 'list', 'custom_search', 'iframe', 'map', 'calendar', 'file', 'files', 'mouseover', 'sort',
909			'slideshow', 'convene', 'redirect', 'galleriffic'];
910
911		$ignore = null;
912		$enabled = $this->plugin_enabled($name, $ignore);
913		if (in_array($name, $excluded) || ! $enabled) {
914			$plugin_result = '&nbsp;&nbsp;&nbsp;&nbsp;' . $ck_editor_plugin;
915		} else {
916			if (! isset($info['format']) || $info['format'] !== 'html') {
917				$oldOptions = $this->option;
918				$plugin_result = $this->parse_data($plugin_result, ['is_html' => false, 'suppress_icons' => true, 'ck_editor' => true, 'noparseplugins' => true]);
919				$this->setOptions($oldOptions);
920				// reset the noparseplugins option, to allow for proper display in CkEditor
921				$this->option['noparseplugins'] = false;
922			} else {
923				$plugin_result = preg_replace('/~[\/]?np~/ms', '', $plugin_result);	// remove no parse tags otherwise they get nested later (bad)
924			}
925
926			if (! getCookie('wysiwyg_inline_edit', 'preview', false)) { // remove hrefs and onclicks
927				$plugin_result = preg_replace('/\shref\=/i', ' tiki_href=', $plugin_result);
928				$plugin_result = preg_replace('/\sonclick\=/i', ' tiki_onclick=', $plugin_result);
929				$plugin_result = preg_replace('/<script.*?<\/script>/mi', '', $plugin_result);
930				// remove hidden inputs
931				$plugin_result = preg_replace('/<input.*?type=[\'"]?hidden[\'"]?.*>/mi', '', $plugin_result);
932			}
933		}
934		if (! in_array($name, ['html'])) {		// remove <p> and <br>s from non-html
935			$data = str_replace(['<p>', '</p>', "\t"], '', $data);
936			$data = str_replace('<br />', "\n", $data);
937		}
938
939		if ($this->contains_html_block($plugin_result)) {
940			$elem = 'div';
941		} else {
942			$elem = 'span';
943		}
944		$elem_style = 'position:relative;display:inline-block;';
945		if (! $enabled) {
946			$elem_style .= 'opacity:0.3;';
947		}
948		if (in_array($name, ['img', 'div']) && preg_match('/<' . $name . '[^>]*style="(.*?)"/i', $plugin_result, $m)) {
949			if (count($m)) {
950				$elem_style .= $m[1];
951			}
952		}
953
954		$ret = '~np~<' . $elem . ' contenteditable="false" unselectable="on" class="tiki_plugin" data-plugin="' . $name . '" style="' . $elem_style . '"' .
955				' data-syntax="' . htmlentities($ck_editor_plugin, ENT_QUOTES, 'UTF-8') . '"' .
956				' data-args="' . htmlentities($arg_str, ENT_QUOTES, 'UTF-8') . '"' .
957				' data-body="' . htmlentities($data, ENT_QUOTES, 'UTF-8') . '">' . // not <!--{cke_protected}
958				'<img src="' . $icon . '" width="16" height="16" class="plugin_icon" />' .
959				$plugin_result . '<!-- end tiki_plugin --></' . $elem . '>~/np~';
960
961		return 	$ret;
962	}
963
964	function find_plugins($data, $name = null)
965	{
966		$parserlib = TikiLib::lib('parser');
967		$argumentParser = new WikiParser_PluginArgumentParser;
968		$matches = WikiParser_PluginMatcher::match($data);
969		$occurrences = [];
970		foreach ($matches as $match) {
971			$plugin = [
972				'name' => $match->getName(),
973				'arguments' => $argumentParser->parse($match->getArguments()),
974				'body' => $match->getBody(),
975			];
976
977			$dummy_output = '';
978			if ($parserlib->plugin_enabled($plugin['name'], $dummy_output)) {
979				if ($name === null || $plugin['name'] == $name) {
980					$occurrences[] = $plugin;
981				}
982			}
983		}
984		return $occurrences;
985	}
986
987	function process_save_plugins($data, array $context)
988	{
989		$parserlib = TikiLib::lib('parser');
990
991		$argumentParser = new WikiParser_PluginArgumentParser;
992
993		$matches = WikiParser_PluginMatcher::match($data);
994
995		foreach ($matches as $match) {
996			$plugin_name = $match->getName();
997			$body = $match->getBody();
998			$arguments = $argumentParser->parse($match->getArguments());
999
1000			$dummy_output = '';
1001			if ($parserlib->plugin_enabled($plugin_name, $dummy_output)) {
1002				$func_name = 'wikiplugin_' . $plugin_name . '_rewrite';
1003
1004				if (function_exists($func_name)) {
1005					$parserlib->plugin_apply_filters($plugin_name, $data, $arguments);
1006					$output = $func_name($body, $arguments, $context);
1007
1008					if ($output !== false) {
1009						$match->replaceWith($output);
1010					}
1011				}
1012
1013				if ($plugin_name == 'translationof') {
1014					$this->add_translationof_relation($data, $arguments, $context['itemId']);
1015				}
1016			}
1017		}
1018
1019		$matches_text = $matches->getText();
1020
1021		return $matches_text;
1022	}
1023
1024	//*
1025	protected function plugin_apply_filters($name, & $data, & $args)
1026	{
1027		$tikilib = TikiLib::lib('tiki');
1028
1029		$info = $this->plugin_info($name, $args);
1030
1031		$default = TikiFilter::get(isset($info['defaultfilter']) ? $info['defaultfilter'] : 'xss');
1032
1033		// Apply filters on the body
1034		$filter = isset($info['filter']) ? TikiFilter::get($info['filter']) : $default;
1035		//$data = TikiLib::htmldecode($data);		// jb 9.0 commented out in fix for html entitles
1036		$data = $filter->filter($data);
1037
1038		if (isset($this->option) && (! empty($this->option['is_html']) && (! $this->option['is_html']))) {
1039			$noparsed = ['data' => [], 'key' => []];
1040			$this->strip_unparsed_block($data, $noparsed);
1041			$data = str_replace(['<', '>'], ['&lt;', '&gt;'], $data);
1042			foreach ($noparsed['data'] as &$instance) {
1043				$instance = '~np~' . $instance . '~/np~';
1044			}
1045			unset($instance);
1046			$data = str_replace($noparsed['key'], $noparsed['data'], $data);
1047		}
1048
1049		// Make sure all arguments are declared
1050		if (isset($info['params'])) {
1051			$params = $info['params'];
1052		}
1053		$argsCopy = $args;
1054		if (! isset($info['extraparams']) && isset($params) && is_array($params)) {
1055			$args = array_intersect_key($args, $params);
1056		}
1057
1058		// Apply filters on values individually
1059		if (! empty($args)) {
1060			foreach ($args as $argKey => &$argValue) {
1061				if (! isset($params[$argKey])) {
1062					continue;// extra params
1063				}
1064				$paramInfo = $params[$argKey];
1065				$filter = isset($paramInfo['filter']) ? TikiFilter::get($paramInfo['filter']) : $default;
1066				$argValue = TikiLib::htmldecode($argValue);
1067
1068				if (isset($paramInfo['separator'])) {
1069					$vals = [];
1070
1071					$vals = $tikilib->array_apply_filter($tikilib->multi_explode($paramInfo['separator'], $argValue), $filter);
1072
1073					$argValue = array_values($vals);
1074				} else {
1075					$argValue = $filter->filter($argValue);
1076				}
1077			}
1078		}
1079	}
1080
1081	//*
1082	protected function convert_plugin_output($output, $from, $to)
1083	{
1084		if (! $output instanceof WikiParser_PluginOutput) {
1085			if ($from === 'wiki') {
1086				$output = WikiParser_PluginOutput::wiki($output);
1087			} elseif ($from === 'html') {
1088				$output = WikiParser_PluginOutput::html($output);
1089			}
1090		}
1091
1092		if ($to === 'html') {
1093			return $output->toHtml($this->option);
1094		} elseif ($to === 'wiki') {
1095			return $output->toWiki();
1096		}
1097	}
1098
1099	//*
1100	function plugin_replace_args($content, $rules, $args)
1101	{
1102		$patterns = [];
1103		$replacements = [];
1104
1105		foreach ($rules as $token => $info) {
1106			$patterns[] = "%$token%";
1107			if (isset($info['input']) && ! empty($info['input'])) {
1108				$token = $info['input'];
1109			}
1110
1111			if (isset($args[$token])) {
1112				$value = $args[$token];
1113			} else {
1114				$value = isset($info['default']) ? $info['default'] : '';
1115			}
1116
1117			switch (isset($info['encoding']) ? $info['encoding'] : 'none') {
1118				case 'html':
1119					$replacements[] = htmlentities($value, ENT_QUOTES, 'UTF-8');
1120					break;
1121				case 'url':
1122					$replacements[] = rawurlencode($value);
1123					break;
1124				default:
1125					$replacements[] = $value;
1126			}
1127		}
1128
1129		return str_replace($patterns, $replacements, $content);
1130	}
1131
1132	//*
1133	function plugin_is_editable($name)
1134	{
1135		global $tiki_p_edit, $prefs, $section;
1136		$info = $this->plugin_info($name);
1137		// note that for 3.0 the plugin editor only works in wiki pages, but could be extended later
1138		return $section == 'wiki page' && $info && $tiki_p_edit == 'y' && $prefs['wiki_edit_plugin'] == 'y'
1139			&& ! $this->plugin_is_inline($name);
1140	}
1141
1142	/**
1143	 * Gets a wiki parseable content and substitutes links for $oldName by
1144	 * links for $newName.
1145	 *
1146	 * @param string wiki parseable content
1147	 * @param string old page name
1148	 * @param string new page name
1149	 * @return string new wiki parseable content with links replaced
1150	 */
1151	function replace_links($data, $oldName, $newName)
1152	{
1153		global $prefs;
1154		$quotedOldName = preg_quote($oldName, '/');
1155		$semanticlib = TikiLib::lib('semantic');
1156
1157				// FIXME: Affects non-parsed sections
1158		foreach ($semanticlib->getAllTokens() as $sem) {
1159			$data = str_replace("($sem($oldName", "($sem($newName", $data);
1160		}
1161
1162		if ($prefs['feature_wikiwords'] == 'y') {
1163			if (strstr($newName, ' ')) {
1164				$data = preg_replace("/(?<= |\n|\t|\r|\,|\;|^)$quotedOldName(?= |\n|\t|\r|\,|\;|$)/", '((' . $newName . '))', $data);
1165			} else {
1166				$data = preg_replace("/(?<= |\n|\t|\r|\,|\;|^)$quotedOldName(?= |\n|\t|\r|\,|\;|$)/", $newName, $data);
1167			}
1168		}
1169
1170		$data = preg_replace("/(?<=\(\()$quotedOldName(?=\)\)|\|)/i", $newName, $data);
1171
1172		$quotedOldHtmlName = preg_quote(urlencode($oldName), '/');
1173
1174		$htmlSearch = '/<a class="wiki" href="tiki-index\.php\?page=' . $quotedOldHtmlName . '([^"]*)"/i';
1175		$htmlReplace = '<a class="wiki" href="tiki-index.php?page=' . urlencode($newName) . '\\1"';
1176		$data = preg_replace($htmlSearch, $htmlReplace, $data);
1177
1178		$htmlSearch = '/<a class="wiki" href="' . $quotedOldHtmlName . '"/i';
1179		$htmlReplace = '<a class="wiki" href="' . urlencode($newName) . '"';
1180		$data = preg_replace($htmlSearch, $htmlReplace, $data);
1181
1182		$htmlWantedSearch = '/(' . $quotedOldName . ')?<a class="wiki wikinew" href="tiki-editpage\.php\?page=' . $quotedOldHtmlName . '"[^<]+<\/a>/i';
1183		$data = preg_replace($htmlWantedSearch, '((' . $newName . '))', $data);
1184
1185		return $data;
1186	}
1187
1188	// Replace hotwords in given line
1189	//*
1190	function replace_hotwords($line, $words = null)
1191	{
1192		global $prefs;
1193
1194		if ($prefs['feature_hotwords'] == 'y') {
1195			$hotw_nw = ($prefs['feature_hotwords_nw'] == 'y') ? "target='_blank'" : '';
1196
1197			// FIXME: Replacements may fail if the value contains an unescaped metacharacters (which is why the default value contains escape characters). The value should probably be escaped with preg_quote().
1198			$sep = empty($prefs['feature_hotwords_sep']) ? " \n\t\r\,\;\(\)\.\:\[\]\{\}\!\?\"" : $prefs['feature_hotwords_sep'];
1199
1200			if (is_null($words)) {
1201				$words = $this->get_hotwords();
1202			}
1203			foreach ($words as $word => $url) {
1204				$escapedWord = preg_quote($word, '/');
1205
1206				/* In CVS revisions 1.429 and 1.373.2.40 of tikilib.php, mose added the following magic steps, commenting "fixed the hotwords autolinking in case it is in a description field".
1207				 * I do not understand what description fields this refers to. I do not know if this is correct and still needed. Step 1 seems to prevent replacements in an HTML tag. Chealer 2017-03-08
1208				 */
1209
1210				// Step 1: Insert magic sequences which will neutralize step 2 in some cases.
1211				$line = preg_replace("/(=(\"|')[^\"']*[$sep'])$escapedWord([$sep][^\"']*(\"|'))/i", "$1:::::$word,:::::$3", $line);
1212
1213				// Step 2: Add links where the hotword appears (not neutralized)
1214				$line = preg_replace("/([$sep']|^)$escapedWord($|[$sep])/i", "$1<a class=\"wiki\" href=\"$url\" $hotw_nw>$word</a>$2", $line);
1215
1216				// Step 3: Remove magic sequences inserted in step 1
1217				$line = preg_replace("/:::::$escapedWord,:::::/i", "$word", $line);
1218			}
1219		}
1220		return $line;
1221	}
1222
1223	// Make plain text URIs in text into clickable hyperlinks
1224	//	check to see if autolinks is enabled before calling this function ($prefs['feature_autolinks'] == "y")
1225	//*
1226	function autolinks($text)
1227	{
1228		if ($text) {
1229			global $prefs;
1230			static $mail_protect_pattern = '';
1231
1232			$attrib = '';
1233			if ($prefs['popupLinks'] == 'y') {
1234				$attrib .= 'target="_blank" ';
1235			}
1236			if ($prefs['feature_wiki_ext_icon'] == 'y') {
1237				$attrib .= 'class="wiki external" ';
1238				include_once(__DIR__ . '/../smarty_tiki/function.icon.php');
1239				$ext_icon = smarty_function_icon(['name' => 'link-external'], TikiLib::lib('smarty')->getEmptyInternalTemplate());
1240			} else {
1241				$attrib .= 'class="wiki" ';
1242				$ext_icon = "";
1243			}
1244
1245			// add a space so we can match links starting at the beginning of the first line
1246			$text = " " . $text;
1247			$patterns = [];
1248			$replacements = [];
1249
1250			// protocol://suffix
1251			$patterns[] = "#([\n ])([a-z0-9]+?)://([^<, \n\r]+)#i";
1252			$replacements[] = "\\1<a $attrib href=\"\\2://\\3\">\\2://\\3$ext_icon</a>";
1253
1254			// www.domain.ext/optionalpath
1255			$patterns[] = "#([\n ])www\.([a-z0-9\-]+)\.([a-z0-9\-.\~]+)((?:/[^,< \n\r]*)?)#i";
1256			$replacements[] = "\\1<a $attrib href=\"http://www.\\2.\\3\\4\">www.\\2.\\3\\4$ext_icon</a>";
1257
1258			// email address (foo@domain.ext)
1259			$patterns[] = "#([\n ])([a-z0-9\-_.]+?)@([\w\-]+\.([\w\-\.]+\.)*[\w]+)#i";
1260			if ($this->option['protect_email'] && $this->option['print'] !== 'y' && $prefs['feature_wiki_protect_email'] == 'y') {
1261				if (! $mail_protect_pattern) {
1262					$mail_protect_pattern = "\\1" . TikiLib::protect_email("\\2", "\\3");
1263				}
1264				$replacements[] = $mail_protect_pattern;
1265			} else {
1266				$replacements[] = "\\1<a class='wiki' href=\"mailto:\\2@\\3\">\\2@\\3</a>";
1267			}
1268
1269			$patterns[] = "#([\n ])magnet\:\?([^,< \n\r]+)#i";
1270			$replacements[] = "\\1<a class='wiki' href=\"magnet:?\\2\">magnet:?\\2</a>";
1271
1272			$text = preg_replace($patterns, $replacements, $text);
1273			// strip the space we added
1274			$text = substr($text, 1);
1275		}
1276
1277		return $text;
1278
1279		//		} else {
1280		//			return $text;
1281		//		}
1282	}
1283
1284	/**
1285	 * close_blocks - Close out open paragraph, lists, and div's
1286	 *
1287	 * During parse_data, information is kept on blocks of text (paragraphs, lists, divs)
1288	 * that need to be closed out. This function does that, rather than duplicating the
1289	 * code inline.
1290	 *
1291	 * @param	$data			- Output data
1292	 * @param	$in_paragraph		- TRUE if there is an open paragraph
1293	 * @param	$listbeg		- array of open list terminators
1294	 * @param	$divdepth		- array indicating how many div's are open
1295	 * @param	$close_paragraph	- TRUE if open paragraph should be closed.
1296	 * @param	$close_lists		- TRUE if open lists should be closed.
1297	 * @param	$close_divs		- TRUE if open div's should be closed.
1298	 */
1299	/* private */
1300	//*
1301	function close_blocks(&$data, &$in_paragraph, &$listbeg, &$divdepth, $close_paragraph, $close_lists, $close_divs)
1302	{
1303
1304		$closed = 0;	// Set to non-zero if something has been closed out
1305		// Close the paragraph if inside one.
1306		if ($close_paragraph && $in_paragraph) {
1307			$data .= "</p>\n";
1308			$in_paragraph = 0;
1309			$closed++;
1310		}
1311		// Close open lists
1312		if ($close_lists) {
1313			while (count($listbeg)) {
1314				$data .= array_shift($listbeg);
1315				$closed++;
1316			}
1317		}
1318
1319		// Close open divs
1320		if ($close_divs) {
1321			$temp_max = count($divdepth);
1322			for ($i = 1; $i <= $temp_max; $i++) {
1323				$data .= '</div>';
1324				$closed++;
1325			}
1326		}
1327
1328		return $closed;
1329	}
1330
1331	// Transitional wrapper over WikiParser_Parsable::parse()
1332	//PARSEDATA
1333	// options defaults : is_html => false, absolute_links => false, language => ''
1334	//*
1335	function parse_data($data, $option = [])
1336	{
1337		return (new WikiParser_Parsable($data))->parse($option);
1338	}
1339
1340	/**
1341	 * Used as preg_replace_callback methods within in the parse_data() method to escape color style attributes when
1342	 * generating HTML for the wiki color syntax - e.g. ~~#909:text~~
1343	 *
1344	 * @param $matches
1345	 * @return string
1346	 */
1347	protected function colorAttrEscape($matches)
1348	{
1349		$matches[1] = trim($matches[1]);
1350		$matches[3] = trim($matches[3]);
1351
1352		$esc = new Zend\Escaper\Escaper();
1353		$color = ! empty($matches[1]) ? 'color:' . str_replace('&#x23;', '#', $esc->escapeHtmlAttr($matches[1])) : '';
1354		$background = ! empty($matches[3]) ? 'background-color:' . str_replace('&#x23;', '#', $esc->escapeHtmlAttr($matches[3])) : '';
1355		$semi = ! empty($color) && ! empty($background) ? '; ' : '';
1356		$text = ! empty($matches[4]) ? $matches[4] : '';
1357		return '<span style="' . $color . $semi . $background . '">' . $text . '</span>';
1358	}
1359
1360	/**
1361	 *
1362	 * intended for use within wiki plugins. This option preserves opening and closing whitespace that renders into
1363	 * annoying <p> tags when parsed, and also respects HTML rendering preferences.
1364	 *
1365	 * @param $data string wiki/html to be parsed
1366	 * @param $optionsOverride array options to override the current defaults
1367	 * @param $inlineFirstP boolean If the returned data starts with a <p>, this option will force it to display as inline:block
1368	  *					useful when the returned data is required to display without adding overhead spacing caused by <p>
1369	 *
1370	 * @return string parsed data
1371	 */
1372	function parse_data_plugin($data, $inlineFirstP = false, $optionsOverride = [])
1373	{
1374		$options['is_html'] = ($GLOBALS['prefs']['feature_wiki_allowhtml'] === 'y' && $GLOBALS['info']['is_html'] == true) ? true : false;
1375
1376		foreach ($optionsOverride as $name => $value) {
1377			$options[$name] = $value;
1378		}
1379
1380		// record initial whitespace
1381		preg_match('(^\s*)', $data, $bwhite);
1382		preg_match('(\s*$)', $data, $ewhite);
1383
1384		// remove all the whitespace
1385		$data = trim($data);
1386		$data = $this->parse_data($data, $options);
1387		// remove whitespace that was added while parsing (yes it does happen)
1388		$data = trim($data);
1389
1390		if ($inlineFirstP) {
1391			$data = preg_replace('/^(\s*?)<p>/', '$1<p style="display:inline-block">', ' ' . $data);
1392		}
1393
1394		// add original whitespace back to preserve spacing
1395		return ($bwhite[0] . $data . $ewhite[0]);
1396	}
1397
1398	/** Simpler and faster parse than parse_data()
1399	 * This is only called from the parse Smarty modifier, for preference definitions.
1400	 */
1401	function parse_data_simple($data)
1402	{
1403		$data = $this->parse_data_wikilinks($data, true);
1404		$data = $this->parse_data_externallinks($data, true);
1405		$data = $this->parse_data_inline_syntax($data);
1406		if ($this->option['typography'] && ! $this->option['ck_editor']) {
1407			$data = typography($data, $this->option['language']);
1408		}
1409
1410		return $data;
1411	}
1412
1413	//*
1414	protected function parse_data_wikilinks($data, $simple_wiki, $ck_editor = false) //TODO: need a wikilink handler
1415	{
1416		global $page_regex, $prefs;
1417
1418		// definitively put out the protected words ))protectedWord((
1419		if ($prefs['feature_wikiwords'] == 'y') {
1420			preg_match_all("/\)\)(\S+?)\(\(/", $data, $matches);
1421			$noParseWikiLinksK = [];
1422			$noParseWikiLinksT = [];
1423			foreach ($matches[0] as $mi => $match) {
1424				do {
1425					$randNum = chr(0xff) . rand(0, 1048576) . chr(0xff);
1426				} while (strstr($data, $randNum));
1427				$data = str_replace($match, $randNum, $data);
1428				$noParseWikiLinksK[] = $randNum;
1429				$noParseWikiLinksT[] = $matches[1][$mi];
1430			}
1431		}
1432
1433		// Links with description
1434		preg_match_all("/\(([a-z0-9-]+)?\(($page_regex)\|([^\)]*?)\)\)/", $data, $pages);
1435
1436		$temp_max = count($pages[1]);
1437		for ($i = 0; $i < $temp_max; $i++) {
1438			$exactMatch = $pages[0][$i];
1439			$description = $pages[6][$i];
1440			$anchor = null;
1441
1442			if ($description && $description{0} == '#') {
1443				$temp = $description;
1444				$anchor = strtok($temp, '|');
1445				$description = strtok('|');
1446			}
1447
1448			$replacement = $this->get_wiki_link_replacement($pages[2][$i], ['description' => $description,'reltype' => $pages[1][$i],'anchor' => $anchor], $ck_editor);
1449
1450			$data = str_replace($exactMatch, $replacement, $data);
1451		}
1452
1453		// Wiki page syntax without description
1454		preg_match_all("/\(([a-z0-9-]+)?\( *($page_regex) *\)\)/", $data, $pages);
1455
1456		foreach ($pages[2] as $idx => $page_parse) {
1457			$exactMatch = $pages[0][$idx];
1458			$replacement = $this->get_wiki_link_replacement($page_parse, [ 'reltype' => $pages[1][$idx] ], $ck_editor);
1459
1460			$data = str_replace($exactMatch, $replacement, $data);
1461		}
1462
1463		// Links to internal pages
1464		// If they are parenthesized then don't treat as links
1465		// Prevent ))PageName(( from being expanded \"\'
1466		//[A-Z][a-z0-9_\-]+[A-Z][a-z0-9_\-]+[A-Za-z0-9\-_]*
1467		if ($prefs['feature_wiki'] == 'y' && $prefs['feature_wikiwords'] == 'y') {
1468			if (! $simple_wiki) {
1469				// The first part is now mandatory to prevent [Foo|MyPage] from being converted!
1470				if ($prefs['feature_wikiwords_usedash'] == 'y') {
1471					preg_match_all("/(?<=[ \n\t\r\,\;]|^)([A-Z][a-z0-9_\-\x80-\xFF]+[A-Z][a-z0-9_\-\x80-\xFF]+[A-Za-z0-9\-_\x80-\xFF]*)(?=$|[ \n\t\r\,\;\.])/", $data, $pages);
1472				} else {
1473					preg_match_all("/(?<=[ \n\t\r\,\;]|^)([A-Z][a-z0-9\x80-\xFF]+[A-Z][a-z0-9\x80-\xFF]+[A-Za-z0-9\x80-\xFF]*)(?=$|[ \n\t\r\,\;\.])/", $data, $pages);
1474				}
1475				//TODO to have a real utf8 Wikiword where the capitals can be a utf8 capital
1476				$words = ($prefs['feature_hotwords'] == 'y') ? $this->get_hotwords() : [];
1477				foreach (array_unique($pages[1]) as $page_parse) {
1478					if (! array_key_exists($page_parse, $words)) { // If this is not a hotword
1479						$repl = $this->get_wiki_link_replacement($page_parse, ['plural' => $prefs['feature_wiki_plurals'] == 'y'], $ck_editor);
1480
1481						$data = preg_replace("/(?<=[ \n\t\r\,\;]|^)$page_parse(?=$|[ \n\t\r\,\;\.])/", "$1" . $repl . "$2", $data);
1482					}
1483				}
1484			}
1485
1486			// Reinsert ))Words((
1487			$data = str_replace($noParseWikiLinksK, $noParseWikiLinksT, $data);
1488		}
1489
1490		return $data;
1491	}
1492
1493	protected function parse_data_externallinks($data, $suppress_icons = false)
1494	{
1495		global $prefs;
1496		$tikilib = TikiLib::lib('tiki');
1497
1498		// *****
1499		// This section handles external links of the form [url] and such.
1500		// *****
1501
1502		$links = $tikilib->get_links($data);
1503		$notcachedlinks = $tikilib->get_links_nocache($data);
1504		$cachedlinks = array_diff($links, $notcachedlinks);
1505		$tikilib->cache_links($cachedlinks);
1506
1507		// Note that there're links that are replaced
1508		foreach ($links as $link) {
1509			$target = '';
1510			$class = 'class="wiki"';
1511			$ext_icon = '';
1512			$rel = '';
1513
1514			if ($prefs['popupLinks'] == 'y') {
1515				$target = 'target="_blank"';
1516			}
1517			if (! strstr($link, '://')) {
1518				$target = '';
1519			} else {
1520				$class = 'class="wiki external"';
1521				if ($prefs['feature_wiki_ext_icon'] == 'y' && ! ($this->option['suppress_icons'] || $suppress_icons)) {
1522					$smarty = TikiLib::lib('smarty');
1523					include_once('lib/smarty_tiki/function.icon.php');
1524					$ext_icon = smarty_function_icon(['name' => 'link-external'], $smarty->getEmptyInternalTemplate());
1525				}
1526				$rel = 'external';
1527				if ($prefs['feature_wiki_ext_rel_nofollow'] == 'y') {
1528					$rel .= ' nofollow';
1529				}
1530			}
1531
1532			// The (?<!\[) stuff below is to give users an easy way to
1533			// enter square brackets in their output; things like [[foo]
1534			// get rendered as [foo]. -rlpowell
1535
1536			if ($prefs['cachepages'] == 'y' && $tikilib->is_cached($link)) {
1537				//use of urlencode for using cached versions of dynamic sites
1538				$cosa = "<a class=\"wikicache\" target=\"_blank\" href=\"tiki-view_cache.php?url=" . urlencode($link) . "\">(cache)</a>";
1539
1540				$link2 = str_replace("/", "\/", preg_quote($link));
1541				$pattern = "/(?<!\[)\[$link2\|([^\]\|]+)\|([^\]\|]+)\|([^\]]+)\]/"; //< last param expected here is always nocache
1542				$data = preg_replace($pattern, "<a $class $target href=\"$link\" rel=\"$2 $rel\">$1</a>$ext_icon", $data);
1543				$pattern = "/(?<!\[)\[$link2\|([^\]\|]+)\|([^\]]+)\]/";//< last param here ($2) is used for relation (rel) attribute (e.g. shadowbox) or nocache
1544				preg_match($pattern, $data, $matches);
1545				if (isset($matches[2]) && $matches[2] == 'nocache') {
1546					$data = preg_replace($pattern, "<a $class $target href=\"$link\" rel=\"$rel\">$1</a>$ext_icon", $data);
1547				} else {
1548					$data = preg_replace($pattern, "<a $class $target href=\"$link\" rel=\"$rel\" data-box=\"$2\">$1</a>$ext_icon $cosa", $data);
1549				}
1550				$pattern = "/(?<!\[)\[$link2\|([^\]\|]+)\]/";
1551				$data = preg_replace($pattern, "<a $class $target href=\"$link\" rel=\"$rel\">$1</a>$ext_icon $cosa", $data);
1552				$pattern = "/(?<!\[)\[$link2\]/";
1553				$data = preg_replace($pattern, "<a $class $target href=\"$link\" rel=\"$rel\">$link</a>$ext_icon $cosa", $data);
1554			} else {
1555				$link2 = str_replace("/", "\/", preg_quote($link));
1556				$link = trim($link);
1557				$link = str_replace('"', '%22', $link);
1558				$data = str_replace("|nocache", "", $data);
1559
1560				$pattern = "/(?<!\[)\[$link2\|([^\]\|]+)\|([^\]]+)\]/";
1561				$data = preg_replace_callback($pattern, function ($matches) use ($class, $target, $link, $rel, $ext_icon) {
1562					return "<a $class $target href=\"$link\" rel=\"$rel\" data-box=\"" . str_replace('"', '%22', $matches[1]) . "\">{$matches[1]}</a>$ext_icon";
1563				}, $data);
1564				$pattern = "/(?<!\[)\[$link2\|([^\]\|]+)([^\]])*\]/";
1565				$data = preg_replace($pattern, "<a $class $target href=\"$link\" rel=\"$rel\">$1</a>$ext_icon", $data);
1566				$pattern = "/(?<!\[)\[$link2\|?\]/";
1567				$data = preg_replace($pattern, "<a $class $target href=\"$link\" rel=\"$rel\">$link</a>$ext_icon", $data);
1568			}
1569		}
1570
1571		// Handle double square brackets. to display [foo] use [[foo] -rlpowell. Improved by sylvieg to avoid replacing them in [[code]] cases.
1572		if (empty($this->option['process_double_brackets']) || $this->option['process_double_brackets'] != 'n') {
1573			$data = preg_replace("/\[\[([^\]]*)\](?!\])/", "[$1]", $data);
1574			$data = preg_replace("/\[\[([^\]]*)$/", "[$1", $data);
1575		}
1576
1577		return $data;
1578	}
1579
1580	//*
1581	protected function parse_data_inline_syntax($line, $words = null, $ck_editor = false)
1582	{
1583		global $prefs;
1584
1585		// Replace monospaced text
1586		$line = preg_replace("/(^|\s)-\+(.*?)\+-/", "$1<code>$2</code>", $line);
1587		// Replace bold text
1588		$line = preg_replace("/__(.*?)__/", "<strong>$1</strong>", $line);
1589		// Replace italic text
1590		$line = preg_replace("/\'\'(.*?)\'\'/", "<em>$1</em>", $line);
1591
1592		if (! $ck_editor) {
1593			if ($prefs['feature_hotwords'] == 'y') {
1594				// Replace Hotwords before begin
1595				$line = $this->replace_hotwords($line, $words);
1596			}
1597
1598			// Make plain URLs clickable hyperlinks
1599			if ($prefs['feature_autolinks'] == 'y') {
1600				$line = $this->autolinks($line);
1601			}
1602		}
1603
1604		if (! $ck_editor) {
1605			// Replace definition lists
1606			$line = preg_replace("/^;([^:]*):([^\/\/].*)/", "<dl><dt>$1</dt><dd>$2</dd></dl>", $line);
1607			$line = preg_replace("/^;(<a [^<]*<\/a>):([^\/\/].*)/", "<dl><dt>$1</dt><dd>$2</dd></dl>", $line);
1608		}
1609
1610		return $line;
1611	}
1612
1613	//*
1614	protected function parse_data_tables($data)
1615	{
1616		global $prefs;
1617
1618		// pretty trackers use pipe for output|template specification, so we need to escape
1619		$data = preg_replace('/{\$f_(\w+)\|(output|template:.*?)}/i', '{\$f_$1-escapedpipe-$2}', $data);
1620
1621		/*
1622		 * Wiki Tables syntax
1623		 */
1624		// tables in old style
1625		if ($prefs['feature_wiki_tables'] != 'new') {
1626			if (preg_match_all("/\|\|(.*)\|\|/", $data, $tables)) {
1627				$maxcols = 1;
1628				$cols = [];
1629				$temp_max = count($tables[0]);
1630				for ($i = 0; $i < $temp_max; $i++) {
1631					$rows = explode('||', $tables[0][$i]);
1632					$temp_max2 = count($rows);
1633					for ($j = 0; $j < $temp_max2; $j++) {
1634						$cols[$i][$j] = explode('|', $rows[$j]);
1635						if (count($cols[$i][$j]) > $maxcols) {
1636							$maxcols = count($cols[$i][$j]);
1637						}
1638					}
1639				} // for ($i ...
1640
1641				$temp_max3 = count($tables[0]);
1642				for ($i = 0; $i < $temp_max3; $i++) {
1643					$repl = '<table class="wikitable table table-striped table-hover">';
1644
1645					$temp_max4 = count($cols[$i]);
1646					for ($j = 0; $j < $temp_max4; $j++) {
1647						$ncols = count($cols[$i][$j]);
1648
1649						if ($ncols == 1 && ! $cols[$i][$j][0]) {
1650							continue;
1651						}
1652
1653						$repl .= '<tr>';
1654
1655						for ($k = 0; $k < $ncols; $k++) {
1656							$repl .= '<td class="wikicell" ';
1657
1658							if ($k == $ncols - 1 && $ncols < $maxcols) {
1659								$repl .= ' colspan="' . ($maxcols - $k) . '"';
1660							}
1661
1662							$repl .= '>' . $cols[$i][$j][$k] . '</td>';
1663						} // // for ($k ...
1664
1665						$repl .= '</tr>';
1666					} // for ($j ...
1667
1668					$repl .= '</table>';
1669					$data = str_replace($tables[0][$i], $repl, $data);
1670				} // for ($i ...
1671			} // if (preg_match_all("/\|\|(.*)\|\|/", $data, $tables))
1672		} else {
1673			// New syntax for tables
1674			// REWRITE THIS CODE
1675			if (preg_match_all("/\|\|(.*?)\|\|/s", $data, $tables)) {
1676				$maxcols = 1;
1677				$cols = [];
1678				$temp_max5 = count($tables[0]);
1679				for ($i = 0; $i < $temp_max5; $i++) {
1680					$rows = preg_split("/(\n|\<br\/\>)/", $tables[0][$i]);
1681					$col[$i] = [];
1682					$temp_max6 = count($rows);
1683					for ($j = 0; $j < $temp_max6; $j++) {
1684						$rows[$j] = str_replace('||', '', $rows[$j]);
1685						$cols[$i][$j] = explode('|', $rows[$j]);
1686						if (count($cols[$i][$j]) > $maxcols) {
1687							$maxcols = count($cols[$i][$j]);
1688						}
1689					}
1690				}
1691
1692				$temp_max7 = count($tables[0]);
1693				for ($i = 0; $i < $temp_max7; $i++) {
1694					$repl = '<table class="wikitable table table-striped table-hover">';
1695					$temp_max8 = count($cols[$i]);
1696					for ($j = 0; $j < $temp_max8; $j++) {
1697						$ncols = count($cols[$i][$j]);
1698
1699						if ($ncols == 1 && ! $cols[$i][$j][0]) {
1700							continue;
1701						}
1702
1703						$repl .= '<tr>';
1704
1705						for ($k = 0; $k < $ncols; $k++) {
1706							$repl .= '<td class="wikicell" ';
1707							if ($k == $ncols - 1 && $ncols < $maxcols) {
1708								$repl .= ' colspan="' . ($maxcols - $k) . '"';
1709							}
1710
1711							$repl .= '>' . $cols[$i][$j][$k] . '</td>';
1712						}
1713						$repl .= '</tr>';
1714					}
1715					$repl .= '</table>';
1716					$data = str_replace($tables[0][$i], $repl, $data);
1717				}
1718			}
1719		}
1720
1721		// unescape the pipes for pretty tracker
1722		$data = preg_replace('/{\$f_(\w+)-escapedpipe-(output|template:.*?)}/i', '{\$f_$1|$2}', $data);
1723
1724		return $data;
1725	}
1726
1727	//*
1728	function parse_wiki_argvariable(&$data)
1729	{
1730		global $prefs, $user;
1731		$tikilib = TikiLib::lib('tiki');
1732		$smarty = TikiLib::lib('smarty');
1733
1734		if ($prefs['feature_wiki_argvariable'] == 'y' && ! $this->option['ck_editor']) {
1735			if (preg_match_all("/\\{\\{((\w+)(\\|([^\\}]*))?)\\}\\}/", $data, $args, PREG_SET_ORDER)) {
1736				$needles = [];
1737				$replacements = [];
1738
1739				foreach ($args as $arg) {
1740					$value = isset($arg[4]) ? $arg[4] : '';
1741					$name = $arg[2];
1742					switch ($name) {
1743						case 'user':
1744							$value = $user;
1745							break;
1746						case 'page':
1747							$value = $this->option['page'];
1748							break;
1749						case 'pageid':
1750							if ($_REQUEST['page'] != null) {
1751								$value = $tikilib->get_page_id_from_name($_REQUEST['page']);
1752								break;
1753							} else {
1754								$value = '';
1755								break;
1756							}
1757						case 'domain':
1758							if ($smarty->getTemplateVars('url_host') != null) {
1759								$value = $smarty->getTemplateVars('url_host');
1760								break;
1761							} else {
1762								$value = '';
1763								break;
1764							}
1765						case 'domainslash':
1766							if ($smarty->getTemplateVars('url_host') != null) {
1767								$value = $smarty->getTemplateVars('url_host') . '/';
1768								break;
1769							} else {
1770								$value = '';
1771								break;
1772							}
1773						case 'domainslash_if_multitiki':
1774							if (is_file('db/virtuals.inc')) {
1775								$virtuals = array_map('trim', file('db/virtuals.inc'));
1776							}
1777							if ($virtuals && $smarty->getTemplateVars('url_host') != null) {
1778								$value = $smarty->getTemplateVars('url_host') . '/';
1779								break;
1780							} else {
1781								$value = '';
1782								break;
1783							}
1784						case 'lastVersion':
1785							$histlib = TikiLib::lib('hist');
1786							// get_page_history arguments: page name, page contents (set to "false" to save memory), history_offset (none, therefore "0"), max. records (just one for this case);
1787							$history = $histlib->get_page_history($this->option['page'], false, 0, 1);
1788							if ($history[0]['version'] != null) {
1789								$value = $history[0]['version'];
1790								break;
1791							} else {
1792								$value = '';
1793								break;
1794							}
1795						case 'lastAuthor':
1796							$histlib = TikiLib::lib('hist');
1797
1798							// get_page_history arguments: page name, page contents (set to "false" to save memory), history_offset (none, therefore "0"), max. records (just one for this case);
1799							$history = $histlib->get_page_history($this->option['page'], false, 0, 1);
1800							if ($history[0]['user'] != null) {
1801								if ($prefs['user_show_realnames'] == 'y') {
1802									$value = TikiLib::lib('user')->clean_user($history[0]['user']);
1803									break;
1804								} else {
1805									$value = $history[0]['user'];
1806									break;
1807								}
1808							} else {
1809								$value = '';
1810								break;
1811							}
1812						case 'lastModif':
1813							$histlib = TikiLib::lib('hist');
1814							// get_page_history arguments: page name, page contents (set to "false" to save memory), history_offset (none, therefore "0"), max. records (just one for this case);
1815							$history = $histlib->get_page_history($this->option['page'], false, 0, 1);
1816							if ($history[0]['lastModif'] != null) {
1817								$value = $tikilib->get_short_datetime($history[0]['lastModif']);
1818								break;
1819							} else {
1820								$value = '';
1821								break;
1822							}
1823						case 'lastItemVersion':
1824							$trklib = TikiLib::lib('trk');
1825							$auto_query_args = ['itemId'];
1826							if (! empty($_REQUEST['itemId'])) {
1827								$item_info = $trklib->get_item_info($_REQUEST['itemId']);
1828								$itemObject = Tracker_Item::fromInfo($item_info);
1829								if (! $itemObject->canView()) {
1830									$smarty->assign('errortype', 401);
1831									$smarty->assign('msg', tra('You do not have permission to view this information from this tracker.'));
1832									$smarty->display('error.tpl');
1833									die;
1834								}
1835								$fieldId = empty($_REQUEST['fieldId']) ? 0 : $_REQUEST['fieldId'];
1836								$filter = [];
1837								if (! empty($_REQUEST['version'])) {
1838									$filter['version'] = $_REQUEST['version'];
1839								}
1840								$offset = empty($_REQUEST['offset']) ? 0 : $_REQUEST['offset'];
1841								$history = $trklib->get_item_history($item_info, $fieldId, $filter, $offset, $prefs['maxRecords']);
1842
1843								$value = $history['data'][0]['version'];
1844								break;
1845							} else {
1846								$value = '';
1847								break;
1848							}
1849						case 'lastItemAuthor':
1850							$trklib = TikiLib::lib('trk');
1851							$auto_query_args = ['itemId'];
1852							if (! empty($_REQUEST['itemId'])) {
1853								$item_info = $trklib->get_item_info($_REQUEST['itemId']);
1854								$itemObject = Tracker_Item::fromInfo($item_info);
1855								if (! $itemObject->canView()) {
1856									$smarty->assign('errortype', 401);
1857									$smarty->assign('msg', tra('You do not have permission to view this information from this tracker.'));
1858									$smarty->display('error.tpl');
1859									die;
1860								}
1861								if ($item_info['lastModifBy'] != null) {
1862									if ($prefs['user_show_realnames'] == 'y') {
1863										$value = TikiLib::lib('user')->clean_user($item_info['lastModifBy']);
1864										break;
1865									} else {
1866										$value = $item_info['lastModifBy'];
1867										break;
1868									}
1869								}
1870								break;
1871							} else {
1872								$value = '';
1873								break;
1874							}
1875						case 'lastItemModif':
1876							$trklib = TikiLib::lib('trk');
1877							$auto_query_args = ['itemId'];
1878							if (! empty($_REQUEST['itemId'])) {
1879								$item_info = $trklib->get_item_info($_REQUEST['itemId']);
1880								$itemObject = Tracker_Item::fromInfo($item_info);
1881								if (! $itemObject->canView()) {
1882									$smarty->assign('errortype', 401);
1883									$smarty->assign('msg', tra('You do not have permission to view this information from this tracker.'));
1884									$smarty->display('error.tpl');
1885									die;
1886								}
1887								$value = $tikilib->get_short_datetime($item_info['lastModif']);
1888								break;
1889							} else {
1890								$value = '';
1891								break;
1892							}
1893						case 'lastApprover':
1894							global $prefs, $user;
1895							$tikilib = TikiLib::lib('tiki');
1896
1897							if ($prefs['flaggedrev_approval'] == 'y') {
1898								$flaggedrevisionlib = TikiLib::lib('flaggedrevision');
1899
1900								if ($flaggedrevisionlib->page_requires_approval($this->option['page'])) {
1901									if ($version_info = $flaggedrevisionlib->get_version_with($this->option['page'], 'moderation', 'OK')) {
1902										if ($this->content_to_render === null) {
1903											$revision_displayed = $version_info['version'];
1904											$approval = $flaggedrevisionlib->find_approval_information($this->option['page'], $revision_displayed);
1905										}
1906									}
1907								}
1908							}
1909
1910							if (! empty($approval['user'])) {
1911								if ($prefs['user_show_realnames'] == 'y') {
1912									$value = TikiLib::lib('user')->clean_user($approval['user']);
1913									break;
1914								} else {
1915									$value = $approval['user'];
1916									break;
1917								}
1918							} else {
1919								$value = '';
1920								break;
1921							}
1922						case 'lastApproval':
1923							global $prefs, $user;
1924							$tikilib = TikiLib::lib('tiki');
1925
1926							if ($prefs['flaggedrev_approval'] == 'y') {
1927								$flaggedrevisionlib = TikiLib::lib('flaggedrevision');
1928
1929								if ($flaggedrevisionlib->page_requires_approval($this->option['page'])) {
1930									if ($version_info = $flaggedrevisionlib->get_version_with($this->option['page'], 'moderation', 'OK')) {
1931										if ($this->content_to_render === null) {
1932											$revision_displayed = $version_info['version'];
1933											$approval = $flaggedrevisionlib->find_approval_information($this->option['page'], $revision_displayed);
1934										}
1935									}
1936								}
1937							}
1938
1939							if ($approval['lastModif'] != null) {
1940								$value = $tikilib->get_short_datetime($approval['lastModif']);
1941								break;
1942							} else {
1943								$value = '';
1944								break;
1945							}
1946						case 'lastApprovedVersion':
1947							global $prefs, $user;
1948							$tikilib = TikiLib::lib('tiki');
1949
1950							if ($prefs['flaggedrev_approval'] == 'y') {
1951								$flaggedrevisionlib = TikiLib::lib('flaggedrevision');
1952
1953								if ($flaggedrevisionlib->page_requires_approval($this->option['page'])) {
1954									$version_info = $flaggedrevisionlib->get_version_with($this->option['page'], 'moderation', 'OK');
1955								}
1956							}
1957
1958							if ($version_info['version'] != null) {
1959								$value = $version_info['version'];
1960								break;
1961							} else {
1962								$value = '';
1963								break;
1964							}
1965						case 'currentVersion':
1966							if (isset($_REQUEST['preview'])) {
1967								$value = (int)$_REQUEST["preview"];
1968								break;
1969							} elseif (isset($_REQUEST['version'])) {
1970								$value = (int)$_REQUEST["version"];
1971								break;
1972							} elseif ($prefs['flaggedrev_approval'] == 'y' && ! isset($_REQUEST['latest'])) {
1973								$flaggedrevisionlib = TikiLib::lib('flaggedrevision');
1974
1975								if ($flaggedrevisionlib->page_requires_approval($this->option['page'])) {
1976									$version_info = $flaggedrevisionlib->get_version_with($this->option['page'], 'moderation', 'OK');
1977								}
1978								if ($version_info['version'] != null) {
1979									$value = $version_info['version'];
1980									break;
1981								}
1982							} else {
1983								$histlib = TikiLib::lib('hist');
1984								// get_page_history arguments: page name, page contents (set to "false" to save memory), history_offset (none, therefore "0"), max. records (just one for this case);
1985								$history = $histlib->get_page_history($this->option['page'], false, 0, 1);
1986								if ($history[0]['version'] != null) {
1987									$value = $history[0]['version'];
1988									break;
1989								} else {
1990									$value = '';
1991									break;
1992								}
1993							}
1994						case 'currentVersionApprover':
1995							global $prefs, $user;
1996							$tikilib = TikiLib::lib('tiki');
1997
1998							if ($prefs['flaggedrev_approval'] == 'y') {
1999								$flaggedrevisionlib = TikiLib::lib('flaggedrevision');
2000								if ($flaggedrevisionlib->page_requires_approval($this->option['page'])) {
2001									if ($versions_info = $flaggedrevisionlib->get_versions_with($this->option['page'], 'moderation', 'OK')) {
2002										if (isset($_REQUEST['preview'])) {
2003											$revision_displayed = (int)$_REQUEST["preview"];
2004										} elseif (isset($_REQUEST['version'])) {
2005											$revision_displayed = (int)$_REQUEST["version"];
2006										} elseif (isset($_REQUEST['latest'])) {
2007											$revision_displayed = null;
2008										} else {
2009											$versions_info = $flaggedrevisionlib->get_versions_with($this->option['page'], 'moderation', 'OK');
2010											$revision_displayed = $versions_info[0];
2011										}
2012
2013										if ($this->content_to_render === null) {
2014											$approval = $flaggedrevisionlib->find_approval_information($this->option['page'], $revision_displayed);
2015										}
2016									}
2017								}
2018							}
2019
2020							if (! empty($approval['user'])) {
2021								if ($prefs['user_show_realnames'] == 'y') {
2022									$value = TikiLib::lib('user')->clean_user($approval['user']);
2023									break;
2024								} else {
2025									$value = $approval['user'];
2026									break;
2027								}
2028							} else {
2029								$value = '';
2030								break;
2031							}
2032						case 'currentVersionApproval':
2033							global $prefs, $user;
2034							$tikilib = TikiLib::lib('tiki');
2035
2036							if ($prefs['flaggedrev_approval'] == 'y') {
2037								$flaggedrevisionlib = TikiLib::lib('flaggedrevision');
2038
2039								if ($flaggedrevisionlib->page_requires_approval($this->option['page'])) {
2040									if ($versions_info = $flaggedrevisionlib->get_versions_with($this->option['page'], 'moderation', 'OK')) {
2041										if (isset($_REQUEST['preview'])) {
2042											$revision_displayed = (int)$_REQUEST["preview"];
2043										} elseif (isset($_REQUEST['version'])) {
2044											$revision_displayed = (int)$_REQUEST["version"];
2045										} elseif (isset($_REQUEST['latest'])) {
2046											$revision_displayed = null;
2047										} else {
2048											$versions_info = $flaggedrevisionlib->get_versions_with($this->option['page'], 'moderation', 'OK');
2049											$revision_displayed = $versions_info[0];
2050										}
2051
2052										if ($this->content_to_render === null) {
2053											$approval = $flaggedrevisionlib->find_approval_information($this->option['page'], $revision_displayed);
2054										}
2055									}
2056								}
2057							}
2058
2059							if ($approval['lastModif'] != null) {
2060								$value = $tikilib->get_short_datetime($approval['lastModif']);
2061								break;
2062							} else {
2063								$value = '';
2064								break;
2065							}
2066						case 'currentVersionApproved':
2067							global $prefs, $user;
2068							$tikilib = TikiLib::lib('tiki');
2069
2070							if ($prefs['flaggedrev_approval'] == 'y') {
2071								$flaggedrevisionlib = TikiLib::lib('flaggedrevision');
2072
2073								if ($flaggedrevisionlib->page_requires_approval($this->option['page'])) {
2074									//$versions_info = $flaggedrevisionlib->get_versions_with($this->option['page'], 'moderation', 'OK');
2075									if (isset($_REQUEST['preview'])) {
2076										$revision_displayed = (int)$_REQUEST["preview"];
2077									} elseif (isset($_REQUEST['version'])) {
2078										$revision_displayed = (int)$_REQUEST["version"];
2079									} elseif (isset($_REQUEST['latest'])) {
2080										$revision_displayed = null;
2081									} else {
2082										$versions_info = $flaggedrevisionlib->get_versions_with($this->option['page'], 'moderation', 'OK');
2083										$revision_displayed = $versions_info[0];
2084									}
2085								}
2086							}
2087
2088							if ($revision_displayed != null && $approval = $flaggedrevisionlib->find_approval_information($this->option['page'], $revision_displayed)) {
2089								$value = tr("yes");
2090								break;
2091							} else {
2092								$value = tr("no");
2093								break;
2094							}
2095						case 'cat':
2096							if (empty($_GET['cat']) && ! empty($_REQUEST['organicgroup']) && ! empty($this->option['page'])) {
2097								$utilities = new \Tiki\Package\Extension\Utilities();
2098								if ($folder = $utilities->getFolderFromObject('wiki page', $this->option['page'])) {
2099									$ogname = $folder . '_' . $_REQUEST['organicgroup'];
2100									$cat = TikiLib::lib('categ')->get_category_id($ogname);
2101									$value = $cat;
2102								} else {
2103									$value = '';
2104								}
2105							} elseif (! empty($_GET['cat'])) {
2106								$value = $_GET['cat'];
2107							} else {
2108								$value = '';
2109							}
2110							break;
2111						default:
2112							if (isset($_GET[$name])) {
2113								$value = $_GET[$name];
2114							} else {
2115								$value = '';
2116								include_once('lib/wiki-plugins/wikiplugin_showpref.php');
2117								if ($prefs['wikiplugin_showpref'] == 'y' && $showpref = wikiplugin_showpref('', ['pref' => $name])) {
2118									$value = $showpref;
2119								}
2120							}
2121							break;
2122					}
2123
2124					$needles[] = $arg[0];
2125					$replacements[] = $value;
2126				}
2127				$data = str_replace($needles, $replacements, $data);
2128			}
2129		}
2130	}
2131
2132	//*
2133	protected function parse_data_dynamic_variables($data, $lang = null)
2134	{
2135		global $tiki_p_edit_dynvar, $prefs;
2136
2137		$enclose = '%';
2138		if ($prefs['wiki_dynvar_style'] == 'disable') {
2139			return $data;
2140		} elseif ($prefs['wiki_dynvar_style'] == 'double') {
2141			$enclose = '%%';
2142		}
2143
2144		// Replace dynamic variables
2145		// Dynamic variables are similar to dynamic content but they are editable
2146		// from the page directly, intended for short data, not long text but text
2147		// will work too
2148		//     Now won't match HTML-style '%nn' letter codes and some special utf8 situations...
2149		if (preg_match_all("/[^%]$enclose([^% 0-9A-Z][^% 0-9A-Z][^% ]*){$enclose}[^%]/", $data, $dvars)) {
2150			// remove repeated elements
2151			$dvars = array_unique($dvars[1]);
2152			// Now replace each dynamic variable by a pair composed of the
2153			// variable value and a text field to edit the variable. Each
2154			foreach ($dvars as $dvar) {
2155				$value = $this->get_dynamic_variable($dvar, $lang);
2156				//replace backslash with html entity to avoid losing backslashes in the preg_replace function below
2157				$value = str_replace('\\', '&bsol;', $value);
2158				// Now build 2 divs
2159				$id = 'dyn_' . $dvar;
2160
2161				if (isset($tiki_p_edit_dynvar)&& $tiki_p_edit_dynvar == 'y') {
2162					$span1 = "<span style='display:inline;' id='dyn_" . $dvar . "_display'><a class='dynavar' onclick='javascript:toggle_dynamic_var(\"$dvar\");' title='" . tra('Click to edit dynamic variable', '', true) . ": $dvar'>$value</a></span>";
2163					$span2 = "<span style='display:none;' id='dyn_" . $dvar . "_edit'><input type='text' class='input-sm' name='dyn_" . $dvar . "' value='" . $value . "' />" . '<input type="submit" class="btn btn-primary btn-sm" name="_dyn_update" value="' . tra('Update variable', '', true) . '"/></span>';
2164				} else {
2165					$span1 = "<span class='dynavar' style='display:inline;' id='dyn_" . $dvar . "_display'>$value</span>";
2166					$span2 = '';
2167				}
2168				$html = $span1 . $span2;
2169				//It's important to replace only once
2170				$dvar_preg = preg_quote($dvar);
2171				$data = preg_replace("+$enclose$dvar_preg$enclose+", $html, $data, 1);
2172				//Further replacements only with the value
2173				$data = str_replace("$enclose$dvar$enclose", $value, $data);
2174			}
2175		}
2176
2177		return $data;
2178	}
2179
2180	//*
2181	private function get_dynamic_variable($name, $lang = null)
2182	{
2183		$tikilib = TikiLib::lib('tiki');
2184		$result = $tikilib->table('tiki_dynamic_variables')->fetchAll(['data', 'lang'], ['name' => $name]);
2185
2186		$value = tr('No value assigned');
2187
2188		foreach ($result as $row) {
2189			if ($row['lang'] == $lang) {
2190				// Exact match
2191				return $row['data'];
2192			} elseif (empty($row['lang'])) {
2193				// Universal match, keep in case no exact match
2194				$value = $row['data'];
2195			}
2196		}
2197
2198		return $value;
2199	}
2200
2201	/* This is only called by parse_data(). It does not just deal with TOC-s. */
2202	protected function parse_data_process_maketoc(&$data, $noparsed)
2203	{
2204
2205		global $prefs;
2206		$tikilib = TikiLib::lib('tiki');
2207
2208		// $this->makeTocCount++; Unused since Tiki 12 or earlier
2209
2210		if ($this->option['ck_editor']) {
2211			$need_maketoc = false ;
2212		} else {
2213			$need_maketoc = preg_match('/\{maketoc.*\}/', $data);
2214		}
2215
2216		// Wysiwyg or allowhtml mode {maketoc} handling when not in editor mode (i.e. viewing)
2217		if ($need_maketoc && $this->option['is_html']) {
2218			// Header needs to start at beginning of line (wysiwyg does not necessary obey)
2219			$data = $this->unprotectSpecialChars($data, true);
2220			$data = preg_replace('/<\/([a-z]+)><h([1-6])>/im', "</\\1>\n<h\\2>", $data);
2221			$data = preg_replace('/^\s+<h([1-6])>/im', "<h\\1>", $data); // headings with leading spaces
2222			$data = preg_replace('/\/><h([1-6])>/im', "/>\n<h\\1>", $data); // headings after /> tag
2223			$htmlheadersearch = '/<h([1-6])>\s*([^<]+)\s*<\/h[1-6]>/im';
2224			preg_match_all($htmlheadersearch, $data, $htmlheaders);
2225			$nbhh = count($htmlheaders[1]);
2226			for ($i = 0; $i < $nbhh; $i++) {
2227				$htmlheaderreplace = '';
2228				for ($j = 0; $j < $htmlheaders[1][$i]; $j++) {
2229					$htmlheaderreplace .= '!';
2230				}
2231				$htmlheaderreplace .= $htmlheaders[2][$i];
2232				$data = str_replace($htmlheaders[0][$i], $htmlheaderreplace, $data);
2233			}
2234			$data = $this->protectSpecialChars($data, true);
2235		}
2236
2237		$need_autonumbering = ( preg_match('/^\!+[\-\+]?#/m', $data) > 0 );
2238
2239		$anch = [];
2240		global $anch;
2241		$pageNum = 1;
2242
2243		// Now tokenize the expression and process the tokens
2244		// Use tab and newline as tokenizing characters as well  ////
2245		$lines = explode("\n", $data);
2246		if (empty($lines[count($lines) - 1]) && empty($lines[count($lines) - 2])) {
2247			array_pop($lines);
2248		}
2249		$data = '';
2250		$listbeg = [];
2251		$divdepth = [];
2252		$hdr_structure = [];
2253		$show_title_level = [];
2254		$last_hdr = [];
2255		$nb_last_hdr = 0;
2256		$nb_hdrs = 0;
2257		$inTable = 0;
2258		$inPre = 0;
2259		$inComment = 0;
2260		$inTOC = 0;
2261		$inScript = 0;
2262		$title_text = '';
2263
2264		// loop: process all lines
2265		$in_paragraph = 0;
2266		$in_empty_paragraph = 0;
2267
2268		foreach ($lines as $line) {
2269			// Add newlines between lines
2270			if (isset($current_title_num)) { // Exclude the first line
2271				$data .= "\n";
2272			}
2273
2274			$current_title_num = '';
2275			$numbering_remove = 0;
2276
2277			$line = rtrim($line); // Trim off trailing white space
2278			// Check for titlebars...
2279			// NOTE: that title bar should start at the beginning of the line and
2280			//	   be alone on that line to be autoaligned... otherwise, it is an old
2281			//	   styled title bar...
2282			if (substr(ltrim($line), 0, 2) == '-=' && substr($line, -2, 2) == '=-') {
2283				// Close open paragraph and lists, but not div's
2284				$this->close_blocks($data, $in_paragraph, $listbeg, $divdepth, 1, 1, 0);
2285				//
2286				$align_len = strlen($line) - strlen(ltrim($line));
2287
2288				// My textarea size is about 120 space chars.
2289				//define('TEXTAREA_SZ', 120);
2290
2291				// NOTE: That strict math formula (split into 3 areas) gives
2292				//	   bad visual effects...
2293				// $align = ($align_len < (TEXTAREA_SZ / 3)) ? "left"
2294				//		: (($align_len > (2 * TEXTAREA_SZ / 3)) ? "right" : "center");
2295				//
2296				// Going to introduce some heuristic here :)
2297				// Visualy (remember that space char is thin) center starts at 25 pos
2298				// and 'right' from 60 (HALF of full width!) -- thats all :)
2299				//
2300				// NOTE: Guess align only if more than 10 spaces before -=title=-
2301				if ($align_len > 10) {
2302					$align = ($align_len < 25) ? "left" : (($align_len > 60) ? "right" : "center");
2303
2304					$align = ' style="text-align: ' . $align . ';"';
2305				} else {
2306					$align = '';
2307				}
2308
2309				//
2310				$line = trim($line);
2311				$line = '<div class="titlebar"' . $align . '>' . substr($line, 2, strlen($line) - 4) . '</div>';
2312				$data .= $line . "\n";
2313				// TODO: Case is handled ...  no need to check other conditions
2314				//	   (it is apriori known that they are all false, moreover sometimes
2315				//	   check procedure need > O(0) of compexity)
2316				//	   -- continue to next line...
2317				//	   MUST replace all remaining parse blocks to the same logic...
2318				continue;
2319			}
2320
2321			// Replace old styled titlebars
2322			if (strlen($line) != strlen($line = preg_replace("/-=(.+?)=-/", "<div class='titlebar'>$1</div>", $line))) {
2323				// Close open paragraph, but not lists (why not?) or div's
2324				$this->close_blocks($data, $in_paragraph, $listbeg, $divdepth, 1, 0, 0);
2325				$data .= $line . "\n";
2326
2327				continue;
2328			}
2329
2330			// check if we are inside a ~hc~ block and, if so, ignore
2331			// monospaced and do not insert <br />
2332			$lineInLowerCase = TikiLib::strtolower($this->unprotectSpecialChars($line, true));
2333
2334			$inComment += substr_count($lineInLowerCase, "<!--");
2335			$inComment -= substr_count($lineInLowerCase, "-->");
2336			if ($inComment < 0) {	// stop lines containing just --> being detected as comments
2337				$inComment = 0;
2338			}
2339
2340			// check if we are inside a ~pre~ block and, if so, ignore
2341			// monospaced and do not insert <br />
2342			$inPre += substr_count($lineInLowerCase, "<pre");
2343			$inPre -= substr_count($lineInLowerCase, "</pre");
2344
2345			// check if we are inside a table, if so, ignore monospaced and do
2346			// not insert <br />
2347
2348			$inTable += substr_count($lineInLowerCase, "<table");
2349			$inTable -= substr_count($lineInLowerCase, "</table");
2350
2351			// check if we are inside an ul TOC list, if so, ignore monospaced and do
2352			// not insert <br />
2353			$inTOC += substr_count($lineInLowerCase, "<ul class=\"toc");
2354			$inTOC -= substr_count($lineInLowerCase, "</ul><!--toc-->");
2355
2356			// check if we are inside a script not insert <br />
2357			$inScript += substr_count($lineInLowerCase, "<script ");
2358			$inScript -= substr_count($lineInLowerCase, "</script>");
2359
2360			// If the first character is ' ' and we are not in pre then we are in pre
2361			if ($prefs['feature_wiki_monosp'] == 'y' && substr($line, 0, 1) == ' ' /* The first character is a space (' '). */
2362				&& $inTable == 0 && $inPre == 0 && $inComment == 0 && ! $this->option['is_html']) {
2363				// Close open paragraph and lists, but not div's
2364				$this->close_blocks($data, $in_paragraph, $listbeg, $divdepth, 1, 1, 0);
2365
2366				// make font monospaced
2367				// For fixed formatting, use ~pp~...~/pp~
2368				$line = '<tt>' . $line . '</tt>';
2369			}
2370
2371			// Replace hotwords and more
2372			// 08-Jul-2003, by zaufi
2373			// HotWords will be replace only in ordinal text
2374			// It looks __really__ goofy in Headers or Titles
2375			$line = $this->parse_data_inline_syntax($line, null, $this->option['ck_editor']);
2376
2377			// This line is parseable then we have to see what we have
2378			if (substr($line, 0, 3) == '---') {
2379				// This is not a list item --- close open paragraph and lists, but not div's
2380				$this->close_blocks($data, $in_paragraph, $listbeg, $divdepth, 1, 1, 0);
2381				$line = '<hr />';
2382			} else {
2383				$litype = substr($line, 0, 1);
2384				if (($litype == '*' || $litype == '#') && ! (strlen($line) - count($listbeg) > 4 && preg_match('/^\*+$/', $line))) {
2385					// Close open paragraph, but not lists or div's
2386					$this->close_blocks($data, $in_paragraph, $listbeg, $divdepth, 1, 0, 0);
2387					$listlevel = $tikilib->how_many_at_start($line, $litype);
2388					$liclose = '</li>';
2389					$addremove = 0;
2390					if ($listlevel < count($listbeg)) {
2391						while ($listlevel != count($listbeg)) {
2392							$data .= array_shift($listbeg);
2393						}
2394						if (substr(current($listbeg), 0, 5) != '</li>') {
2395							$liclose = '';
2396						}
2397					} elseif ($listlevel > count($listbeg)) {
2398						$listyle = '';
2399						while ($listlevel != count($listbeg)) {
2400							array_unshift($listbeg, ($litype == '*' ? '</ul>' : '</ol>'));
2401							if ($listlevel == count($listbeg)) {
2402								$listate = substr($line, $listlevel, 1);
2403								if (($listate == '+' || $listate == '-') && ! ($litype == '*' && ! strstr(current($listbeg), '</ul>') || $litype == '#' && ! strstr(current($listbeg), '</ol>'))) {
2404									$thisid = 'id' . microtime() * 1000000;
2405									if (! $this->option['ck_editor']) {
2406										$data .= '<br /><a id="flipper' . $thisid . '" class="link" href="javascript:flipWithSign(\'' . $thisid . '\')">[' . ($listate == '-' ? '+' : '-') . ']</a>';
2407									}
2408									$listyle = ' id="' . $thisid . '" style="display:' . ($listate == '+' || $this->option['ck_editor'] ? 'block' : 'none') . ';"';
2409									$addremove = 1;
2410								}
2411							}
2412							$data .= ($litype == '*' ? "<ul$listyle>" : "<ol$listyle>");
2413						}
2414						$liclose = '';
2415					}
2416					if ($litype == '*' && ! strstr(current($listbeg), '</ul>') || $litype == '#' && ! strstr(current($listbeg), '</ol>')) {
2417						$data .= array_shift($listbeg);
2418						$listyle = '';
2419						$listate = substr($line, $listlevel, 1);
2420						if (($listate == '+' || $listate == '-')) {
2421							$thisid = 'id' . microtime() * 1000000;
2422							if (! $this->option['ck_editor']) {
2423								$data .= '<br /><a id="flipper' . $thisid . '" class="link" href="javascript:flipWithSign(\'' . $thisid . '\')">[' . ($listate == '-' ? '+' : '-') . ']</a>';
2424							}
2425							$listyle = ' id="' . $thisid . '" style="display:' . ($listate == '+' || $this->option['ck_editor'] ? 'block' : 'none') . ';"';
2426							$addremove = 1;
2427						}
2428						$data .= ($litype == '*' ? "<ul$listyle>" : "<ol$listyle>");
2429						$liclose = '';
2430						array_unshift($listbeg, ($litype == '*' ? '</li></ul>' : '</li></ol>'));
2431					}
2432					$line = $liclose . '<li>' . substr($line, $listlevel + $addremove);
2433					if (substr(current($listbeg), 0, 5) != '</li>') {
2434						array_unshift($listbeg, '</li>' . array_shift($listbeg));
2435					}
2436				} elseif ($litype == '+') {
2437					// Close open paragraph, but not list or div's
2438					$this->close_blocks($data, $in_paragraph, $listbeg, $divdepth, 1, 0, 0);
2439					$listlevel = $tikilib->how_many_at_start($line, $litype);
2440					// Close lists down to requested level
2441					while ($listlevel < count($listbeg)) {
2442						$data .= array_shift($listbeg);
2443					}
2444
2445					// Must append paragraph for list item of given depth...
2446					$listlevel = $tikilib->how_many_at_start($line, $litype);
2447					if (count($listbeg)) {
2448						if (substr(current($listbeg), 0, 5) != '</li>') {
2449							array_unshift($listbeg, '</li>' . array_shift($listbeg));
2450							$liclose = '<li>';
2451						} else {
2452							$liclose = '<br />';
2453						}
2454					} else {
2455						$liclose = '';
2456					}
2457					$line = $liclose . substr($line, count($listbeg));
2458				} else {
2459					// This is not a list item - close open lists,
2460					// but not paragraph or div's. If we are
2461					// closing a list, there really shouldn't be a
2462					// paragraph open anyway.
2463					$this->close_blocks($data, $in_paragraph, $listbeg, $divdepth, 0, 1, 0);
2464
2465					// Get count of (possible) header signs at start
2466					$hdrlevel = $tikilib->how_many_at_start($line, '!');
2467
2468					if ($litype == '!' && $hdrlevel > 0 && $hdrlevel <= 6 /* HTML has h1 to h6, but no h7 or above */) { // If the line starts with 1 to 6 exclamation marks ("!")
2469						/*
2470						 * Handle headings autonumbering syntax (i.e. !#Text, !!#Text, ...)
2471						 * Note :
2472						 *    this needs to be done even if the current header has no '#'
2473						 *    in order to generate the right numbers when they are not specified for every headers.
2474						 *    This is the case, for example, when you want to add numbers to headers of level 2 but not to level 1
2475						 */
2476
2477						$line_lenght = strlen($line);
2478
2479						// Generate an array containing the squeleton of maketoc (based on headers levels)
2480						//   i.e. hdr_structure will contain something lile this :
2481						//     array( 1, 2, 2.1, 2.1.1, 2.1.2, 2.2, ... , X.Y.Z... )
2482						//
2483
2484						$hdr_structure[$nb_hdrs] = [];
2485
2486						// Generate the number (e.g. 1.2.1.1) of the current title, based on the previous title number :
2487						//   - if the current title deepest level is lesser than (or equal to)
2488						//     the deepest level of the previous title : then we increment the last level number,
2489						//   - else : we simply add new levels with value '1' (only if the previous level number was shown),
2490						//
2491						if ($nb_last_hdr > 0 && $hdrlevel <= $nb_last_hdr) {
2492							$hdr_structure[$nb_hdrs] = array_slice($last_hdr, 0, $hdrlevel);
2493							if (! empty($show_title_level[$hdrlevel]) || ! $need_autonumbering) {
2494								//
2495								// Increment the level number only if :
2496								//     - the last title of the same level number has a displayed number
2497								//  or - no title has a displayed number (no autonumbering)
2498								//
2499								$hdr_structure[$nb_hdrs][$hdrlevel - 1]++;
2500							}
2501						} else {
2502							if ($nb_last_hdr > 0) {
2503								$hdr_structure[$nb_hdrs] = $last_hdr;
2504							}
2505							for ($h = 0; $h < $hdrlevel - $nb_last_hdr; $h++) {
2506								$hdr_structure[$nb_hdrs][$h + $nb_last_hdr] = '1';
2507							}
2508						}
2509						$show_title_level[$hdrlevel] = preg_match('/^!+[\+\-]?#/', $line);
2510
2511						// Update last_hdr info for the next header
2512						$last_hdr = $hdr_structure[$nb_hdrs];
2513						$nb_last_hdr = count($last_hdr);
2514
2515						if (is_array($last_hdr)) {
2516							$current_title_real_num = implode('.', $last_hdr) . '. ';
2517						} else {
2518							$current_title_real_num = $last_hdr . '. ';
2519						}
2520
2521						// Update the current title number to hide all parents levels numbers if the parent has no autonumbering
2522						$hideall = false;
2523						for ($j = $hdrlevel; $j > 0; $j--) {
2524							if ($hideall || empty($show_title_level[$j])) {
2525								unset($hdr_structure[$j - 1]);
2526								$hideall = true;
2527							}
2528						}
2529
2530						// Store the title number to use only if it has to be shown (if the '#' char is used)
2531						$current_title_num = '';
2532						if (isset($show_title_level[$hdrlevel]) && isset($hdr_structure[$nb_hdrs])) {
2533							$current_title_num = $show_title_level[$hdrlevel] ? implode('.', $hdr_structure[$nb_hdrs]) . '. ' : '';
2534						}
2535
2536						$nb_hdrs++;
2537
2538
2539						// Close open paragraph (lists already closed above)
2540						$this->close_blocks($data, $in_paragraph, $listbeg, $divdepth, 1, 0, 0);
2541						// Close lower level divs if opened
2542						for (; current($divdepth) >= $hdrlevel; array_shift($divdepth)) {
2543							$data .= '</div>';
2544						}
2545
2546						// Remove possible hotwords replaced :)
2547						//   Umm, *why*?  Taking this out lets page
2548						//   links in headers work, which can be nice.
2549						//   -rlpowell
2550						// $line = strip_tags($line);
2551
2552						// OK. Parse headers here...
2553						$anchor = '';
2554						$aclose = '';
2555						$aclose2 = '';
2556						$addremove = $show_title_level[$hdrlevel] ? 1 : 0; // If needed, also remove '#' sign from title beginning
2557
2558						// May be special signs present after '!'s?
2559						$divstate = substr($line, $hdrlevel, 1);
2560						if (($divstate == '+' || $divstate == '-') && ! $this->option['ck_editor']) {
2561							// OK. Must insert flipper after HEADER, and then open new div...
2562							$thisid = 'id' . preg_replace('/[^a-zA-z0-9]/', '', urlencode($this->option['page'])) . $nb_hdrs;
2563							require_once __DIR__ . '/../setup/cookies.php';
2564							$state_cookie = getCookie($thisid, "showhide_headings");
2565							if ($state_cookie === 'o' && $divstate === '-') {
2566								$divstate = '+';
2567							} elseif ($state_cookie === 'c' && $divstate === '+') {
2568								$divstate = '-';
2569							}
2570							$aclose = '<a id="flipper' . $thisid . '" class="link" href="#" onclick="flipWithSign(\'' . $thisid . '\');return false;">[' . ($divstate == '-' ? '+' : '-') . ']</a>';
2571							$aclose2 = '<div id="' . $thisid . '" class="showhide_heading" style="display:' . ($divstate == '+' ? 'block' : 'none') . ';">';
2572							$headerlib = TikiLib::lib('header');
2573							$headerlib->add_jq_onready("setheadingstate('$thisid');");
2574							array_unshift($divdepth, $hdrlevel);
2575							$addremove += 1;
2576						}
2577
2578						// Generate the final title text
2579						$title_text_base = substr($line, $hdrlevel + $addremove);
2580						$title_text = $current_title_num . $title_text_base;
2581
2582						// create stable anchors for all headers
2583						// use header but replace non-word character sequences
2584						// with one underscore (for XHTML 1.0 compliance)
2585						// Workaround pb with plugin replacement and header id
2586						//  first we remove hash from title_text for headings beginning
2587						//  with images and HTML tags
2588						$thisid = preg_replace('/§[a-z0-9]{32}§/', '', $title_text);
2589						$thisid = preg_replace('#</?[^>]+>#', '', $thisid);
2590						$thisid = preg_replace('/[^a-zA-Z0-9\:\.\-\_]+/', '_', $thisid);
2591						$thisid = preg_replace('/^[^a-zA-Z]*/', '', $thisid);
2592						if (empty($thisid)) {
2593							$thisid = 'a' . md5($title_text);
2594						}
2595
2596						// Add a number to the anchor if it already exists, to avoid duplicated anchors
2597						if (isset($all_anchors[$thisid])) {
2598							$all_anchors[$thisid]++;
2599							$thisid .= '_' . $all_anchors[$thisid];
2600						} else {
2601							$all_anchors[$thisid] = 1;
2602						}
2603
2604						// Collect TOC entry if any {maketoc} is present on the page
2605						//if ( $need_maketoc !== false ) {
2606						$anch[] = [
2607										'id' => $thisid,
2608										'hdrlevel' => $hdrlevel,
2609										'pagenum' => $pageNum,
2610										'title' => $title_text_base,
2611										'title_displayed_num' => $current_title_num,
2612										'title_real_num' => $current_title_real_num
2613										];
2614						//}
2615						global $tiki_p_edit, $section;
2616						if ($prefs['wiki_edit_section'] === 'y' && $section === 'wiki page' && $tiki_p_edit === 'y' &&
2617								( $prefs['wiki_edit_section_level'] == 0 || $hdrlevel <= $prefs['wiki_edit_section_level']) &&
2618								(empty($this->option['print']) || ! $this->option['print']) && ! $this->option['suppress_icons'] ) {
2619							$smarty = TikiLib::lib('smarty');
2620							include_once('lib/smarty_tiki/function.icon.php');
2621
2622							if ($prefs['wiki_edit_icons_toggle'] == 'y' && ! isset($_COOKIE['wiki_plugin_edit_view'])) {
2623								$iconDisplayStyle = ' style="display:none;"';
2624							} else {
2625								$iconDisplayStyle = '';
2626							}
2627							$button = '<div class="icon_edit_section"' . $iconDisplayStyle . '><a title="' . tra('Edit Section') . '" href="tiki-editpage.php?';
2628							if (! empty($this->option['page'])) {
2629								$button .= 'page=' . urlencode($this->option['page']) . '&amp;';
2630							}
2631							$button .= 'hdr=' . $nb_hdrs . '">' . smarty_function_icon(['name' => 'edit'], $smarty->getEmptyInternalTemplate()) . '</a></div>';
2632						} else {
2633							$button = '';
2634						}
2635
2636						// replace <div> with <h> style attribute
2637						$do_center = 0;
2638						$title_text = preg_replace('/<div style="text-align: center;">(.*)<\/div>/', '\1', $title_text, 1, $do_center);
2639
2640						// prevent widow words on headings - originally from http://davidwalsh.name/prevent-widows-php-javascript
2641						$title_text = preg_replace('/^\s*(.+\s+\S+)\s+(\S+)\s*$/sumU', '$1&nbsp;$2', $title_text);
2642
2643						$style = $do_center ? ' style="text-align: center;"' : '';
2644
2645						if ($prefs['wiki_heading_links'] === 'y') {
2646							$smarty = TikiLib::lib('smarty');
2647							$smarty->loadPlugin('smarty_function_icon');
2648							$headingLink = '<a href="#' . $thisid . '" class="heading-link">' . smarty_function_icon(['name' => 'link'], $smarty->getEmptyInternalTemplate()) . '</a>';
2649						} else {
2650							$headingLink = '';
2651						}
2652
2653						if ($prefs['feature_wiki_show_hide_before'] == 'y') {
2654							$line = $button . '<h' . ($hdrlevel) . $style . ' class="showhide_heading" id="' . $thisid . '">' . $aclose . ' ' . $title_text . $headingLink . '</h' . ($hdrlevel) . '>' . $aclose2;
2655						} else {
2656							$line = $button . '<h' . ($hdrlevel) . $style . ' class="showhide_heading" id="' . $thisid . '">' . $title_text . $headingLink . '</h' . ($hdrlevel) . '>' . $aclose . $aclose2;
2657						}
2658					} elseif (! strcmp($line, $prefs['wiki_page_separator'])) {
2659						// Close open paragraph, lists, and div's
2660						$this->close_blocks($data, $in_paragraph, $listbeg, $divdepth, 1, 1, 1);
2661						// Leave line unchanged... tiki-index.php will split wiki here
2662						$line = $prefs['wiki_page_separator'];
2663						$pageNum += 1;
2664					} else {
2665						/** Usual paragraph.
2666						 *
2667						 * If the
2668						 * $prefs['feature_wiki_paragraph_formatting']
2669						 * is on, then consecutive lines of
2670						 * text will be gathered into a block
2671						 * that is surrounded by HTML
2672						 * paragraph tags. One or more blank
2673						 * lines, or another special Wiki line
2674						 * (e.g., heading, titlebar, etc.)
2675						 * signifies the end of the
2676						 * paragraph. If the paragraph
2677						 * formatting feature is off, the
2678						 * original Tikiwiki behavior is used,
2679						 * in which each line in the source is
2680						 * terminated by an explicit line
2681						 * break (br tag).
2682						 *
2683						 * @since Version 1.9
2684						 */
2685						if ($inTable == 0 && $inPre == 0 && $inComment == 0 && $inTOC == 0 &&  $inScript == 0
2686								// Don't put newlines at comments' end!
2687								&& strpos($line, "-->") !== (strlen($line) - 3)
2688								&& $this->option['process_wiki_paragraphs']) {
2689							$tline = trim(str_replace('&nbsp;', '', $this->unprotectSpecialChars($line, true)));
2690
2691							if ($prefs['feature_wiki_paragraph_formatting'] == 'y') {
2692								if (count($lines) > 1 || $this->option['min_one_paragraph']) {	// don't apply wiki para if only single line so you can have inline includes
2693									$contains_block = $this->contains_html_block($tline);
2694									$contains_br = $this->contains_html_br($tline);
2695
2696									if (! $contains_block) {	// check inside plugins etc for block elements
2697										preg_match_all('/\xc2\xa7[^\xc2\xa7]+\xc2\xa7/', $tline, $m);	// noparse guid for plugins
2698										if (count($m) > 0) {
2699											$m_count = count($m[0]);
2700											$nop_ix = false;
2701											for ($i = 0; $i < $m_count; $i++) {
2702												//$nop_ix = array_search( $m[0][$i], $noparsed['key'] ); 	// array_search doesn't seem to work here - why? no "keys"?
2703												foreach ($noparsed['key'] as $k => $s) {
2704													if ($m[0][$i] == $s) {
2705														$nop_ix = $k;
2706														break;
2707													}
2708												}
2709												if ($nop_ix !== false) {
2710													$nop_str = $noparsed['data'][$nop_ix];
2711													$contains_block = $this->contains_html_block($nop_str);
2712													if ($contains_block) {
2713														break;
2714													}
2715												}
2716											}
2717										}
2718									}
2719
2720									$add_brs = $prefs['feature_wiki_paragraph_formatting_add_br'] === 'y' && ! $this->option['is_html'];
2721									if ($in_paragraph && ((empty($tline) && ! $in_empty_paragraph) || $contains_block)) {
2722										// If still in paragraph, on meeting first blank line or end of div or start of div created by plugins; close a paragraph
2723										$this->close_blocks($data, $in_paragraph, $listbeg, $divdepth, 1, 0, 0);
2724									} elseif (! $in_paragraph && ! $contains_block && ! $contains_br && (! empty($tline) || $add_brs)) {
2725										// If not in paragraph, first non-blank line; start a paragraph; if not start of div created by plugins
2726										$data .= "<p>";
2727										$in_paragraph = 1;
2728										$in_empty_paragraph = empty($tline) && $add_brs;
2729									} elseif ($in_paragraph && $add_brs && ! $contains_block) {
2730										// A normal in-paragraph line if not close of div created by plugins
2731										if (! empty($tline)) {
2732											$in_empty_paragraph = false;
2733										}
2734										$line = "<br />" . $line;
2735									} // else {
2736									  // A normal in-paragraph line or a consecutive blank line.
2737									  // Leave it as is.
2738									  // }
2739								}
2740							} else {
2741								$line .= "<br />";
2742							}
2743						}
2744					}
2745				}
2746			}
2747			$data .= $line;
2748		}
2749
2750		if ($this->option['is_html']) {
2751			// A paragraph cannot contain paragraphs.
2752			// The following avoids invalid HTML, but could result in formatting different from that intended. Should a replacement here not at least generate a notice? Chealer 2017-08-14
2753			$count = 1;
2754			while ($count == 1) {
2755				$data = preg_replace("#<p>([^(</p>)]*)<p>([^(</p>)]*)</p>#uims", "<p>$1$2", $data, 1, $count);
2756			}
2757			if (is_null($data)) {
2758				trigger_error('Parsing failed (' . array_flip(get_defined_constants(true)['pcre'])[preg_last_error()] . ')', E_USER_WARNING);
2759			}
2760		}
2761
2762		// Close open paragraph, lists, and div's
2763		$this->close_blocks($data, $in_paragraph, $listbeg, $divdepth, 1, 1, 1);
2764
2765		/*
2766		 * Replace special "maketoc" plugins
2767		 *  Valid arguments :
2768		 *    - type (look of the maketoc),
2769		 *    - maxdepth (max level displayed),
2770		 *    - title (replace the default title),
2771		 *    - showhide (if set to y, add the Show/Hide link)
2772		 *    - nolinks (if set to y, don't add links on toc entries)
2773		 *    - nums :
2774		 *       * 'n' means 'no title autonumbering' in TOC,
2775		 *       * 'force' means :
2776		 *	    ~ same as 'y' if autonumbering is used in the page,
2777		 *	    ~ 'number each toc entry as if they were all autonumbered'
2778		 *       * any other value means 'same as page's headings autonumbering',
2779		 *
2780		 *  (Note that title will be translated if a translation is available)
2781		 *
2782		 *  Examples: {maketoc}, {maketoc type=box maxdepth=1 showhide=y}, {maketoc title="Page Content" maxdepth=3}, ...
2783		 *  Obsolete syntax: {maketoc:box}
2784		 */
2785		$new_data = '';
2786		$search_start = 0;
2787		if ($need_maketoc) {
2788			while (($maketoc_start = strpos($data, "{maketoc", $search_start)) !== false) {
2789				$maketoc_length = strpos($data, "}", $maketoc_start) + 1 - $maketoc_start;
2790				$maketoc_string = substr($data, $maketoc_start, $maketoc_length);
2791
2792				// Handle old type definition for type "box" (and preserve environment for the title also)
2793				if ($maketoc_length > 12 && TikiLib::strtolower(substr($maketoc_string, 8, 4)) == ':box') {
2794					$maketoc_string = "{maketoc type=box showhide=y title='" . tra('index', $this->option['language'], true) . '"' . substr($maketoc_string, 12);
2795				}
2796
2797				$maketoc_string = str_replace('&quot;', '"', $maketoc_string);
2798				$maketoc_regs = [];
2799
2800				if ($maketoc_length == 9 || preg_match_all("/([^\s=\(]+)=([^\"\s=\)\}]+|\"[^\"]*\")/", $maketoc_string, $maketoc_regs)) {
2801					if ($maketoc_start > 0) {
2802						$new_data .= substr($data, 0, $maketoc_start);
2803					}
2804
2805					// Set maketoc default values
2806					$maketoc_args = [
2807							'type' => '',
2808							'maxdepth' => 0, // No limit
2809							'title' => tra('Table of contents', $this->option['language'], true),
2810							'showhide' => '',
2811							'nolinks' => '',
2812							'nums' => '',
2813							'levels' => ''
2814							];
2815
2816					// Build maketoc arguments list (and remove " chars if they are around the value)
2817					if (isset($maketoc_regs[1])) {
2818						$nb_args = count($maketoc_regs[1]);
2819						for ($a = 0; $a < $nb_args; $a++) {
2820							$maketoc_args[TikiLib::strtolower($maketoc_regs[1][$a])] = trim($maketoc_regs[2][$a], '"');
2821						}
2822					}
2823
2824					if ($maketoc_args['title'] != '') {
2825						// Translate maketoc title
2826						$maketoc_summary = ' summary="' . tra($maketoc_args['title'], $this->option['language'], true) . '"';
2827						$maketoc_title = "<div id='toctitle'><h3>" . tra($maketoc_args['title'], $this->option['language']) . '</h3>';
2828
2829						if (isset($maketoc_args['showhide']) && $maketoc_args['showhide'] == 'y') {
2830							$maketoc_title .= '<a class="link"  href="javascript:toggleToc()">' . '[' . tra('Show/Hide') . ']' . '</a>';
2831						}
2832						$maketoc_title .= '</div>';
2833					} else {
2834						$maketoc_summary = '';
2835						$maketoc_title = '';
2836					}
2837					if (! empty($maketoc_args['levels'])) {
2838						$maketoc_args['levels'] = preg_split('/\s*,\s*/', $maketoc_args['levels']);
2839					}
2840
2841					// Build maketoc
2842					switch ($maketoc_args['type']) {
2843						case 'box':
2844							$maketoc_header = '';
2845							$maketoc = "<table id='toc' class='toc'$maketoc_summary>\n<tr><td>$maketoc_title<ul>";
2846							$maketoc_footer = "</ul></td></tr></table>\n";
2847							$link_class = 'toclink';
2848							break;
2849						default:
2850							$maketoc = '';
2851							$maketoc_header = "<div id='toc'>" . $maketoc_title;
2852							$maketoc_footer = '</div>';
2853							$link_class = 'link';
2854					}
2855					foreach ($anch as $tocentry) {
2856						if ($maketoc_args['maxdepth'] > 0 && $tocentry['hdrlevel'] > $maketoc_args['maxdepth']) {
2857							continue;
2858						}
2859						if (! empty($maketoc_args['levels']) && ! in_array($tocentry['hdrlevel'], $maketoc_args['levels'])) {
2860							continue;
2861						}
2862						// Generate the toc entry title (with nums)
2863						if ($maketoc_args['nums'] == 'n') {
2864							$tocentry_title = '';
2865						} elseif ($maketoc_args['nums'] == 'force' && ! $need_autonumbering) {
2866							$tocentry_title = $tocentry['title_real_num'];
2867						} else {
2868							$tocentry_title = $tocentry['title_displayed_num'];
2869						}
2870						$tocentry_title .= $tocentry['title'];
2871
2872						// Generate the toc entry link
2873						$tocentry_link = '#' . $tocentry['id'];
2874						if ($tocentry['pagenum'] > 1) {
2875							$tocentry_link = $_SERVER['PHP_SELF'] . '?page=' . $this->option['page'] . '&pagenum=' . $tocentry['pagenum'] . $tocentry_link;
2876						}
2877						if ($maketoc_args['nolinks'] != 'y') {
2878							$tocentry_title = "<a href='$tocentry_link' class='link'>" . $tocentry_title . '</a>';
2879						}
2880
2881						if ($maketoc != '') {
2882							$maketoc .= "\n";
2883						}
2884						$shift = $tocentry['hdrlevel'];
2885						if (! empty($maketoc_args['levels'])) {
2886							for ($i = 1; $i <= $tocentry['hdrlevel']; ++$i) {
2887								if (! in_array($i, $maketoc_args['levels'])) {
2888									--$shift;
2889								}
2890							}
2891						}
2892						switch ($maketoc_args['type']) {
2893							case 'box':
2894								$maketoc .= "<li class='toclevel-" . $shift . "'>" . $tocentry_title . "</li>";
2895								break;
2896							default:
2897								$maketoc .= str_repeat('*', $shift) . $tocentry_title;
2898						}
2899					}
2900
2901					$maketoc = $this->parse_data($maketoc, ['noparseplugins' => true]);
2902
2903					if (preg_match("/^<ul>/", $maketoc)) {
2904						$maketoc = preg_replace("/^<ul>/", '<ul class="toc">', $maketoc);
2905						$maketoc .= '<!--toc-->';
2906					}
2907
2908					if ($link_class != 'link') {
2909						$maketoc = preg_replace("/'link'/", "'$link_class'", $maketoc);
2910					}
2911
2912					//patch-ini - Patch taken from http://dev.tiki.org/item5405
2913					global $TOC_newstring, $TOC_oldstring ;
2914
2915					$TOC_newstring = $maketoc ; //===== get a copy of the newest TOC before we do anything to it
2916					if (strpos($maketoc, $TOC_oldstring)) { // larryg - if this MAKETOC contains previous chapter's TOC entries, remove that portion of the string
2917						$maketoc = substr($maketoc, 0, strpos($maketoc, $TOC_oldstring)) . substr($maketoc, strpos($maketoc, $TOC_oldstring) + strlen($TOC_oldstring)) ;
2918					}
2919
2920					//prepare this chapter's TOC entry to be compared with the next chapter's string]
2921					$head_string = '<li><a href='   ;
2922					$tail_string = '<!--toc-->' ;
2923					if (strpos($TOC_newstring, $head_string) && strpos($TOC_newstring, $tail_string)) {
2924						$TOC_newstring = substr($TOC_newstring, strpos($TOC_newstring, $head_string)) ; // trim unwanted stuff from the beginning of the string
2925						$TOC_newstring = substr($TOC_newstring, 0, (strpos($TOC_newstring, $tail_string) - 5)) ; // trim the stuff from the tail of the string    </ul></li></ul>
2926						$TOC_oldstring = $TOC_newstring ;
2927					}
2928					//patch-end - Patch taken from http://dev.tiki.org/item5405
2929
2930					if (! empty($maketoc)) {
2931						$maketoc = $maketoc_header . $maketoc . $maketoc_footer;
2932					}
2933					$new_data .= $maketoc;
2934					$data = substr($data, $maketoc_start + $maketoc_length);
2935					$search_start = 0; // Reinitialize search start cursor, since data now begins after the last replaced maketoc
2936				} else {
2937					$search_start = $maketoc_start + $maketoc_length;
2938				}
2939			}
2940		}
2941		$data = $new_data . $data;
2942		// Add icon to edit the text before the first section (if there is some)
2943		if ($prefs['wiki_edit_section'] === 'y' && isset($section) && $section === 'wiki page' && $tiki_p_edit === 'y' && (empty($this->option['print']) ||
2944				! $this->option['print'])  && strpos($data, '<div class="icon_edit_section">') != 0 && ! $this->option['suppress_icons']) {
2945			$smarty = TikiLib::lib('smarty');
2946			include_once('lib/smarty_tiki/function.icon.php');
2947			$button = '<div class="icon_edit_section"><a title="' . tra('Edit Section') . '" href="tiki-editpage.php?';
2948			if (! empty($this->option['page'])) {
2949				$button .= 'page=' . urlencode($this->option['page']) . '&amp;';
2950			}
2951			$button .= 'hdr=0">' . smarty_function_icon(['name' => 'edit'], $smarty->getEmptyInternalTemplate()) . '</a></div>';
2952			$data = $button . $data;
2953		}
2954	}
2955
2956	//*
2957	function contains_html_block($inHtml)
2958	{
2959		// detect all block elements as defined on http://www.w3.org/2007/07/xhtml-basic-ref.html
2960		$block_detect_regexp = '/<[\/]?(?:address|blockquote|div|dl|fieldset|h\d|hr|li|noscript|ol|p|pre|table|ul)/i';
2961		return  (preg_match($block_detect_regexp, $this->unprotectSpecialChars($inHtml, true)) > 0);
2962	}
2963
2964	//*
2965	function contains_html_br($inHtml)
2966	{
2967		$block_detect_regexp = '/<(?:br)/i';
2968		return  (preg_match($block_detect_regexp, $this->unprotectSpecialChars($inHtml, true)) > 0);
2969	}
2970
2971	//*
2972	function get_wiki_link_replacement($pageLink, $extra = [], $ck_editor = false)
2973	{
2974		global $prefs;
2975		$wikilib = TikiLib::lib('wiki');
2976		$tikilib = TikiLib::lib('tiki');
2977
2978		// Fetch all externals once
2979		static $externals = false;
2980		if (false === $externals) {
2981			$externals = $tikilib->fetchMap('SELECT LOWER(`name`), `extwiki` FROM `tiki_extwiki`');
2982		}
2983
2984		$displayLink = $pageLink;
2985
2986		// HTML entities encoding breaks page lookup
2987		$pageLink = html_entity_decode($pageLink, ENT_COMPAT, 'UTF-8');
2988
2989		if ($prefs['namespace_enabled'] == 'y' && $prefs['namespace_force_links'] == 'y'
2990			&& $wikilib->get_namespace($this->option['page'])
2991			&& ! $wikilib->get_namespace($pageLink) ) {
2992				$pageLink = $wikilib->get_namespace($this->option['page']) . $prefs['namespace_separator'] . $pageLink;
2993		}
2994
2995		$description = null;
2996		$reltype = null;
2997		$processPlural = false;
2998		$anchor = null;
2999
3000		if (array_key_exists('description', $extra)) {
3001			$description = $extra['description'];
3002		}
3003		if (array_key_exists('reltype', $extra)) {
3004			$reltype = $extra['reltype'];
3005		}
3006		if (array_key_exists('plural', $extra)) {
3007			$processPlural = (boolean) $extra['plural'];
3008		}
3009		if (array_key_exists('anchor', $extra)) {
3010			$anchor = $extra['anchor'];
3011		}
3012
3013		$link = new WikiParser_OutputLink;
3014		$link->setIdentifier($pageLink);
3015		$link->setNamespace($this->option['namespace'], $prefs['namespace_separator']);
3016		$link->setQualifier($reltype);
3017		$link->setDescription($description);
3018		$link->setWikiLookup([ $this, 'parser_helper_wiki_info_getter' ]);
3019		if ($ck_editor) {	// prevent page slug being used in wysiwyg editor, needs the page name
3020			$link->setWikiLinkBuilder(
3021				function ($pageLink) {
3022					return $pageLink;
3023				}
3024			);
3025		} else {
3026			$link->setWikiLinkBuilder(
3027				function ($pageLink) {
3028					$wikilib = TikiLib::lib('wiki');
3029					return $wikilib->sefurl($pageLink);
3030				}
3031			);
3032		}
3033		$link->setExternals($externals);
3034		$link->setHandlePlurals($processPlural);
3035		$link->setAnchor($anchor);
3036
3037		if ($prefs['feature_multilingual'] == 'y' && isset($GLOBALS['pageLang'])) {
3038			$link->setLanguage($GLOBALS['pageLang']);
3039		}
3040
3041		return $link->getHtml($ck_editor);
3042	}
3043
3044	//*
3045	function parser_helper_wiki_info_getter($pageName)
3046	{
3047		global $prefs;
3048		$tikilib = TikiLib::lib('tiki');
3049		$page_info = $tikilib->get_page_info($pageName, false);
3050
3051		if ($page_info !== false) {
3052			return $page_info;
3053		}
3054
3055		// If page does not exist directly, attempt to find an alias
3056		if ($prefs['feature_wiki_pagealias'] == 'y') {
3057			$semanticlib = TikiLib::lib('semantic');
3058
3059			$toPage = $pageName;
3060			$tokens = explode(',', $prefs['wiki_pagealias_tokens']);
3061
3062			$prefixes = explode(',', $prefs["wiki_prefixalias_tokens"]);
3063			foreach ($prefixes as $p) {
3064				$p = trim($p);
3065				if (strlen($p) > 0 && TikiLib::strtolower(substr($pageName, 0, strlen($p))) == TikiLib::strtolower($p)) {
3066					$toPage = $p;
3067					$tokens = 'prefixalias';
3068				}
3069			}
3070
3071			$links = $semanticlib->getLinksUsing(
3072				$tokens,
3073				[ 'toPage' => $toPage ]
3074			);
3075
3076			if (count($links) > 1) {
3077				// There are multiple aliases for this page. Need to disambiguate.
3078				//
3079				// When feature_likePages is set, trying to display the alias itself will
3080				// display an error page with the list of aliased pages in the "like pages" section.
3081				// This allows the user to pick the appropriate alias.
3082				// So, leave the $pageName to the alias.
3083				//
3084				// If feature_likePages is not set, then the user will only see that the page does not
3085				// exist. So it's better to just pick the first one.
3086				//
3087				if ($prefs['feature_likePages'] == 'y' || $tokens == 'prefixalias') {
3088					// Even if there is more then one match, if prefix is being redirected then better
3089					// to fail than to show possibly wrong page
3090					return true;
3091				} else {
3092					// If feature_likePages is NOT set, then trying to display the first one is fine
3093					// $pageName is by ref so it does get replaced
3094					$pageName = $links[0]['fromPage'];
3095					return $tikilib->get_page_info($pageName);
3096				}
3097			} elseif (count($links)) {
3098				// there is exactly one match
3099				if ($prefs['feature_wiki_1like_redirection'] == 'y') {
3100					return true;
3101				} else {
3102					$pageName = $links[0]['fromPage'];
3103					return $tikilib->get_page_info($pageName);
3104				}
3105			}
3106		}
3107	}
3108	//*
3109	function parse_tagged_users($data)
3110	{
3111		$count = 1;
3112		return preg_replace_callback(
3113			'/(?:^|\s)@(\w+)/i',
3114			function ($matches) use (&$count) {
3115				$myUser = substr($matches[0], strpos($matches[0], "@") + 1);
3116				if ($myUser) {
3117					$u = TikiLib::lib('user')->get_user_info($myUser);
3118					if (is_array($u) && $u['userId'] > 0) {
3119						$v = TikiLib::lib('user')->build_userinfo_tag($myUser, '', 'userlink', 'y', 'mentioned-' . $myUser . '-section-' . $count);
3120						$count++;
3121						if ($v) {
3122							$prefix = ($matches[0][1] == '@') ? $matches[0][0] : '';
3123							return $prefix . $v;
3124						}
3125					}
3126
3127					return $matches[0];
3128				}
3129				return $matches[0];
3130			},
3131			$data
3132		);
3133	}
3134
3135	//*
3136	function parse_smileys($data)
3137	{
3138		global $prefs;
3139		static $patterns;
3140
3141		if ($prefs['feature_smileys'] == 'y') {
3142			if (! $patterns) {
3143				$patterns = [
3144					// Example of all Tiki Smileys (the old syntax)
3145					// (:biggrin:) (:confused:) (:cool:) (:cry:) (:eek:) (:evil:) (:exclaim:) (:frown:)
3146					// (:idea:) (:lol:) (:mad:) (:mrgreen:) (:neutral:) (:question:) (:razz:) (:redface:)
3147					// (:rolleyes:) (:sad:) (:smile:) (:surprised:) (:twisted:) (:wink:) (:arrow:) (:santa:)
3148
3149					"/\(:([^:]+):\)/" => "<img alt=\"$1\" src=\"img/smiles/icon_$1.gif\" />",
3150
3151					// :) :-)
3152					'/(\s|^):-?\)/' => "$1<img alt=\":-)\" title=\"" . tra('smiling') . "\" src=\"img/smiles/icon_smile.gif\" />",
3153					// :( :-(
3154					'/(\s|^):-?\(/' => "$1<img alt=\":-(\" title=\"" . tra('sad') . "\" src=\"img/smiles/icon_sad.gif\" />",
3155					// :D :-D
3156					'/(\s|^):-?D/' => "$1<img alt=\":-D\" title=\"" . tra('grinning') . "\" src=\"img/smiles/icon_biggrin.gif\" />",
3157					// :S :-S :s :-s
3158					'/(\s|^):-?S/i' => "$1<img alt=\":-S\" title=\"" . tra('confused') . "\" src=\"img/smiles/icon_confused.gif\" />",
3159					// B) B-) 8-)
3160					'/(\s|^)(B-?|8-)\)/' => "$1<img alt=\"B-)\" title=\"" . tra('cool') . "\" src=\"img/smiles/icon_cool.gif\" />",
3161					// :'( :_(
3162					'/(\s|^):[\'|_]\(/' => "$1<img alt=\":_(\" title=\"" . tra('crying') . "\" src=\"img/smiles/icon_cry.gif\" />",
3163					// 8-o 8-O =-o =-O
3164					'/(\s|^)[8=]-O/i' => "$1<img alt=\"8-O\" title=\"" . tra('frightened') . "\" src=\"img/smiles/icon_eek.gif\" />",
3165					// }:( }:-(
3166					'/(\s|^)\}:-?\(/' => "$1<img alt=\"}:(\" title=\"" . tra('evil stuff') . "\" src=\"img/smiles/icon_evil.gif\" />",
3167					// !-) !)
3168					'/(\s|^)\!-?\)/' => "$1<img alt=\"(!)\" title=\"" . tra('exclamation mark !') . "\" src=\"img/smiles/icon_exclaim.gif\" />",
3169					// >:( >:-(
3170					'/(\s|^)\>:-?\(/' => "$1<img alt=\"}:(\" title=\"" . tra('frowning') . "\" src=\"img/smiles/icon_frown.gif\" />",
3171					// i-)
3172					'/(\s|^)i-\)/' => "$1<img alt=\"(" . tra('light bulb') . ")\" title=\"" . tra('idea !') . "\" src=\"img/smiles/icon_idea.gif\" />",
3173					// LOL
3174					'/(\s|^)LOL(\s|$)/' => "$1<img alt=\"(" . tra('LOL') . ")\" title=\"" . tra('laughing out loud !') . "\" src=\"img/smiles/icon_lol.gif\" />$2",
3175					// >X( >X[ >:[ >X-( >X-[ >:-[
3176					'/(\s|^)\>[:X]-?\(/' => "$1<img alt=\">:[\" title=\"" . tra('mad') . "\" src=\"img/smiles/icon_mad.gif\" />",
3177					// =D =-D
3178					'/(\s|^)[=]-?D/' => "$1<img alt=\"=D\" title=\"" . tra('Mr. Green laughing') . "\" src=\"img/smiles/icon_mrgreen.gif\" />",
3179				];
3180			}
3181
3182			foreach ($patterns as $p => $r) {
3183				$data = preg_replace($p, $r, $data);
3184			}
3185		}
3186		return $data;
3187	}
3188
3189	//*
3190	function get_pages($data, $withReltype = false)
3191	{
3192		global $page_regex, $prefs;
3193		$tikilib = TikiLib::lib('tiki');
3194
3195		$matches = WikiParser_PluginMatcher::match($data);
3196		foreach ($matches as $match) {
3197			if ($match->getName() == 'code') {
3198				$match->replaceWith('');
3199			}
3200		}
3201
3202		$data = $matches->getText();
3203
3204		$htmlLinks = ["0" => "dummy"];
3205		$htmlLinksSefurl = ["0" => "dummy"];
3206		preg_match_all("/\(([a-z0-9-]+)?\( *($page_regex) *\)\)/", $data, $normal);
3207		preg_match_all("/\(([a-z0-9-]+)?\( *($page_regex) *\|(.+?)\)\)/", $data, $withDesc);
3208		preg_match_all('/<a class="wiki[^\"]*" href="tiki-index\.php\?page=([^\?&"]+)[^"]*"/', $data, $htmlLinks1);
3209		preg_match_all('/<a href="tiki-index\.php\?page=([^\?&"]+)[^"]*"/', $data, $htmlLinks2);
3210		$htmlLinks[1] = array_merge($htmlLinks1[1], $htmlLinks2[1]);
3211		$htmlLinks[1] = array_filter($htmlLinks[1]);
3212		preg_match_all('/<a class="wiki[^\"]*" href="([^\?&"]+)[^"]*"/', $data, $htmlLinksSefurl1);
3213		preg_match_all('/<a href="([^\?&"]+)[^"]*"/', $data, $htmlLinksSefurl2);
3214		$htmlLinksSefurl[1] = array_merge($htmlLinksSefurl1[1], $htmlLinksSefurl2[1]);
3215		preg_match_all('/<a class="wiki wikinew" href="tiki-editpage\.php\?page=([^\?&"]+)"/', $data, $htmlWantedLinks);
3216		// TODO: revise the need to call modified urldecode() (shouldn't be needed after r37568). 20110922
3217		foreach ($htmlLinks[1] as &$h) {
3218			$h = $tikilib->urldecode($h);
3219		}
3220		foreach ($htmlLinksSefurl[1] as &$h) {
3221			$h = $tikilib->urldecode($h);
3222		}
3223		foreach ($htmlWantedLinks[1] as &$h) {
3224			$h = $tikilib->urldecode($h);
3225		}
3226
3227		// Post process SEFURL for html wiki pages
3228		if (count($htmlLinksSefurl[1])) {
3229			// Remove any possible "tiki-index.php" in the SEFURL link list.
3230			//	Non-sefurl links will be mapped as "tiki-index.php"
3231			$tikiindex = [];
3232			foreach ($htmlLinksSefurl[1] as $pageName) {
3233				if (strpos($pageName, 'tiki-index.php') !== false) {
3234					$tikiindex[] = $pageName;
3235				}
3236			}
3237			$htmlLinksSefurl[1] = array_diff($htmlLinksSefurl[1], $tikiindex);
3238
3239			if (count($htmlLinksSefurl[1])) {
3240				// The case <a href=" ... will catch manually entered links. Only add links to wiki pages
3241				$pages = $tikilib->get_all_pages();
3242				$tikiindex = [];
3243				foreach ($htmlLinksSefurl[1] as $link) {
3244					// Validate that the link is to a wiki page
3245					if (! in_array($link, $pages)) {
3246						// If it's not referring to a wiki page, add it to the removal list
3247						$tikiindex[] = $link;
3248					}
3249				}
3250				$htmlLinksSefurl[1] = array_diff($htmlLinksSefurl[1], $tikiindex);
3251			}
3252		}
3253
3254		if ($prefs['feature_wikiwords'] == 'y') {
3255			preg_match_all("/([ \n\t\r\,\;]|^)?([A-Z][a-z0-9_\-]+[A-Z][a-z0-9_\-]+[A-Za-z0-9\-_]*)($|[ \n\t\r\,\;\.])/", $data, $wikiLinks);
3256
3257			$pageList = array_merge($normal[2], $withDesc[2], $wikiLinks[2], $htmlLinks[1], $htmlLinksSefurl[1], $htmlWantedLinks[1]);
3258			if ($withReltype) {
3259				$relList = array_merge(
3260					$normal[1],
3261					$withDesc[1],
3262					count($wikiLinks[2]) ? array_fill(0, count($wikiLinks[2]), null) : [],
3263					count($htmlLinks[1]) ? array_fill(0, count($htmlLinks[1]), null) : [],
3264					count($htmlLinksSefurl[1]) ? array_fill(0, count($htmlLinksSefurl[1]), null) : [],
3265					count($htmlWantedLinks[1]) ? array_fill(0, count($htmlWantedLinks[1]), null) : []
3266				);
3267			}
3268		} else {
3269			$pageList = array_merge($normal[2], $withDesc[2], $htmlLinks[1], $htmlLinksSefurl[1], $htmlWantedLinks[1]);
3270			if ($withReltype) {
3271				$relList = array_merge(
3272					$normal[1],
3273					$withDesc[1],
3274					count($htmlLinks[1]) ? array_fill(0, count($htmlLinks[1]), null) : [],
3275					count($htmlLinksSefurl[1]) ? array_fill(0, count($htmlLinksSefurl[1]), null) : [],
3276					count($htmlWantedLinks[1]) ? array_fill(0, count($htmlWantedLinks[1]), null) : []
3277				);
3278			}
3279		}
3280
3281		if ($withReltype) {
3282			$complete = [];
3283			foreach ($pageList as $idx => $name) {
3284				if (! array_key_exists($name, $complete)) {
3285					$complete[$name] = [];
3286				}
3287				if (! empty($relList[$idx]) && ! in_array($relList[$idx], $complete[$name])) {
3288					$complete[$name][] = $relList[$idx];
3289				}
3290			}
3291
3292			return $complete;
3293		} else {
3294			return array_unique($pageList);
3295		}
3296	}
3297
3298	private function get_hotwords()
3299	{
3300		$tikilib = TikiLib::lib('tiki');
3301		static $cache_hotwords;
3302		if (isset($cache_hotwords)) {
3303			return $cache_hotwords;
3304		}
3305		$query = "select * from `tiki_hotwords`";
3306		$result = $tikilib->fetchAll($query, [], -1, -1, false);
3307		$ret = [];
3308		foreach ($result as $res) {
3309			$ret[$res["word"]] = $res["url"];
3310		}
3311		$cache_hotwords = $ret;
3312		return $ret;
3313	}
3314
3315	/*
3316	* When we save a page that contains a TranslationOf plugin, we need to remember
3317	* that relation, so we later, when the page referenced by this plugin gets translated,
3318	* we can replace the plugin by a proper link to the translation.
3319	*/
3320	public function add_translationof_relation($data, $arguments, $page_being_parsed)
3321	{
3322		$relationlib = TikiLib::lib('relation');
3323
3324		$relationlib->add_relation('tiki.wiki.translationof', 'wiki page', $page_being_parsed, 'wiki page', $arguments['translation_page']);
3325	}
3326
3327	/**
3328	 * Refresh the list of plugins that might require validation
3329	 *
3330	 * @param \Psr\Log\LoggerInterface|null $logger
3331	 */
3332	public function pluginRefresh($logger = null)
3333	{
3334		$headerLib = \TikiLib::lib('header');
3335		$tempHeaderLib = serialize($headerLib);    // cache headerlib so we can remove all js etc added by plugins
3336
3337		$access = \TikiLib::lib('access');
3338		$access->check_feature('wiki_validate_plugin');
3339		$access->check_permission('tiki_p_plugin_approve');
3340
3341		$tikiLib = TikiLib::lib('tiki');
3342		$parserLib = TikiLib::lib('parser');
3343
3344		// disable redirect plugin etc
3345		$access->preventRedirect(true);
3346		$access->preventDisplayError(true);
3347
3348		$pages = $tikiLib->list_pages();
3349		foreach ($pages['data'] as $apage) {
3350			if ($logger instanceof Psr\Log\LoggerInterface) {
3351				$logger->debug(tr('Processing page: %0, is_html: %1', $apage['pageName'], $apage['is_html']));
3352			}
3353			$page = $apage['pageName'];
3354			$parserLib->setOptions(
3355				[
3356					'page' => $page,
3357					'is_html' => $apage['is_html'],
3358				]
3359			);
3360			$parserLib->parse_first($apage['data'], $pre, $no);
3361		}
3362
3363		$access->preventRedirect(false);
3364		$access->preventDisplayError(false);
3365
3366		$headerLib = unserialize($tempHeaderLib);
3367		unset($tempHeaderLib);
3368	}
3369
3370	/**
3371	 * Create a popup with thumbnail file preview
3372	 *
3373	 * @param $content
3374	 * @return string
3375	 */
3376	public function searchFilePreview($content)
3377	{
3378		global $prefs, $user, $tikipath, $tikidomain;
3379		if ($prefs['search_file_thumbnail_preview'] !== 'y') {
3380			return $content;
3381		}
3382
3383		preg_match_all('/data-type="file" data-object="(\d+)/', $content, $matchFiles);
3384		if (! empty($matchFiles[1])) {
3385			$fileIds = $matchFiles[1];
3386			$userLib = TikiLib::lib('user');
3387			$maxWidthPreview = $prefs['fgal_maximum_image_width_preview'];
3388
3389			foreach ($fileIds as $fileId) {
3390				$file = \Tiki\FileGallery\File::id($fileId);
3391				if (! $file->exists()) {
3392					continue;
3393				}
3394				if (! $userLib->user_has_perm_on_object($user, $file->fileId, 'file', 'tiki_p_download_files')) {
3395					continue;
3396				}
3397				$search = 'data-type="file" data-object="' . $fileId . '"';
3398				if (strpos($file->filetype, 'image') !== false) {
3399					$appendMaxSize = '';
3400
3401					if (! empty($maxWidthPreview)) {
3402						$appendMaxSize = "&amp;x=" . $maxWidthPreview;
3403					}
3404
3405					$popup = 'data-type="file" data-object="' . $fileId . '" data-toggle="popover" data-trigger="hover focus" data-content="<img src=\'tiki-download_file.php?fileId=' . $fileId . '&amp;thumbnail' . $appendMaxSize . '\'>" data-html="1" data-width="' . $maxWidthPreview . '"';
3406					$content = str_replace($search, $popup, $content);
3407				} else {
3408					$filePath = $file->getWrapper()->getReadableFile();
3409					$fileMd5 = $file->getWrapper()->getChecksum();
3410
3411					$cacheLib = TikiLib::lib('cache');
3412					$cacheName = $fileMd5;
3413					$cacheType = 'preview_' . $fileId . '_';
3414
3415					if (! $cacheLib->isCached($cacheName, $cacheType) && Tiki\Lib\Alchemy\AlchemyLib::isLibraryAvailable()) {
3416						// This will allow apps executed by Alchemy (like when converting doc to pdf) to have a writable home
3417						// save existing ENV
3418						$envHomeDefined = isset($_ENV) && array_key_exists('HOME', $_ENV);
3419						if ($envHomeDefined) {
3420							$envHomeCopy = $_ENV['HOME'];
3421						}
3422
3423						// set a proper home folder
3424						$_ENV['HOME'] = $tikipath . DIRECTORY_SEPARATOR . 'temp' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR . $tikidomain;
3425
3426						$targetFile = 'temp/target_' . $fileId . '.png';
3427
3428						Tiki\Lib\Alchemy\AlchemyLib::hintMimeTypeByFilePath($filePath, $file->filetype);
3429
3430						$alchemy = new Tiki\Lib\Alchemy\AlchemyLib();
3431						$height = 400;
3432						$width = 200;
3433
3434						if (! empty($prefs['fgal_maximum_image_width_preview'])) {
3435							$originalHeight = $height;
3436							$originalWidth = $width;
3437							$width = $prefs['fgal_maximum_image_width_preview'];
3438							$height = (int) $originalHeight * $width / $originalWidth;
3439						}
3440
3441						$alchemy->convertToImage($filePath, $targetFile, $height, $width, false);
3442
3443						// Restore the environment
3444						if ($envHomeDefined) {
3445							$_ENV['HOME'] = $envHomeCopy;
3446						} else {
3447							unset($_ENV['HOME']);
3448						}
3449
3450						if (file_exists($targetFile)) {
3451							$cacheContent = file_get_contents($targetFile);
3452							$cacheLib->empty_type_cache($cacheType);
3453							$cacheLib->cacheItem($cacheName, $cacheContent, $cacheType);
3454							unlink($targetFile);
3455						}
3456					}
3457
3458					if ($cacheLib->isCached($cacheName, $cacheType)) {
3459						$popup = 'data-type="file" data-object="' . $fileId . '" data-toggle="popover" data-trigger="hover focus" data-content="<img src=\'tiki-download_file.php?fileId=' . $fileId . '&amp;preview\'>" data-html="1" data-width="' . $maxWidthPreview . '"';
3460						$content = str_replace($search, $popup, $content);
3461					}
3462				}
3463			}
3464		}
3465
3466		return $content;
3467	}
3468}
3469