1<?php
2/*
3* e107 website system
4*
5* Copyright (C) 2008-2016 e107 Inc (e107.org)
6* Released under the terms and conditions of the
7* GNU General Public License (http://www.gnu.org/licenses/gpl.txt)
8*
9* Text processing and parsing functions
10*
11*/
12
13if (!defined('e107_INIT')) { exit(); }
14
15// Directory for the hard-coded utf-8 handling routines
16define('E_UTF8_PACK', e_HANDLER.'utf8/');
17
18define("E_NL", chr(2));
19
20class e_parse extends e_parser
21{
22	/**
23	 * Determine how to handle utf-8.
24	 *    0 = 'do nothing'
25	 *    1 = 'use mb_string'
26	 *    2 = emulation
27	 *
28	 * @var integer
29	 */
30	protected $utfAction;
31
32	// Shortcode processor - see __get()
33	//var $e_sc;
34
35	// BBCode processor
36	var $e_bb;
37
38	// Profanity filter
39	var $e_pf;
40
41	// Emote filter
42	var $e_emote;
43
44	// 'Hooked' parsers (array)
45	var $e_hook;
46
47	var $search = array('&amp;#039;', '&#039;', '&#39;', '&quot;', 'onerror', '&gt;', '&amp;quot;', ' & ');
48
49	var $replace = array("'", "'", "'", '"', 'one<i></i>rror', '>', '"', ' &amp; ');
50
51	// Set to TRUE or FALSE once it has been calculated
52	var $e_highlighting;
53
54	// Highlight query
55	var $e_query;
56
57	public $thumbWidth = 100;
58
59	public $thumbHeight = 0;
60
61	public $thumbCrop = 0;
62
63	private $thumbEncode = 0;
64
65	private $staticCount = 0;
66
67	// BBcode that contain preformatted code.
68	private $preformatted = array('html', 'markdown');
69
70
71	// Set up the defaults
72	var $e_optDefault = array(
73		// default context: reflects legacy settings (many items enabled)
74		'context' 		=> 'OLDDEFAULT',
75		//
76		'fromadmin' 	=> FALSE,
77
78		// Enable emote display
79		'emotes'		=> TRUE,
80
81		// Convert defines(constants) within text.
82		'defs' 			=> FALSE,
83
84		// replace all {e_XXX} constants with their e107 value - 'rel' or 'abs'
85		'constants' 	=> FALSE,
86
87		// Enable hooked parsers
88		'hook'			=> TRUE,
89
90		// Allow scripts through (new for 0.8)
91		'scripts'		=> TRUE,
92
93		// Make links clickable
94		'link_click'	=> TRUE,
95
96		// Substitute on clickable links (only if link_click == TRUE)
97		'link_replace'	=> TRUE,
98
99		// Parse shortcodes - TRUE enables parsing
100
101		'parse_sc' 		=> FALSE,
102		// remove HTML tags.
103		'no_tags' 		=> FALSE,
104
105		// Restore entity form of quotes and such to single characters - TRUE disables
106		'value'			=> FALSE,
107
108		// Line break compression - TRUE removes newline characters
109		'nobreak' 		=> FALSE,
110
111		// Retain newlines - wraps to \n instead of <br /> if TRUE (for non-HTML email text etc)
112		'retain_nl' 	=> FALSE
113		);
114
115	// Super modifiers override default option values
116	var	$e_SuperMods = array(
117				//text is part of a title (e.g. news title)
118				'TITLE' =>
119					array(
120						'nobreak'=>TRUE, 'retain_nl'=>TRUE, 'link_click' => FALSE, 'emotes'=>FALSE, 'defs'=>TRUE, 'parse_sc'=>TRUE
121						),
122				'TITLE_PLAIN' =>
123					array(
124						'nobreak'=>TRUE, 'retain_nl'=>TRUE, 'link_click' => FALSE, 'emotes'=>FALSE, 'defs'=>TRUE, 'parse_sc'=>TRUE, 'no_tags' => TRUE
125						),
126				//text is user-entered (i.e. untrusted) and part of a title (e.g. forum title)
127				'USER_TITLE' =>
128					array(
129						'nobreak'=>TRUE, 'retain_nl'=>TRUE, 'link_click' => FALSE, 'scripts' => FALSE, 'emotes'=>FALSE, 'hook'=>FALSE
130						),
131				// text is 'body' of email or similar - being sent 'off-site' so don't rely on server availability
132				'E_TITLE' =>
133					array(
134						'nobreak'=>TRUE, 'retain_nl'=>TRUE, 'defs'=>TRUE, 'parse_sc'=>TRUE, 'emotes'=>FALSE, 'scripts' => FALSE, 'link_click' => FALSE
135						),
136				// text is part of the summary of a longer item (e.g. content summary)
137				'SUMMARY' =>
138					array(
139						'defs'=>TRUE, 'constants'=>'full', 'parse_sc'=>TRUE
140						),
141				// text is the description of an item (e.g. download, link)
142				'DESCRIPTION' =>
143					array(
144						'defs'=>TRUE, 'constants'=>'full', 'parse_sc'=>TRUE
145						),
146				// text is 'body' or 'bulk' text (e.g. custom page body, content body)
147				'BODY' =>
148					array(
149						'defs'=>TRUE, 'constants'=>'full', 'parse_sc'=>TRUE
150						),
151				// text is parsed by the Wysiwyg editor. eg. TinyMce
152				'WYSIWYG' =>
153					array(
154							'hook' => false, 'link_click' => false, 'link_replace' => false, 'retain_nl' => true
155						),
156				// text is user-entered (i.e. untrusted)'body' or 'bulk' text (e.g. custom page body, content body)
157				'USER_BODY' =>
158					array(
159						'constants'=>'full', 'scripts' => FALSE, 'nostrip'=>FALSE
160						),
161				// text is 'body' of email or similar - being sent 'off-site' so don't rely on server availability
162				'E_BODY' =>
163					array(
164						'defs'=>TRUE, 'constants'=>'full', 'parse_sc'=>TRUE, 'emotes'=>FALSE, 'scripts' => FALSE, 'link_click' => FALSE
165						),
166				// text is text-only 'body' of email or similar - being sent 'off-site' so don't rely on server availability
167				'E_BODY_PLAIN' =>
168					array(
169						'defs'=>TRUE, 'constants'=>'full', 'parse_sc'=>TRUE, 'emotes'=>FALSE, 'scripts' => FALSE, 'link_click' => FALSE, 'retain_nl' => TRUE, 'no_tags' => TRUE
170						),
171				// text is the 'content' of a link (A tag, etc)
172				'LINKTEXT' =>
173					array(
174						'nobreak'=>TRUE, 'retain_nl'=>TRUE, 'link_click' => FALSE, 'emotes'=>FALSE, 'hook'=>FALSE, 'defs'=>TRUE, 'parse_sc'=>TRUE
175						),
176				// text is used (for admin edit) without fancy conversions or html.
177				'RAWTEXT' =>
178					array(
179						'nobreak'=>TRUE, 'retain_nl'=>TRUE, 'link_click' => FALSE, 'emotes'=>FALSE, 'hook'=>FALSE, 'no_tags'=>TRUE
180						)
181		);
182
183	// Individual modifiers change the current context
184	var $e_Modifiers = array(
185				'emotes_off'	=> array('emotes' => FALSE),
186				'emotes_on'		=> array('emotes' => TRUE),
187				'no_hook'		=> array('hook' => FALSE),
188				'do_hook'		=> array('hook' => TRUE),
189				// New for 0.8
190				'scripts_off'	=> array('scripts' => FALSE),
191				// New for 0.8
192				'scripts_on'	=> array('scripts' => TRUE),
193				'no_make_clickable' => array('link_click' => FALSE),
194				'make_clickable' => array('link_click' => TRUE),
195				'no_replace' 	=> array('link_replace' => FALSE),
196				// Replace text of clickable links (only if make_clickable option set)
197				'replace' 		=> array('link_replace' => TRUE),
198				// No path replacement
199				'consts_off'	=> array('constants' => FALSE),
200				// Relative path replacement
201				'consts_rel'	=> array('constants' => 'rel'),
202				// Absolute path replacement
203				'consts_abs'	=> array('constants' => 'abs'),
204				// Full path replacement
205				'consts_full'	=> array('constants' => 'full'),
206				// No shortcode parsing
207				'scparse_off'	=> array('parse_sc'	=> FALSE),
208
209				'scparse_on'	=> array('parse_sc'	=> TRUE),
210				// Strip tags
211				'no_tags' 		=> array('no_tags' 	=> TRUE),
212				// Leave tags
213				'do_tags' 		=> array('no_tags' 	=> FALSE),
214
215				'fromadmin'		=> array('fromadmin' => TRUE),
216				'notadmin'		=> array('fromadmin' => FALSE),
217				// entity replacement
218				'er_off'		=> array('value' => FALSE),
219				'er_on'			=> array('value' => TRUE),
220				// Decode constant if exists
221				'defs_off'		=> array('defs' => FALSE),
222				'defs_on'		=> array('defs' => TRUE),
223
224				'dobreak'		=> array('nobreak' => FALSE),
225				'nobreak'		=> array('nobreak' => TRUE),
226				// Line break using \n
227				'lb_nl'			=> array('retain_nl' => TRUE),
228				// Line break using <br />
229				'lb_br'			=> array('retain_nl' => FALSE),
230
231				// Legacy option names below here - discontinue later
232				'retain_nl'		=> array('retain_nl' => TRUE),
233				'defs'			=> array('defs' => TRUE),
234				'parse_sc'		=> array('parse_sc'	=> TRUE),
235				'constants'		=> array('constants' => 'rel'),
236				'value'			=> array('value' => TRUE),
237				'wysiwyg'		=> array('wysiwyg'=>TRUE)
238		);
239
240
241	/**
242	 * Constructor - keep it public for backward compatibility
243	 still some new e_parse() in the core
244	 *
245	 */
246	public function __construct()
247	{
248		// initialise the type of UTF-8 processing methods depending on PHP version and mb string extension
249		parent::__construct();
250
251
252		$this->init();
253		$this->initCharset();
254
255		// Preprocess the supermods to be useful default arrays with all values
256		foreach ($this->e_SuperMods as $key => $val)
257		{
258			// precalculate super defaults
259			$this->e_SuperMods[$key] = array_merge($this->e_optDefault , $this->e_SuperMods[$key]);
260			$this->e_SuperMods[$key]['context'] = $key;
261		}
262	}
263
264
265	/**
266	 * Initialise the type of UTF-8 processing methods depending on PHP version and mb string extension.
267	 *
268	 * NOTE: can't be called until CHARSET is known
269	 but we all know that it is UTF-8 now
270	 *
271	 * @return void
272	 */
273	private function initCharset()
274	{
275		// Start by working out what, if anything, we do about utf-8 handling.
276		// 'Do nothing' is the simple option
277		$this->utfAction = 0;
278// CHARSET is utf-8
279//		if(strtolower(CHARSET) == 'utf-8')
280//		{
281			if(version_compare(PHP_VERSION, '6.0.0') < 1)
282			{
283				// Need to do something here
284				if(extension_loaded('mbstring'))
285				{
286					// Check for function overloading
287					$temp = ini_get('mbstring.func_overload');
288					// Just check the string functions - will be non-zero if overloaded
289					if(($temp & MB_OVERLOAD_STRING) == 0)
290					{
291						// Can use the mb_string routines
292						$this->utfAction = 1;
293					}
294					// Set the default encoding, so we don't have to specify every time
295					mb_internal_encoding('UTF-8');
296				}
297				else
298				{
299					// Must use emulation - will probably be slow!
300					$this->utfAction = 2;
301					require_once(E_UTF8_PACK.'utils/unicode.php');
302					// Always load the core routines - bound to need some of them!
303					require_once(E_UTF8_PACK.'native/core.php');
304				}
305			}
306//		}
307	}
308
309
310	/**
311	 * Unicode (UTF-8) analogue of standard @link http://php.net/strlen strlen PHP function.
312	 * Returns the length of the given string.
313	 *
314	 * @param string $str The UTF-8 encoded string being measured for length.
315	 * @return integer The length (amount of UTF-8 characters) of the string on success, and 0 if the string is empty.
316	 */
317	public function ustrlen($str)
318	{
319		switch($this->utfAction)
320		{
321			case 0:
322				return strlen($str);
323			case 1:
324				return mb_strlen($str);
325		}
326		// Default case shouldn't happen often
327		// Save a call - invoke the function directly
328		return strlen(utf8_decode($str));
329	}
330
331
332	/**
333	 * Unicode (UTF-8) analogue of standard @link http://php.net/strtolower strtolower PHP function.
334	 * Make a string lowercase.
335	 *
336	 * @param string $str The UTF-8 encoded string to be lowercased.
337	 * @return string Specified string with all alphabetic characters converted to lowercase.
338	 */
339	public function ustrtolower($str)
340	{
341		switch($this->utfAction)
342		{
343			case 0:
344				return strtolower($str);
345			case 1:
346				return mb_strtolower($str);
347		}
348		// Default case shouldn't happen often
349		return utf8_strtolower($str);
350	}
351
352
353	/**
354	 * Unicode (UTF-8) analogue of standard @link http://php.net/strtoupper strtoupper PHP function.
355	 * Make a string uppercase.
356	 *
357	 * @param string $str The UTF-8 encoded string to be uppercased.
358	 * @return string Specified string with all alphabetic characters converted to uppercase.
359	 */
360	public function ustrtoupper($str)
361	{
362		switch($this->utfAction)
363		{
364			case 0:
365				return strtoupper($str);
366			case 1:
367				return mb_strtoupper($str);
368		}
369		// Default case shouldn't happen often
370		return utf8_strtoupper($str);
371	}
372
373
374	/**
375	 * Unicode (UTF-8) analogue of standard @link http://php.net/strpos strpos PHP function.
376	 * Find the position of the first occurrence of a case-sensitive UTF-8 encoded string.
377	 * Returns the numeric position (offset in amount of UTF-8 characters)
378	 *  of the first occurrence of needle in the haystack string.
379	 *
380	 * @param string $haystack The UTF-8 encoded string being searched in.
381	 * @param integer $needle The UTF-8 encoded string being searched for.
382	 * @param integer $offset [optional] The optional offset parameter allows you to specify which character in haystack to start searching.
383	 * 				 The position returned is still relative to the beginning of haystack.
384	 * @return integer|boolean Returns the position as an integer. If needle is not found, the function will return boolean FALSE.
385	 */
386	public function ustrpos($haystack, $needle, $offset = 0)
387	{
388		switch($this->utfAction)
389		{
390			case 0:
391				return strpos($haystack, $needle, $offset);
392			case 1:
393				return mb_strpos($haystack, $needle, $offset);
394		}
395		return utf8_strpos($haystack, $needle, $offset);
396	}
397
398
399	/**
400	 * Unicode (UTF-8) analogue of standard @link http://php.net/strrpos strrpos PHP function.
401	 * Find the position of the last  occurrence of a case-sensitive UTF-8 encoded string.
402	 * Returns the numeric position (offset in amount of UTF-8 characters)
403	 *  of the last occurrence of needle in the haystack string.
404	 *
405	 * @param string $haystack The UTF-8 encoded string being searched in.
406	 * @param integer $needle The UTF-8 encoded string being searched for.
407	 * @param integer $offset [optional] - The optional offset parameter allows you to specify which character in haystack to start searching.
408	 * 				 The position returned is still relative to the beginning of haystack.
409	 * @return integer|boolean Returns the position as an integer. If needle is not found, the function will return boolean FALSE.
410	 */
411	public function ustrrpos($haystack, $needle, $offset = 0)
412	{
413		switch($this->utfAction)
414		{
415			case 0:
416				return strrpos($haystack, $needle, $offset);
417			case 1:
418				return mb_strrpos($haystack, $needle, $offset);
419		}
420		return utf8_strrpos($haystack, $needle, $offset);
421	}
422
423
424	/**
425	 * Unicode (UTF-8) analogue of standard @link http://php.net/stristr stristr PHP function.
426	 * Returns all of haystack starting from and including the first occurrence of needle to the end.
427	 *
428	 * @param string $haystack The UTF-8 encoded string to search in.
429	 * @param mixed $needle If needle is not a string, it is converted to an integer and applied as the ordinal value of a character.
430	 * @param integer $length [optional] (PHP 5.3+) If TRUE, returns the part of the haystack before the first occurrence of the needle (excluding needle).
431	 * @return string Returns the matched substring. If needle is not found, returns FALSE.
432	 */
433	public function ustristr($haystack, $needle, $before_needle = false)
434	{
435		switch($this->utfAction)
436		{
437			case 0:
438				return stristr($haystack, $needle, $before_needle);
439			case 1:
440				//return mb_substr($haystack, $needle, $before_needle);
441				return mb_stristr($haystack, $needle, $before_needle);
442		}
443		// No utf8 pack backup
444		return stristr($haystack, $needle, $before_needle);
445	}
446
447	/**
448	 * Unicode (UTF-8) analogue of standard @link http://php.net/substr substr PHP function.
449	 * Returns the portion of string specified by the start and length parameters.
450	 *
451	 * NOTE: May be subtle differences in return values dependent on which routine is used.
452	 *  Native substr() routine can return FALSE. mb_substr() and utf8_substr() just return an empty string.
453	 *
454	 * @param string $str The UTF-8 encoded string.
455	 * @param integer $start Start of portion to be returned. Position is counted in amount of UTF-8 characters from the beginning of str.
456	 * 				First character's position is 0. Second character position is 1, and so on.
457	 * @param integer $length [optional] If length is given, the string returned will contain at most length characters beginning from start
458	 * 				(depending on the length of string). If length is omitted, the rest of string from start will be returned.
459	 * @return string The extracted UTF-8 encoded part of input string.
460	 */
461	public function usubstr($str, $start, $length = NULL)
462	{
463		switch($this->utfAction)
464		{
465			case 0:
466				return substr($str, $start, $length);
467			case 1:
468				if(is_null($length))
469				{
470					return mb_substr($str, $start);
471				}
472				else
473				{
474					return mb_substr($str, $start, $length);
475				}
476		}
477		return utf8_substr($str, $start, $length);
478	}
479
480	/**
481	 * Converts the supplied text (presumed to be from user input) to a format suitable for storing in a database table.
482	 *
483	 * @param mixed $data
484	 * @param boolean $nostrip [optional] Assumes all data is GPC ($_GET, $_POST, $_COOKIE) unless indicate otherwise by setting this var to TRUE.
485	 * 				If magic quotes is enabled on the server and you do not tell toDB() that the data is non GPC then slashes will be stripped when they should not be.
486	 * @param boolean $no_encode [optional] This parameter should nearly always be FALSE. It is used by the save_prefs() function to preserve HTML content within prefs even when
487	 * 				the save_prefs() function has been called by a non admin user / user without html posting permissions.
488	 * @param boolean|string $mod [optional] model = admin-ui usage. The 'no_html' and 'no_php' modifiers blanket prevent HTML and PHP posting regardless of posting permissions. (used in logging)
489	 *		The 'pReFs' value is for internal use only, when saving prefs, to prevent sanitisation of HTML.
490	 * @param mixed $parm [optional]
491	 * @return string|array
492	 * @todo complete the documentation of this essential method
493	 */
494	public function toDB($data = null, $nostrip =false, $no_encode = false, $mod = false, $parm = null)
495	{
496		if($data === null)
497		{
498			return null;
499		}
500
501		if (is_array($data))
502		{
503			$ret = array();
504
505			foreach ($data as $key => $var)
506			{
507				//Fix - sanitize keys as well
508				$key = filter_var($key,FILTER_SANITIZE_STRING);
509				$ret[$key] = $this->toDB($var, $nostrip, $no_encode, $mod, $parm);
510			}
511
512			return $ret;
513		}
514
515		if (MAGIC_QUOTES_GPC == true && $nostrip == false)
516		{
517			$data = stripslashes($data);
518		}
519
520		if(intval($data) === $data || $data === '0') // simple integer.
521		{
522			return $data;
523		}
524
525		$core_pref = e107::getConfig();
526
527		if ($mod !== 'pReFs') //XXX We're not saving prefs.
528		{
529
530			$data = $this->preFilter($data); // used by bb_xxx.php toDB() functions. bb_code.php toDB() allows us to properly bypass HTML cleaning below.
531			$data = $this->cleanHtml($data); // clean it regardless of if it is text or html. (html could have missing closing tags)
532
533			if(($this->isHtml($data)) && strpos($mod, 'no_html') === false)
534			{
535				$this->isHtml = true;
536			//	$data = $this->cleanHtml($data); // sanitize all html. (moved above to include everything)
537
538				$data = str_replace(array('%7B','%7D'),array('{','}'),$data); // fix for {e_XXX} paths.
539			}
540			else // caused double-encoding of '&'
541			{
542			//	$data = str_replace('&amp;','&',$data);
543		//		$data = str_replace('<','&lt;',$data);
544		//		$data = str_replace('>','&gt;',$data);
545			//	$data = str_replace('&','&amp;',$data);
546
547			}
548
549
550			if (!check_class($core_pref->get('post_html', e_UC_MAINADMIN)))
551			{
552				$data = strip_tags($data); // remove tags from cleaned html.
553				$data = str_replace(array('[html]','[/html]'),'',$data);
554			}
555
556			//  $data = html_entity_decode($data, ENT_QUOTES, 'utf-8');	// Prevent double-entities. Fix for [code]  - see bb_code.php toDB();
557		}
558
559
560
561		if (check_class($core_pref->get('post_html'))) /*$core_pref->is('post_html') && XXX preformecd by cleanHtml() */
562		{
563			$no_encode = true;
564		}
565
566		if($parm !== null && is_numeric($parm) && !check_class($core_pref->get('post_html'), '', $parm))
567		{
568			$no_encode = false;
569		}
570
571		if ($no_encode === true && strpos($mod, 'no_html') === false)
572		{
573			$search = array('$', '"', "'", '\\', '<?');
574			$replace = array('&#036;', '&quot;', '&#039;', '&#092;', '&lt;?');
575			$ret = str_replace($search, $replace, $data);
576		}
577		else // add entities for everything. we want to save the code.
578		{
579
580			$search = array('&gt;', '&lt;');
581			$replace = array('>', '<');
582			$data = str_replace($search, $replace, $data); // prevent &amp;gt; etc.
583
584			$data = htmlspecialchars($data, ENT_QUOTES, 'UTF-8');
585			$data = str_replace('\\', '&#092;', $data);
586
587			$ret = preg_replace("/&amp;#(\d*?);/", "&#\\1;", $data);
588		}
589
590		// XXX - php_bbcode has been deprecated.
591		if ((strpos($mod, 'no_php') !== false) || !check_class($core_pref->get('php_bbcode')))
592		{
593			$ret = preg_replace("#\[(php)#i", "&#91;\\1", $ret);
594		}
595
596		// Don't allow hooks to mess with prefs.
597		if($mod !== 'model')
598		{
599			return $ret;
600		}
601
602
603		/**
604		 * e_parse hook
605		 */
606		$eParseList = $core_pref->get('e_parse_list');
607		if(!empty($eParseList))
608		{
609
610			$opts = array(
611				'nostrip'   => $nostrip,
612				'noencode'  => $no_encode,
613				'type'      => $parm['type'],
614				'field'     => $parm['field']
615			);
616
617			foreach($eParseList as $plugin)
618			{
619				$hookObj = e107::getAddon($plugin, 'e_parse');
620				if($tmp = e107::callMethod($hookObj, 'toDB', $ret, $opts))
621				{
622					$ret = $tmp;
623				}
624
625			}
626
627		}
628
629
630		return $ret;
631	}
632
633
634
635	/**
636	 *	Check for umatched 'dangerous' HTML tags
637	 *		(these can destroy page layout where users are able to post HTML)
638	 * @DEPRECATED
639	 *	@param string $data
640	 *	@param string $tagList - if empty, uses default list of input tags. Otherwise a CSV list of tags to check (any type)
641	 *
642	 *	@return boolean TRUE if an unopened closing tag found
643	 *					FALSE if nothing found
644	 */
645	function htmlAbuseFilter($data, $tagList = '')
646	{
647
648		if ($tagList == '')
649		{
650			$checkTags = array('textarea', 'input', 'td', 'tr', 'table');
651		}
652		else
653		{
654			$checkTags = explode(',', $tagList);
655		}
656		$tagArray = array_flip($checkTags);
657		foreach ($tagArray as &$v) { $v = 0; };		// Data fields become zero; keys are tag names.
658		$data = strtolower(preg_replace('#\[code\].*?\[\/code\]#i', '', $data));            // Ignore code blocks. All lower case simplifies the rest
659		$matches = array();
660		if (!preg_match_all('#<(\/|)([^<>]*?[^\/])>#', $data, $matches, PREG_SET_ORDER))
661		{
662			//echo "No tags found<br />";
663			return TRUE;				// No tags found; so all OK
664		}
665		//print_a($matches);
666		foreach ($matches as $m)
667		{
668			// $m[0] is the complete tag; $m[1] is '/' or empty; $m[2] is the tag and any attributes
669			list ($tag) = explode(' ', $m[2], 2);
670			if (!isset($tagArray[$tag])) continue;			// Not a tag of interest
671			if ($m[1] == '/')
672			{	// Closing tag
673				if ($tagArray[$tag] == 0)
674				{
675					//echo "Close before open: {$tag}<br />";
676					return TRUE;		// Closing tag before we've had an opening tag
677				}
678				$tagArray[$tag]--;		// Obviously had at least one opening tag
679			}
680			else
681			{	// Opening tag
682				$tagArray[$tag]++;
683			}
684		}
685		//print_a($tagArray);
686		foreach ($tagArray as $t)
687		{
688			if ($t > 0) return TRUE;		// More opening tags than closing tags
689		}
690		return FALSE;						// OK now
691	}
692
693
694
695
696	/**
697	 * @DEPRECATED XXX TODO Remove this horrible thing which adds junk to a db.
698	 *	Checks a string for potentially dangerous HTML tags, including malformed tags
699	 *
700	 */
701	public function dataFilter($data, $mode='bbcode')
702	{
703
704
705		$ans = '';
706		$vetWords = array('<applet', '<body', '<embed', '<frame', '<script','%3Cscript',
707						 '<frameset', '<html', '<iframe', '<style', '<layer', '<link',
708						 '<ilayer', '<meta', '<object', '<plaintext', 'javascript:',
709						 'vbscript:','data:text/html');
710
711		$ret = preg_split('#(\[code.*?\[/code.*?])#mis', $data, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE );
712
713		foreach ($ret as $s)
714		{
715			if (substr($s, 0, 5) != '[code')
716			{
717				$vl = array();
718				$t = html_entity_decode(rawurldecode($s), ENT_QUOTES, CHARSET);
719				$t = str_replace(array("\r", "\n", "\t", "\v", "\f", "\0"), '', $t);
720				$t1 = strtolower($t);
721				foreach ($vetWords as $vw)
722				{
723					if (strpos($t1, $vw) !== FALSE)
724					{
725						$vl[] = $vw;		// Add to list of words found
726					}
727					if (substr($vw, 0, 1) == '<')
728					{
729						$vw = '</'.substr($vw, 1);
730						if (strpos($t1, $vw) !== FALSE)
731						{
732							$vl[] = $vw;		// Add to list of words found
733						}
734					}
735				}
736				// More checks here
737				if (count($vl))
738				{	// Do something
739					$s = preg_replace_callback('#('.implode('|', $vl).')#mis', array($this, 'modtag'), $t);
740				}
741			}
742			$s = preg_replace('#(?:onmouse.+?|onclick|onfocus)\s*?\=#', '[sanitised]$0[/sanitised]', $s);
743			$s = preg_replace_callback('#base64([,\(])(.+?)([\)\'\"])#mis', array($this, 'proc64'), $s);
744			$ans .= $s;
745		}
746
747		if($mode == 'link' && count($vl))
748		{
749			return "#sanitized";
750		}
751
752		return $ans;
753	}
754
755
756	/**
757	 * Check base-64 encoded code
758	 */
759	private function proc64($match)
760	{
761		$decode = base64_decode($match[2]);
762		return 'base64'.$match[1].base64_encode($this->dataFilter($decode)).$match[3];
763	}
764
765
766	// XXX REmove ME.
767	private function modTag($match)
768	{
769		$ans = '';
770		if (isset($match[1]))
771		{
772			$chop = intval(strlen($match[1]) / 2);
773			$ans = substr($match[1], 0, $chop).'##xss##'.substr($match[1], $chop);
774		}
775		else
776		{
777			$ans = '?????';
778		}
779		return '[sanitised]'.$ans.'[/sanitised]';
780
781	}
782
783
784
785	/**
786	 *	Processes data as needed before its written to the DB.
787	 *	Currently gives bbcodes the opportunity to do something
788	 *
789	 *	@param $data string - data about to be written to DB
790	 *	@return string - modified data
791	 */
792	public function preFilter($data)
793	{
794		if (!is_object($this->e_bb))
795		{
796			require_once(e_HANDLER.'bbcode_handler.php');
797			$this->e_bb = new e_bbcode;
798		}
799		$ret = $this->e_bb->parseBBCodes($data, USERID, 'default', 'PRE');			// $postID = logged in user here
800		return $ret;
801	}
802
803
804
805
806	function toForm($text)
807	{
808
809		if(empty($text)) // fix - handle proper 0, Space etc values.
810		{
811			return $text;
812		}
813
814
815		if(is_string($text) && substr($text,0,6) == '[html]')
816		{
817			// $text = $this->toHTML($text,true);
818			$search = array('&quot;','&#039;','&#092;', '&',); // '&' must be last.
819			$replace = array('"',"'","\\", '&amp;');
820
821		//	return htmlspecialchars_decode($text);
822			$text = str_replace($search,$replace,$text);
823		//	return $text;
824			//$text  = htmlentities($text,ENT_NOQUOTES, "UTF-8");
825
826		//	return $text;
827
828		}
829	//	return htmlentities($text);
830
831		$search = array('&#036;', '&quot;', '<', '>', '+');
832		$replace = array('$', '"', '&lt;', '&gt;', '%2B');
833		$text = str_replace($search, $replace, $text);
834		if (e107::wysiwyg() !== true && is_string($text))
835		{
836			// fix for utf-8 issue with html_entity_decode(); ???
837			$text = urldecode($text);
838		//	$text = str_replace("&nbsp;", " ", $text);
839		}
840		return $text;
841	}
842
843
844	function post_toForm($text)
845	{
846		if(is_array($text))
847		{
848			foreach ($text as $key=>$value)
849			{
850				$text[$this->post_toForm($key)] = $this->post_toForm($value);
851			}
852			return $text;
853		}
854		if(MAGIC_QUOTES_GPC == TRUE)
855		{
856			$text = stripslashes($text);
857		}
858		return str_replace(array("'", '"', "<", ">"), array("&#039;", "&quot;", "&lt;", "&gt;"), $text);
859	}
860
861
862	function post_toHTML($text, $original_author = FALSE, $extra = '', $mod = FALSE)
863	{
864		$text = $this->toDB($text, FALSE, FALSE, $mod, $original_author);
865		return $this->toHTML($text, TRUE, $extra);
866	}
867
868	/**
869	 * @param $text - template to parse.
870	 * @param boolean $parseSCFiles - parse core 'single' shortcodes
871	 * @param object|array $extraCodes - shortcode class containing sc_xxxxx methods or an array of key/value pairs or legacy shortcode content (eg. content within .sc)
872	 * @param object $eVars - XXX more info needed.
873	 * @return string
874	 */
875	function parseTemplate($text, $parseSCFiles = true, $extraCodes = null, $eVars = null)
876	{
877
878		if(!is_bool($parseSCFiles))
879		{
880			trigger_error("\$parseSCFiles in parseTemplate() was given incorrect data");
881		}
882
883		return e107::getScParser()->parseCodes($text, $parseSCFiles, $extraCodes, $eVars);
884	}
885
886
887	/**
888	 * Check if we are using the simple-Parse array format, or a legacy .sc format which contains 'return '
889	 * @param array $extraCodes
890	 */
891	private function isSimpleParse($extraCodes)
892	{
893
894		if(!is_array($extraCodes))
895		{
896			return false;
897		}
898
899		foreach ($extraCodes as $sc => $code)
900		{
901			if(preg_match('/return(.*);/',$code)) // still problematic. 'return;' Might be used in common speech.
902			{
903				return false;
904			}
905			else
906			{
907				return true;
908			}
909		/*	if(!strpos($code, 'return '))
910			{
911				return true;
912			}
913			else
914			{
915				return false;
916			}*/
917		}
918	}
919
920
921
922	/**
923	 * Simple parser
924	 *
925	 * @param string $template
926	 * @param e_vars|array $vars
927	 * @param string $replaceUnset string to be used if replace variable is not set, false - don't replace
928	 * @return string parsed content
929	 */
930	function simpleParse($template, $vars, $replaceUnset='')
931	{
932		$this->replaceVars = $vars;
933		$this->replaceUnset = $replaceUnset;
934		return preg_replace_callback("#\{([a-zA-Z0-9_]+)\}#", array($this, 'simpleReplace'), $template);
935	}
936
937	protected function simpleReplace($tmp)
938	{
939
940		$unset = ($this->replaceUnset !== false ? $this->replaceUnset : $tmp[0]);
941
942		if(is_array($this->replaceVars))
943		{
944            $this->replaceVars = new e_vars($this->replaceVars);
945			//return ($this->replaceVars[$key] !== null ? $this->replaceVars[$key]: $unset);
946		}
947		$key = $tmp[1]; // PHP7 fix.
948		return ($this->replaceVars->$key !== null ? $this->replaceVars->$key : $unset); // Doesn't work.
949	}
950
951
952	function htmlwrap($str, $width, $break = "\n", $nobreak = "a", $nobr = "pre", $utf = FALSE)
953	{
954		/*
955		Pretty well complete rewrite to try and handle utf-8 properly.
956		Breaks each utf-8 'word' every $width characters max. If possible, breaks after 'safe' characters.
957		$break is the character inserted to flag the break.
958		$nobreak is a list of tags within which word wrap is to be inactive
959		*/
960
961		//TODO handle htmlwrap somehow
962		//return $str;
963
964		// Don't wrap if non-numeric width
965		$width = intval($width);
966		// And trap stupid wrap counts
967		if ($width < 6)
968			return $str;
969
970		// Transform protected element lists into arrays
971		$nobreak = explode(" ", strtolower($nobreak));
972
973		// Variable setup
974		$intag = FALSE;
975		$innbk = array();
976		$drain = "";
977
978		// List of characters it is "safe" to insert line-breaks at
979		// It is not necessary to add < and > as they are automatically implied
980		$lbrks = "/?!%)-}]\\\"':;&";
981
982		// Is $str a UTF8 string?
983		if ($utf || strtolower(CHARSET) == 'utf-8')
984		{
985			// 0x1680, 0x180e, 0x2000-0x200a, 0x2028, 0x205f, 0x3000 are 'non-ASCII' Unicode UCS-4 codepoints - see http://www.unicode.org/Public/UNIDATA/UnicodeData.txt
986			// All convert to 3-byte utf-8 sequences:
987			// 0x1680	0xe1	0x9a	0x80
988			// 0x180e	0xe1	0xa0	0x8e
989			// 0x2000	0xe2	0x80	0x80
990			//   -
991			// 0x200a	0xe2	0x80	0x8a
992			// 0x2028	0xe2	0x80	0xa8
993			// 0x205f	0xe2	0x81	0x9f
994			// 0x3000	0xe3	0x80	0x80
995			$utf8 = 'u';
996			$whiteSpace = '#([\x20|\x0c]|[\xe1][\x9a][\x80]|[\xe1][\xa0][\x8e]|[\xe2][\x80][\x80-\x8a,\xa8]|[\xe2][\x81][\x9f]|[\xe3][\x80][\x80]+)#';
997			// Have to explicitly enumerate the whitespace chars, and use non-utf-8 mode, otherwise regex fails on badly formed utf-8
998		}
999		else
1000		{
1001			$utf8 = '';
1002			// For non-utf-8, can use a simple match string
1003			$whiteSpace = '#(\s+)#';
1004		}
1005
1006
1007		// Start of the serious stuff - split into HTML tags and text between
1008		$content = preg_split('#(<.*?'.'>)#mis', $str, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE );
1009		foreach($content as $value)
1010		{
1011			if ($value[0] == "<")
1012			{
1013				// We are within an HTML tag
1014				// Create a lowercase copy of this tag's contents
1015				$lvalue = strtolower(substr($value, 1, -1));
1016				if ($lvalue)
1017				{
1018					// Tag of non-zero length
1019					// If the first character is not a / then this is an opening tag
1020					if ($lvalue[0] != "/")
1021					{
1022						// Collect the tag name
1023						preg_match("/^(\w*?)(\s|$)/", $lvalue, $t);
1024
1025						// If this is a protected element, activate the associated protection flag
1026						if(in_array($t[1], $nobreak))
1027							array_unshift($innbk, $t[1]);
1028					}
1029					else
1030					{
1031						// Otherwise this is a closing tag
1032						// If this is a closing tag for a protected element, unset the flag
1033						if (in_array(substr($lvalue, 1), $nobreak))
1034						{
1035							reset($innbk);
1036							while (list($key, $tag) = each($innbk))
1037							{
1038								if (substr($lvalue, 1) == $tag)
1039								{
1040									unset($innbk[$key]);
1041									break;
1042								}
1043							}
1044							$innbk = array_values($innbk);
1045						}
1046					}
1047				}
1048				else
1049				{
1050					// Eliminate any empty tags altogether
1051					$value = '';
1052				}
1053				// Else if we're outside any tags, and with non-zero length string...
1054			}
1055			elseif ($value)
1056			{
1057				// If unprotected...
1058				if (!count($innbk))
1059				{
1060					// Use the ACK (006) ASCII symbol to replace all HTML entities temporarily
1061					$value = str_replace("\x06", "", $value);
1062					preg_match_all("/&([a-z\d]{2,7}|#\d{2,5});/i", $value, $ents);
1063					$value = preg_replace("/&([a-z\d]{2,7}|#\d{2,5});/i", "\x06", $value);
1064					//			echo "Found block length ".strlen($value).': '.substr($value,20).'<br />';
1065					// Split at spaces - note that this will fail if presented with invalid utf-8 when doing the regex whitespace search
1066					//			$split = preg_split('#(\s)#'.$utf8, $value, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE );
1067					$split = preg_split($whiteSpace, $value, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE );
1068					$value = '';
1069					foreach ($split as $sp)
1070					{
1071						//			echo "Split length ".strlen($sp).': '.substr($sp,20).'<br />';
1072						$loopCount = 0;
1073						while (strlen($sp) > $width)
1074						{
1075							// Enough characters that we may need to do something.
1076							$pulled = '';
1077							if ($utf8)
1078							{
1079								// Pull out a piece of the maximum permissible length
1080								if (preg_match('#^((?:[\x00-\x7F]|[\xC0-\xFF][\x80-\xBF]+){0,'.$width.'})(.{0,1}).*#s',$sp,$matches) == 0)
1081								{
1082									// Make any problems obvious for now
1083									$value .= '[!<b>invalid utf-8: '.$sp.'<b>!]';
1084									$sp = '';
1085								}
1086								elseif (empty($matches[2]))
1087								{
1088									// utf-8 length is less than specified - treat as a special case
1089									$value .= $sp;
1090									$sp = '';
1091								}
1092								else
1093								{
1094									// Need to find somewhere to break the string
1095									for($i = strlen($matches[1]) - 1; $i >= 0; $i--)
1096									{
1097										if(strpos($lbrks, $matches[1][$i]) !== FALSE)
1098											break;
1099									}
1100									if($i < 0)
1101									{
1102										// No 'special' break character found - break at the word boundary
1103										$pulled = $matches[1];
1104									}
1105									else
1106									{
1107										$pulled = substr($sp, 0, $i + 1);
1108									}
1109								}
1110								$loopCount++;
1111								if ($loopCount > 20)
1112								{
1113									// Make any problems obvious for now
1114									$value .= '[!<b>loop count exceeded: '.$sp.'</b>!]';
1115									$sp = '';
1116								}
1117							}
1118							else
1119							{
1120								for ($i = min($width, strlen($sp)); $i > 0; $i--)
1121								{
1122									// No speed advantage to defining match character
1123									if (strpos($lbrks, $sp[$i-1]) !== FALSE)
1124										break;
1125								}
1126								if ($i == 0)
1127								{
1128									// No 'special' break boundary character found - break at the word boundary
1129									$pulled = substr($sp, 0, $width);
1130								}
1131								else
1132								{
1133									$pulled = substr($sp, 0, $i);
1134								}
1135							}
1136							if ($pulled)
1137							{
1138								$value .= $pulled.$break;
1139								// Shorten $sp by whatever we've processed (will work even for utf-8)
1140								$sp = substr($sp, strlen($pulled));
1141							}
1142						}
1143						// Add in any residue
1144						$value .= $sp;
1145					}
1146					// Put captured HTML entities back into the string
1147					foreach ($ents[0] as $ent)
1148						$value = preg_replace("/\x06/", $ent, $value, 1);
1149				}
1150			}
1151			// Send the modified segment down the drain
1152			$drain .= $value;
1153		}
1154		// Return contents of the drain
1155		return $drain;
1156	}
1157
1158	/**
1159	 * CakePHP(tm) :  Rapid Development Framework (http://www.cakephp.org)
1160	 * Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org)
1161	 *
1162	 * Truncate a HTML string
1163	 *
1164	 * Cuts a string to the length of $length and adds the value of $ending if the text is longer than length.
1165	 *
1166	 * @param string  $text String to truncate.
1167	 * @param integer $length Length of returned string, including ellipsis.
1168	 * @param string $ending It will be used as Ending and appended to the trimmed string.
1169	 * @param boolean $exact If false, $text will not be cut mid-word
1170	 * @return string Trimmed string.
1171	 */
1172	function html_truncate($text, $length = 100, $ending = '...', $exact = true)
1173	{
1174		if($this->ustrlen(preg_replace('/<.*?>/', '', $text)) <= $length)
1175		{
1176			return $text;
1177		}
1178		$totalLength = 0;
1179		$openTags = array();
1180		$truncate = '';
1181		preg_match_all('/(<\/?([\w+]+)[^>]*>)?([^<>]*)/', $text, $tags, PREG_SET_ORDER);
1182
1183		foreach($tags as $tag)
1184		{
1185			if(!$tag[2] || !preg_match('/img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param/si', $tag[2]))
1186			{
1187				if(preg_match('/<[\w]+[^>]*>/s', $tag[0]))
1188				{
1189					array_unshift($openTags, $tag[2]);
1190				}
1191				else if(preg_match('/<\/([\w]+)[^>]*>/s', $tag[0], $closeTag))
1192				{
1193					$pos = array_search($closeTag[1], $openTags);
1194					if($pos !== false)
1195					{
1196						array_splice($openTags, $pos, 1);
1197					}
1198				}
1199			}
1200			$truncate .= $tag[1];
1201			$contentLength = $this->ustrlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', ' ', $tag[3]));
1202
1203			if($contentLength + $totalLength > $length)
1204			{
1205				$left = $length - $totalLength;
1206				$entitiesLength = 0;
1207				if(preg_match_all('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', $tag[3], $entities, PREG_OFFSET_CAPTURE))
1208				{
1209					foreach($entities[0] as $entity)
1210					{
1211						if($entity[1] + 1 - $entitiesLength <= $left)
1212						{
1213							$left--;
1214							$entitiesLength += $this->ustrlen($entity[0]);
1215						}
1216						else
1217						{
1218							break;
1219						}
1220					}
1221				}
1222
1223				$truncate .= $this->usubstr($tag[3], 0, $left + $entitiesLength);
1224				break;
1225			}
1226			else
1227			{
1228				$truncate .= $tag[3];
1229				$totalLength += $contentLength;
1230			}
1231			if($totalLength >= $length)
1232			{
1233				break;
1234			}
1235		}
1236		if(!$exact)
1237		{
1238			$spacepos = $this->ustrrpos($truncate, ' ');
1239			if(isset($spacepos))
1240			{
1241				$bits = $this->usubstr($truncate, $spacepos);
1242				preg_match_all('/<\/([a-z]+)>/i', $bits, $droppedTags, PREG_SET_ORDER);
1243				if(!empty($droppedTags))
1244				{
1245					foreach($droppedTags as $closingTag)
1246					{
1247						if(!in_array($closingTag[1], $openTags))
1248						{
1249							array_unshift($openTags, $closingTag[1]);
1250						}
1251					}
1252				}
1253				$truncate = $this->usubstr($truncate, 0, $spacepos);
1254			}
1255		}
1256		$truncate .= $ending;
1257		foreach($openTags as $tag)
1258		{
1259			$truncate .= '</' . $tag . '>';
1260		}
1261		return $truncate;
1262	}
1263
1264	/**
1265	 * Truncate a HTML string to a maximum length $len ­ append the string $more if it was truncated
1266	 *
1267	 * @param string $text String to process
1268	 * @param integer $len [optional] Length of characters to be truncated - default 200
1269	 * @param string $more [optional] String which will be added if truncation - default ' ... '
1270	 * @return string
1271	 */
1272	public function html_truncate_old ($text, $len = 200, $more = ' ... ')
1273	{
1274		$pos = 0;
1275		$curlen = 0;
1276		$tmp_pos = 0;
1277		$intag = FALSE;
1278		while($curlen < $len && $curlen < strlen($text))
1279		{
1280			switch($text [$pos] )
1281			{
1282				case "<":
1283					if($text [$pos + 1] == "/")
1284					{
1285						$closing_tag = TRUE;
1286					}
1287					$intag = TRUE;
1288					$tmp_pos = $pos - 1;
1289					$pos++;
1290				break;
1291
1292
1293				case ">":
1294					if($text [$pos - 1] == "/")
1295					{
1296						$closing_tag = TRUE;
1297					}
1298					if($closing_tag == TRUE)
1299					{
1300						$tmp_pos = 0;
1301						$closing_tag = FALSE;
1302					}
1303					$intag = FALSE;
1304					$pos++;
1305				break;
1306
1307
1308				case "&":
1309					if($text [$pos + 1] == "#")
1310					{
1311						$end = strpos(substr($text, $pos, 7), ";");
1312						if($end !== FALSE)
1313						{
1314							$pos += ($end + 1);
1315							if(!$intag)
1316							{
1317								$curlen++;
1318							}
1319						break;
1320						}
1321					}
1322					else
1323					{
1324						$pos++;
1325						if(!$intag)
1326						{
1327							$curlen++;
1328						}
1329					break;
1330					}
1331				default:
1332					$pos++;
1333					if(!$intag)
1334					{
1335						$curlen++;
1336					}
1337				break;
1338			}
1339		}
1340		$ret = ($tmp_pos > 0 ? substr($text, 0, $tmp_pos+1) : substr($text, 0, $pos));
1341		if($pos < strlen($text))
1342		{
1343			$ret = $ret.$more;
1344		}
1345		return $ret;
1346	}
1347
1348
1349	/**
1350	 * Truncate a string of text to a maximum length $len ­ append the string $more if it was truncated
1351	 * Uses current CHARSET ­ for utf-8, returns $len characters rather than $len bytes
1352	 *
1353	 * @param string $text ­ string to process
1354	 * @param integer $len ­ length of characters to be truncated
1355	 * @param string $more ­ string which will be added if truncation
1356	 * @return string
1357	 */
1358	public function text_truncate($text, $len = 200, $more = ' ... ')
1359	{
1360		// Always valid
1361
1362		if($this->ustrlen($text) <= $len)
1363		{
1364			return $text;
1365		}
1366
1367		$text = html_entity_decode($text,ENT_QUOTES,'utf-8');
1368
1369		if(function_exists('mb_strimwidth'))
1370		{
1371			return mb_strimwidth($text, 0, $len, $more);
1372		}
1373
1374		$ret = $this->usubstr($text, 0, $len);
1375
1376		// search for possible broken html entities
1377		// - if an & is in the last 8 chars, removing it and whatever follows shouldn't hurt
1378		// it should work for any characters encoding
1379
1380		$leftAmp = $this->ustrrpos($this->usubstr($ret, -8), '&');
1381		if($leftAmp)
1382		{
1383			$ret = $this->usubstr($ret, 0, $this->ustrlen($ret) - 8 + $leftAmp);
1384		}
1385
1386		return $ret.$more;
1387
1388	}
1389
1390
1391	function textclean ($text, $wrap = 100)
1392	{
1393		$text = str_replace("\n\n\n", "\n\n", $text);
1394		$text = $this->htmlwrap($text, $wrap);
1395		$text = str_replace(array('<br /> ', ' <br />', ' <br /> '), '<br />', $text);
1396		/* we can remove any linebreaks added by htmlwrap function as any \n's will be converted later anyway */
1397		return $text;
1398	}
1399
1400
1401	// Test for text highlighting, and determine the text highlighting transformation
1402	// Returns TRUE if highlighting is active for this page display
1403	function checkHighlighting()
1404	{
1405		global $pref;
1406
1407		if (!defined('e_SELF'))
1408		{
1409			// Still in startup, so can't calculate highlighting
1410			return FALSE;
1411		}
1412
1413		if(!isset($this->e_highlighting))
1414		{
1415			$this->e_highlighting = FALSE;
1416			$shr = (isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : "");
1417			if($pref['search_highlight'] && (strpos(e_SELF, 'search.php') === FALSE) && ((strpos($shr, 'q=') !== FALSE) || (strpos($shr, 'p=') !== FALSE)))
1418			{
1419				$this->e_highlighting = TRUE;
1420				if(!isset($this->e_query))
1421				{
1422					$query = preg_match('#(q|p)=(.*?)(&|$)#', $shr, $matches);
1423					$this->e_query = str_replace(array('+', '*', '"', ' '), array('', '.*?', '', '\b|\b'), trim(urldecode($matches[2])));
1424				}
1425			}
1426		}
1427		return $this->e_highlighting;
1428	}
1429
1430
1431	/**
1432	 * Replace text represenation of website urls and email addresses with clickable equivalents.
1433	 * @param string $text
1434	 * @param string $type email|url
1435	 * @param array $opts options. (see below)
1436	 * @param string $opts['sub'] substitute text within links
1437	 * @param bool $opts['ext'] load link in new window (not for email)
1438	 * @return string
1439	 */
1440	public function makeClickable($text='', $type='email', $opts=array())
1441	{
1442
1443		if(empty($text))
1444		{
1445			return '';
1446		}
1447
1448		$textReplace = (!empty($opts['sub'])) ? $opts['sub'] : '';
1449
1450		if(substr($textReplace,-6) === '.glyph')
1451		{
1452			$textReplace = $this->toGlyph($textReplace,'');
1453		}
1454
1455		switch($type)
1456		{
1457			default:
1458			case "email":
1459
1460				preg_match_all("#(?:[\n\r ]|^)?([a-z0-9\-_.]+?)@([\w\-]+\.([\w\-\.]+\.)*[\w]+)#i", $text, $match);
1461
1462				if(!empty($match[0]))
1463				{
1464
1465					$srch = array();
1466					$repl = array();
1467
1468					foreach($match[0] as $eml)
1469					{
1470						$email = trim($eml);
1471						$srch[] = $email;
1472						$repl[] = $this->emailObfuscate($email,$textReplace);
1473					}
1474					$text = str_replace($srch,$repl,$text);
1475				}
1476				break;
1477
1478			case "url":
1479
1480				$linktext = (!empty($textReplace)) ? $textReplace : '$3';
1481				$external = (!empty($opts['ext'])) ? 'target="_blank"' : '';
1482
1483				$text= preg_replace("/(^|[\n \(])([\w]*?)([\w]*?:\/\/[\w]+[^ \,\"\n\r\t<]*)/is", "$1$2<a class=\"e-url\" href=\"$3\" ".$external.">".$linktext."</a>", $text);
1484				$text= preg_replace("/(^|[\n \(])([\w]*?)((www)\.[^ \,\"\t\n\r\)<]*)/is", "$1$2<a class=\"e-url\" href=\"http://$3\" ".$external.">".$linktext."</a>", $text);
1485				$text= preg_replace("/(^|[\n ])([\w]*?)((ftp)\.[^ \,\"\t\n\r<]*)/is", "$1$2<a class=\"e-url\" href=\"$4://$3\" ".$external.">".$linktext."</a>", $text);
1486
1487				break;
1488
1489		}
1490
1491		return $text;
1492
1493
1494
1495	}
1496
1497
1498
1499	function parseBBCodes($text, $postID)
1500	{
1501		if (!is_object($this->e_bb))
1502		{
1503			require_once(e_HANDLER.'bbcode_handler.php');
1504			$this->e_bb = new e_bbcode;
1505		}
1506
1507
1508		$text = $this->e_bb->parseBBCodes($text, $postID);
1509
1510		return $text;
1511	}
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525	/**
1526	 * Converts the text (presumably retrieved from the database) for HTML output.
1527	 *
1528	 * @param string $text
1529	 * @param boolean $parseBB [optional]
1530	 * @param string $modifiers [optional] TITLE|SUMMARY|DESCRIPTION|BODY|RAW|LINKTEXT etc.
1531	 *		Comma-separated list, no spaces allowed
1532	 *		first modifier must be a CONTEXT modifier, in UPPER CASE.
1533	 *		subsequent modifiers are lower case - see $this->e_Modifiers for possible values
1534	 * @param mixed $postID [optional]
1535	 * @param boolean $wrap [optional]
1536	 * @return string
1537	 * @todo complete the documentation of this essential method
1538	 */
1539	public function toHTML($text, $parseBB = FALSE, $modifiers = '', $postID = '', $wrap = FALSE)
1540	{
1541		if($text == '')
1542		{
1543			return $text;
1544		}
1545
1546		$pref = e107::getPref();
1547
1548		global $fromadmin;
1549
1550		// Set default modifiers to start
1551		$opts = $this->e_optDefault;
1552
1553
1554		// Now process any modifiers that are specified
1555		if ($modifiers)
1556		{
1557			$aMods = explode(',', $modifiers);
1558
1559			// If there's a supermodifier, it must be first, and in uppercase
1560			$psm = trim($aMods[0]);
1561			if (isset($this->e_SuperMods[$psm]))
1562			{
1563				// Supermodifier found - override default values where necessary
1564				$opts = array_merge($opts,$this->e_SuperMods[$psm]);
1565				$opts['context'] = $psm;
1566				unset($aMods[0]);
1567			}
1568
1569			// Now find any regular modifiers; use them to modify the context
1570			// (there should only be one or two out of the list of possibles)
1571			foreach ($aMods as $mod)
1572			{
1573				// Slight concession to varying coding styles - stripping spaces is a waste of CPU cycles!
1574				$mod = trim($mod);
1575				if (isset($this->e_Modifiers[$mod]))
1576				{
1577					// This is probably quicker than array_merge
1578					// - especially as usually only one or two loops
1579					foreach ($this->e_Modifiers[$mod] as $k => $v)
1580					{
1581						// Update our context-specific options
1582						$opts[$k] = $v;
1583					}
1584				}
1585			}
1586		}
1587
1588		// Turn off a few things if not enabled in options
1589		if(empty($pref['smiley_activate']))
1590		{
1591			$opts['emotes'] = false;
1592		}
1593
1594		if(empty($pref['make_clickable']))
1595		{
1596			$opts['link_click'] = false;
1597		}
1598
1599		if(empty($pref['link_replace']))
1600		{
1601			$opts['link_replace'] = false;
1602		}
1603
1604		if($this->isHtml($text)) //BC FIx for when HTML is saved without [html][/html]
1605		{
1606			$opts['nobreak'] = true;
1607			$text = trim($text);
1608
1609			if(strpos($text,'[center]') === 0) // quick bc fix TODO Find a better solution. [center][/center] containing HTML.
1610            {
1611		    	$text = str_replace("[center]","<div style='text-align:center'>",$text);
1612		        $text = str_replace("[/center]","</div>",$text);
1613            }
1614		}
1615
1616		$fromadmin = $opts['fromadmin'];
1617
1618		// Convert defines(constants) within text. eg. Lan_XXXX - must be the entire text string (i.e. not embedded)
1619		// The check for '::' is a workaround for a bug in the Zend Optimiser 3.3.0 and PHP 5.2.4 combination
1620		// - causes crashes if '::' in site name
1621
1622		if($opts['defs'] && (strlen($text) < 35) && ((strpos($text, '::') === FALSE) && defined(trim($text))))
1623		{
1624			$text = constant(trim($text)); // don't return yet, words could be hooked with linkwords etc.
1625		}
1626
1627		if ($opts['no_tags'])
1628		{
1629			$text = strip_tags($text);
1630		}
1631
1632		if (MAGIC_QUOTES_GPC === true) // precaution for badly saved data.
1633		{
1634			$text = stripslashes($text);
1635		}
1636
1637
1638		// Make sure we have a valid count for word wrapping
1639		if (!$wrap && !empty($pref['main_wordwrap']))
1640		{
1641			$wrap = $pref['main_wordwrap'];
1642		}
1643//		$text = " ".$text;
1644
1645
1646		// Now get on with the parsing
1647		$ret_parser = '';
1648		$last_bbcode = '';
1649		// So we can change them on each loop
1650		$saveOpts = $opts;
1651
1652
1653
1654		if ($parseBB == false)
1655		{
1656			$content = array($text);
1657		}
1658		else
1659		{
1660			// Split each text block into bits which are either within one of the 'key' bbcodes, or outside them
1661			// (Because we have to match end words, the 'extra' capturing subpattern gets added to output array. We strip it later)
1662			$content = preg_split('#(\[(table|html|php|code|scode|hide).*?\[/(?:\\2)\])#mis', $text, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE );
1663		}
1664
1665
1666		// Use $full_text variable so its available to special bbcodes if required
1667		foreach ($content as $full_text)
1668		{
1669			$proc_funcs = TRUE;
1670			$convertNL = TRUE;
1671
1672			// We may have 'captured' a bbcode word - strip it if so
1673			if ($last_bbcode == $full_text)
1674			{
1675				$last_bbcode = '';
1676				$proc_funcs = FALSE;
1677				$full_text = '';
1678			}
1679			else
1680			{
1681				// Set the options for this pass
1682				$opts = $saveOpts;
1683
1684				// Have to have a good test in case a 'non-key' bbcode starts the block
1685				// - so pull out the bbcode parameters while we're there
1686				if (($parseBB !== FALSE) && preg_match('#(^\[(table|html|php|code|scode|hide)(.*?)\])(.*?)(\[/\\2\]$)#is', $full_text, $matches ))
1687				{
1688					// It's one of the 'key' bbcodes
1689					// Usually don't want 'normal' processing if its a 'special' bbcode
1690					$proc_funcs = FALSE;
1691					// $matches[0] - complete block from opening bracket of opening tag to closing bracket of closing tag
1692					// $matches[1] - complete opening tag (inclusive of brackets)
1693					// $matches[2] - bbcode word
1694					// $matches[3] - parameter, including '='
1695					// $matches[4] - bit between the tags (i.e. text to process)
1696					// $matches[5] - closing tag
1697					// In case we decide to load a file
1698
1699					$bbPath 		= e_CORE.'bbcodes/';
1700					$bbFile 		= strtolower(str_replace('_', '', $matches[2]));
1701					$bbcode 		= '';
1702					$className 		= '';
1703					$full_text 		= '';
1704					$code_text 		= $matches[4];
1705					$parm 			= $matches[3] ? substr($matches[3],1) : '';
1706					$last_bbcode 	= $matches[2];
1707
1708					switch ($matches[2])
1709					{
1710						case 'php' :
1711							// Probably run the output through the normal processing functions - but put here so the PHP code can disable if desired
1712							$proc_funcs = TRUE;
1713
1714							// This is just the contents of the php.bb file pulled in - its short, so will be quicker
1715			//				$search = array("&quot;", "&#039;", "&#036;", '<br />', E_NL, "-&gt;", "&lt;br /&gt;");
1716			//				$replace = array('"', "'", "$", "\n", "\n", "->", "<br />");
1717							// Shouldn't have any parameter on this bbcode
1718							// Not sure whether checks are necessary now we've reorganised
1719			//				if (!$matches[3]) $bbcode = str_replace($search, $replace, $matches[4]);
1720							// Because we're bypassing most of the initial parser processing, we should be able to just reverse the effects of toDB() and execute the code
1721							// [SecretR] - avoid php code injections, missing php.bb will completely disable user posted php blocks
1722							$bbcode = file_get_contents($bbPath.$bbFile.'.bb');
1723							if (!$matches[3])
1724							{
1725								$code_text = html_entity_decode($matches[4], ENT_QUOTES, 'UTF-8');
1726							}
1727							break;
1728
1729						case 'html' : // This overrides and deprecates html.bb
1730							$proc_funcs = TRUE;
1731
1732
1733						//	$code_text = str_replace("\r\n", " ", $code_text);
1734						//	$code_text = html_entity_decode($code_text, ENT_QUOTES, CHARSET);
1735						//	$code_text = str_replace('&','&amp;',$code_text); // validation safe.
1736							$html_start = "<!-- bbcode-html-start -->"; // markers for html-to-bbcode replacement.
1737							$html_end	= "<!-- bbcode-html-end -->";
1738							$full_text = str_replace(array("[html]","[/html]"), "",$code_text); // quick fix.. security issue?
1739
1740							$full_text = $this->parseBBCodes($full_text, $postID); // parse any embedded bbcodes eg. [img]
1741							$full_text = $this->replaceConstants($full_text,'abs'); // parse any other paths using {e_....
1742							$full_text = $html_start.$full_text.$html_end;
1743							$full_text = $this->parseBBTags($full_text); // strip <bbcode> tags.
1744							$opts['nobreak'] = true;
1745							$parseBB = false; // prevent further bbcode processing.
1746
1747
1748							break;
1749
1750						case 'table' : // strip <br /> from inside of <table>
1751
1752							$convertNL = FALSE;
1753						//	break;
1754
1755						case 'hide' :
1756							$proc_funcs = TRUE;
1757
1758						default :		// Most bbcodes will just execute their normal file
1759							// @todo should we cache these bbcodes? require_once should make class-related codes quite efficient
1760							if (file_exists($bbPath.'bb_'.$bbFile.'.php'))
1761							{	// Its a bbcode class file
1762								require_once($bbPath.'bb_'.$bbFile.'.php');
1763								$className = 'bb_'.$last_bbcode;
1764
1765								$this->bbList[$last_bbcode] = new $className();
1766							}
1767							elseif(file_exists($bbPath.$bbFile.'.bb'))
1768							{
1769								$bbcode = file_get_contents($bbPath.$bbFile.'.bb');
1770							}
1771					}   // end - switch ($matches[2])
1772
1773					if ($className)
1774					{
1775						/** @var e_bb_base $tempCode */
1776						$tempCode = new $className();
1777
1778						$full_text = $tempCode->bbPreDisplay($matches[4], $parm);
1779					}
1780					elseif ($bbcode)
1781					{	// Execute the file
1782						$full_text = eval($bbcode);			// Require output of bbcode to be returned
1783						// added to remove possibility of nested bbcode exploits ...
1784						//   (same as in bbcode_handler - is it right that it just operates on $bbcode_return and not on $bbcode_output? - QUERY XXX-02
1785					}
1786					if(strpos($full_text, '[') !== FALSE)
1787					{
1788						$exp_search = array('eval', 'expression');
1789						$exp_replace = array('ev<b></b>al', 'expres<b></b>sion');
1790						$bbcode_return = str_replace($exp_search, $exp_replace, $full_text);
1791					}
1792				}
1793			}
1794
1795
1796			// Do the 'normal' processing - in principle, as previously - but think about the order.
1797			if ($proc_funcs && !empty($full_text)) // some more speed
1798			{
1799				// Split out and ignore any scripts and style blocks. With just two choices we can match the closing tag in the regex
1800				$subcon = preg_split('#((?:<s)(?:cript[^>]+>.*?</script>|tyle[^>]+>.*?</style>))#mis', $full_text, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE );
1801				foreach ($subcon as $sub_blk)
1802				{
1803
1804					if(strpos($sub_blk,'<script') === 0) // Strip scripts unless permitted
1805					{
1806						if($opts['scripts'])
1807						{
1808							$ret_parser .= html_entity_decode($sub_blk, ENT_QUOTES);
1809						}
1810					}
1811					elseif(strpos($sub_blk,'<style') === 0)
1812					{
1813						// Its a style block - just pass it through unaltered - except, do we need the line break stuff? - QUERY XXX-01
1814						if(defined('DB_INF_SHOW'))
1815						{
1816							echo "Processing stylesheet: {$sub_blk}<br />";
1817						}
1818
1819						$ret_parser .= $sub_blk;
1820					}
1821					else
1822					{
1823						// Do 'normal' processing on a chunk
1824
1825
1826						// Could put tag stripping in here
1827
1828/*
1829						//	Line break compression - filter white space after HTML tags - among other things, ensures HTML tables display properly
1830						// Hopefully now achieved by other means
1831						if ($convertNL && !$opts['nobreak'])
1832						{
1833							$sub_blk = preg_replace("#>\s*[\r]*\n[\r]*#", ">", $sub_blk);
1834						}
1835*/
1836
1837						//	Link substitution
1838						// Convert URL's to clickable links, unless modifiers or prefs override
1839						if ($opts['link_click'])
1840						{
1841							if ($opts['link_replace'] && defset('ADMIN_AREA') !== true)
1842							{
1843
1844								$link_text = $pref['link_text'];
1845								$email_text = ($pref['email_text']) ? $this->replaceConstants($pref['email_text']) : LAN_EMAIL_SUBS;
1846
1847								$sub_blk = $this->makeClickable($sub_blk, 'url', array('sub'=> $link_text,'ext'=>$pref['links_new_window']));
1848								$sub_blk = $this->makeClickable($sub_blk, 'email', array('sub'=> $email_text));
1849							}
1850							else
1851							{
1852
1853								$sub_blk = $this->makeClickable($sub_blk, 'url', array('ext'=>true));
1854								$sub_blk = $this->makeClickable($sub_blk, 'email');
1855
1856							}
1857						}
1858
1859
1860						// Convert emoticons to graphical icons, if enabled
1861						if ($opts['emotes'])
1862						{
1863							if (!is_object($this->e_emote))
1864							{
1865							//	require_once(e_HANDLER.'emote_filter.php');
1866								$this->e_emote = new e_emoteFilter;
1867							}
1868							$sub_blk = $this->e_emote->filterEmotes($sub_blk);
1869						}
1870
1871
1872						// Reduce newlines in all forms to a single newline character (finds '\n', '\r\n', '\n\r')
1873						if (!$opts['nobreak'])
1874						{
1875							if ($convertNL && ($this->preformatted($sub_blk) === false)) // eg. html or markdown
1876							{
1877								// We may need to convert to <br /> later
1878								$sub_blk = preg_replace("#[\r]*\n[\r]*#", E_NL, $sub_blk);
1879							}
1880							else
1881							{
1882								// Not doing any more - its HTML or Markdown so keep it as is.
1883								$sub_blk = preg_replace("#[\r]*\n[\r]*#", "\n", $sub_blk);
1884							}
1885						}
1886
1887
1888						//	Entity conversion
1889						// Restore entity form of quotes and such to single characters, except for text destined for tag attributes or JS.
1890						if($opts['value'])
1891						{
1892							// output used for attribute values.
1893							$sub_blk = str_replace($this->replace, $this->search, $sub_blk);
1894						}
1895						else
1896						{
1897							// output not used for attribute values.
1898							$sub_blk = str_replace($this->search, $this->replace, $sub_blk);
1899						}
1900
1901
1902						//   BBCode processing (other than the four already done, which shouldn't appear at all in the text)
1903						if ($parseBB !== FALSE)
1904						{
1905							if (!is_object($this->e_bb))
1906							{
1907								require_once(e_HANDLER.'bbcode_handler.php');
1908								$this->e_bb = new e_bbcode;
1909							}
1910							if ($parseBB === TRUE)
1911							{
1912								// 'Normal' or 'legacy' processing
1913								if($modifiers == "WYSIWYG")
1914								{
1915									$sub_blk = $this->e_bb->parseBBCodes($sub_blk, $postID, 'wysiwyg');
1916								}
1917								else
1918								{
1919									$sub_blk = $this->e_bb->parseBBCodes($sub_blk, $postID);
1920								}
1921
1922							}
1923							elseif ($parseBB === 'STRIP')
1924							{
1925								// Need to strip all BBCodes
1926								$sub_blk = $this->e_bb->parseBBCodes($sub_blk, $postID, 'default', TRUE);
1927							}
1928							else
1929							{
1930								// Need to strip just some BBCodes
1931								$sub_blk = $this->e_bb->parseBBCodes($sub_blk, $postID, 'default', $parseBB);
1932							}
1933						}
1934
1935
1936						// replace all {e_XXX} constants with their e107 value. modifier determines relative/absolute conversion
1937						// (Moved to after bbcode processing by Cameron)
1938						if ($opts['constants'])
1939						{
1940							$sub_blk = $this->replaceConstants($sub_blk, $opts['constants']);		// Now decodes text values
1941						}
1942
1943
1944						// profanity filter
1945						if (!empty($pref['profanity_filter']))
1946						{
1947							if (!is_object($this->e_pf))
1948							{
1949							//	require_once(e_HANDLER."profanity_filter.php");
1950								$this->e_pf = new e_profanityFilter;
1951							}
1952							$sub_blk = $this->e_pf->filterProfanities($sub_blk);
1953						}
1954
1955
1956						//	Shortcodes
1957						// Optional short-code conversion
1958						if ($opts['parse_sc'])
1959						{
1960							$sub_blk = $this->parseTemplate($sub_blk, TRUE);
1961						}
1962
1963
1964						/**
1965						 * / @deprecated
1966						 */
1967						if ($opts['hook']) //Run any hooked in parsers
1968						{
1969							if ( varset($pref['tohtml_hook']))
1970							{
1971								//Process the older tohtml_hook pref (deprecated)
1972								foreach(explode(",", $pref['tohtml_hook']) as $hook)
1973								{
1974									if (!is_object($this->e_hook[$hook]))
1975									{
1976										if(is_readable(e_PLUGIN.$hook."/".$hook.".php"))
1977										{
1978											require_once(e_PLUGIN.$hook."/".$hook.".php");
1979											$hook_class = "e_".$hook;
1980											$this->e_hook[$hook] = new $hook_class;
1981										}
1982
1983									}
1984
1985									if(is_object($this->e_hook[$hook])) // precaution for old plugins.
1986									{
1987										$sub_blk = $this->e_hook[$hook]->$hook($sub_blk,$opts['context']);
1988									}
1989								}
1990							}
1991
1992							/**
1993						    * / @deprecated
1994						    */
1995							if(isset($pref['e_tohtml_list']) && is_array($pref['e_tohtml_list']))
1996							{
1997								foreach($pref['e_tohtml_list'] as $hook)
1998								{
1999									if (!is_object($this->e_hook[$hook]))
2000									{
2001										if(is_readable(e_PLUGIN.$hook."/e_tohtml.php"))
2002										{
2003											require_once(e_PLUGIN.$hook."/e_tohtml.php");
2004
2005											$hook_class = "e_tohtml_".$hook;
2006
2007											$this->e_hook[$hook] = new $hook_class;
2008										}
2009									}
2010
2011									if(is_object( $this->e_hook[$hook]))
2012									{
2013										/** @var e_tohtml_linkwords $deprecatedHook */
2014										$deprecatedHook = $this->e_hook[$hook];
2015										$sub_blk = $deprecatedHook->to_html($sub_blk, $opts['context']);
2016									}
2017								}
2018							}
2019
2020						/**
2021						* / Preferred 'hook'
2022						*/
2023						if(!empty($pref['e_parse_list']))
2024						{
2025							foreach($pref['e_parse_list'] as $plugin)
2026							{
2027								$hookObj = e107::getAddon($plugin,'e_parse');
2028								if($tmp = e107::callMethod($hookObj, 'toHTML', $sub_blk, $opts['context']))
2029								{
2030									$sub_blk = $tmp;
2031								}
2032
2033							}
2034
2035						}
2036
2037
2038
2039
2040
2041				}
2042
2043
2044						// 	Word wrap
2045						if ($wrap && !$opts['nobreak'])
2046						{
2047							$sub_blk = $this->textclean($sub_blk, $wrap);
2048						}
2049
2050
2051						//	Search highlighting
2052						if ($opts['emotes'])			// Why??
2053						{
2054							if ($this->checkHighlighting())
2055							{
2056								$sub_blk = $this->e_highlight($sub_blk, $this->e_query);
2057							}
2058						}
2059
2060
2061
2062
2063						if($convertNL == true)
2064						{
2065							// Default replaces all \n with <br /> for HTML display
2066							$nl_replace = '<br />';
2067							if ($opts['nobreak'])
2068							{
2069								$nl_replace = '';
2070							}
2071							elseif ($opts['retain_nl'])
2072							{
2073								$nl_replace = "\n";
2074							}
2075
2076							$sub_blk = str_replace(E_NL, $nl_replace, $sub_blk);
2077						}
2078
2079						$ret_parser .= $sub_blk;
2080					}	// End of 'normal' processing for a block of text
2081
2082				}		// End of 'foreach() on each block of non-script text
2083
2084			}		// End of 'normal' parsing (non-script text)
2085			else
2086			{
2087				// Text block that needed no processing at all
2088				$ret_parser .= $full_text;
2089			}
2090		}
2091
2092		// Quick Fix - Remove trailing <br /> on block-level elements (eg. div, pre, table, etc. )
2093		$srch = array();
2094		$repl = array();
2095
2096		foreach($this->blockTags as $val)
2097		{
2098			$srch[] = "</".$val."><br />";
2099			$repl[]	= "</".$val.">";
2100		}
2101
2102		$ret_parser = str_replace($srch, $repl, $ret_parser);
2103
2104		return trim($ret_parser);
2105	}
2106
2107
2108	/**
2109	 * Check if a string begins with a preformatter flag.
2110	 * @param $str
2111	 * @return bool
2112	 */
2113	private function preformatted($str)
2114	{
2115		foreach($this->preformatted as $type)
2116		{
2117			$code = '['.$type.']';
2118			if(strpos($str, $code) === 0)
2119			{
2120				return true;
2121			}
2122
2123		}
2124
2125		return false;
2126	}
2127
2128
2129	/**
2130	 * @param $mixed
2131	 * @return array|false|string
2132	 */
2133	public function toUTF8($mixed)
2134	{
2135
2136		if(is_array($mixed))
2137		{
2138			foreach($mixed as $k => $v)
2139			{
2140				unset($mixed[$k]);
2141				$mixed[$this->toUTF8($k)] = $this->toUTF8($v);
2142			}
2143		}
2144		elseif(is_object($mixed))
2145		{
2146			$objVars = get_object_vars($mixed);
2147			foreach($objVars as $key => $value)
2148			{
2149				$mixed->$key = $this->toUTF8($value);
2150			}
2151		}
2152		elseif(is_string($mixed))
2153		{
2154			return iconv('UTF-8', 'UTF-8//IGNORE', utf8_encode($mixed));
2155		}
2156
2157		return $mixed;
2158	}
2159
2160
2161	function toASCII($text)
2162	{
2163
2164		$char_map = array(
2165			// Latin
2166			'À' => 'A', 'Á' => 'A', 'Â' => 'A', 'Ã' => 'A', 'Ä' => 'A', 'Å' => 'A', 'Æ' => 'AE', 'Ç' => 'C',
2167			'È' => 'E', 'É' => 'E', 'Ê' => 'E', 'Ë' => 'E', 'Ì' => 'I', 'Í' => 'I', 'Î' => 'I', 'Ï' => 'I',
2168			'Ð' => 'D', 'Ñ' => 'N', 'Ò' => 'O', 'Ó' => 'O', 'Ô' => 'O', 'Õ' => 'O', 'Ö' => 'O', 'Ő' => 'O',
2169			'Ø' => 'O', 'Ù' => 'U', 'Ú' => 'U', 'Û' => 'U', 'Ü' => 'U', 'Ű' => 'U', 'Ý' => 'Y', 'Þ' => 'TH',
2170			'ß' => 'ss',
2171			'à' => 'a', 'á' => 'a', 'â' => 'a', 'ã' => 'a', 'ä' => 'a', 'å' => 'a', 'æ' => 'ae', 'ç' => 'c',
2172			'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e', 'ì' => 'i', 'í' => 'i', 'î' => 'i', 'ï' => 'i',
2173			'ð' => 'd', 'ñ' => 'n', 'ò' => 'o', 'ó' => 'o', 'ô' => 'o', 'õ' => 'o', 'ö' => 'o', 'ő' => 'o',
2174			'ø' => 'o', 'ù' => 'u', 'ú' => 'u', 'û' => 'u', 'ü' => 'u', 'ű' => 'u', 'ý' => 'y', 'þ' => 'th',
2175			'ÿ' => 'y',
2176			// Latin symbols
2177			'©' => '(c)',
2178			// Greek
2179			'Α' => 'A', 'Β' => 'B', 'Γ' => 'G', 'Δ' => 'D', 'Ε' => 'E', 'Ζ' => 'Z', 'Η' => 'H', 'Θ' => '8',
2180			'Ι' => 'I', 'Κ' => 'K', 'Λ' => 'L', 'Μ' => 'M', 'Ν' => 'N', 'Ξ' => '3', 'Ο' => 'O', 'Π' => 'P',
2181			'Ρ' => 'R', 'Σ' => 'S', 'Τ' => 'T', 'Υ' => 'Y', 'Φ' => 'F', 'Χ' => 'X', 'Ψ' => 'PS', 'Ω' => 'W',
2182			'Ά' => 'A', 'Έ' => 'E', 'Ί' => 'I', 'Ό' => 'O', 'Ύ' => 'Y', 'Ή' => 'H', 'Ώ' => 'W', 'Ϊ' => 'I',
2183			'Ϋ' => 'Y',
2184			'α' => 'a', 'β' => 'b', 'γ' => 'g', 'δ' => 'd', 'ε' => 'e', 'ζ' => 'z', 'η' => 'h', 'θ' => '8',
2185			'ι' => 'i', 'κ' => 'k', 'λ' => 'l', 'μ' => 'm', 'ν' => 'n', 'ξ' => '3', 'ο' => 'o', 'π' => 'p',
2186			'ρ' => 'r', 'σ' => 's', 'τ' => 't', 'υ' => 'y', 'φ' => 'f', 'χ' => 'x', 'ψ' => 'ps', 'ω' => 'w',
2187			'ά' => 'a', 'έ' => 'e', 'ί' => 'i', 'ό' => 'o', 'ύ' => 'y', 'ή' => 'h', 'ώ' => 'w', 'ς' => 's',
2188			'ϊ' => 'i', 'ΰ' => 'y', 'ϋ' => 'y', 'ΐ' => 'i',
2189			// Turkish
2190			'Ş' => 'S', 'İ' => 'I', 'Ç' => 'C', 'Ü' => 'U', 'Ö' => 'O', 'Ğ' => 'G',
2191			'ş' => 's', 'ı' => 'i', 'ç' => 'c', 'ü' => 'u', 'ö' => 'o', 'ğ' => 'g',
2192			// Russian
2193			'А' => 'A', 'Б' => 'B', 'В' => 'V', 'Г' => 'G', 'Д' => 'D', 'Е' => 'E', 'Ё' => 'Yo', 'Ж' => 'Zh',
2194			'З' => 'Z', 'И' => 'I', 'Й' => 'J', 'К' => 'K', 'Л' => 'L', 'М' => 'M', 'Н' => 'N', 'О' => 'O',
2195			'П' => 'P', 'Р' => 'R', 'С' => 'S', 'Т' => 'T', 'У' => 'U', 'Ф' => 'F', 'Х' => 'H', 'Ц' => 'C',
2196			'Ч' => 'Ch', 'Ш' => 'Sh', 'Щ' => 'Sh', 'Ъ' => '', 'Ы' => 'Y', 'Ь' => '', 'Э' => 'E', 'Ю' => 'Yu',
2197			'Я' => 'Ya',
2198			'а' => 'a', 'б' => 'b', 'в' => 'v', 'г' => 'g', 'д' => 'd', 'е' => 'e', 'ё' => 'yo', 'ж' => 'zh',
2199			'з' => 'z', 'и' => 'i', 'й' => 'j', 'к' => 'k', 'л' => 'l', 'м' => 'm', 'н' => 'n', 'о' => 'o',
2200			'п' => 'p', 'р' => 'r', 'с' => 's', 'т' => 't', 'у' => 'u', 'ф' => 'f', 'х' => 'h', 'ц' => 'c',
2201			'ч' => 'ch', 'ш' => 'sh', 'щ' => 'sh', 'ъ' => '', 'ы' => 'y', 'ь' => '', 'э' => 'e', 'ю' => 'yu',
2202			'я' => 'ya',
2203			// Ukrainian
2204			'Є' => 'Ye', 'І' => 'I', 'Ї' => 'Yi', 'Ґ' => 'G',
2205			'є' => 'ye', 'і' => 'i', 'ї' => 'yi', 'ґ' => 'g',
2206			// Czech
2207			'Č' => 'C', 'Ď' => 'D', 'Ě' => 'E', 'Ň' => 'N', 'Ř' => 'R', 'Š' => 'S', 'Ť' => 'T', 'Ů' => 'U',
2208			'Ž' => 'Z',
2209			'č' => 'c', 'ď' => 'd', 'ě' => 'e', 'ň' => 'n', 'ř' => 'r', 'š' => 's', 'ť' => 't', 'ů' => 'u',
2210			'ž' => 'z',
2211			// Polish
2212			'Ą' => 'A', 'Ć' => 'C', 'Ę' => 'e', 'Ł' => 'L', 'Ń' => 'N', 'Ó' => 'o', 'Ś' => 'S', 'Ź' => 'Z',
2213			'Ż' => 'Z',
2214			'ą' => 'a', 'ć' => 'c', 'ę' => 'e', 'ł' => 'l', 'ń' => 'n', 'ó' => 'o', 'ś' => 's', 'ź' => 'z',
2215			'ż' => 'z',
2216			// Latvian
2217			'Ā' => 'A', 'Č' => 'C', 'Ē' => 'E', 'Ģ' => 'G', 'Ī' => 'i', 'Ķ' => 'k', 'Ļ' => 'L', 'Ņ' => 'N',
2218			'Š' => 'S', 'Ū' => 'u', 'Ž' => 'Z',
2219			'ā' => 'a', 'č' => 'c', 'ē' => 'e', 'ģ' => 'g', 'ī' => 'i', 'ķ' => 'k', 'ļ' => 'l', 'ņ' => 'n',
2220			'š' => 's', 'ū' => 'u', 'ž' => 'z',
2221
2222			'ľ' => 'l', 'ŕ'=>'r',
2223		);
2224
2225		return str_replace(array_keys($char_map), $char_map, $text);
2226
2227	}
2228
2229
2230
2231	/**
2232	 * Use it on html attributes to avoid breaking markup .
2233	 * @example echo "<a href='#' title='".$tp->toAttribute($text)."'>Hello</a>";
2234	 */
2235	function toAttribute($text)
2236	{
2237		// URLs posted without HTML access may have an &amp; in them.
2238
2239		// Xhtml compliance.
2240		$text = htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
2241
2242		if(!preg_match('/&#|\'|"|<|>/s', $text))
2243		{
2244			$text = $this->replaceConstants($text);
2245			return $text;
2246		}
2247		else
2248		{
2249			return $text;
2250		}
2251	}
2252
2253
2254	/**
2255	 * Convert text blocks which are to be embedded within JS
2256	 *
2257	 * @param string|array $stringarray
2258	 * @return string
2259	 */
2260	public function toJS($stringarray)
2261	{
2262		$search = array("\r\n", "\r", "<br />", "'");
2263		$replace = array("\\n", "", "\\n", "\'");
2264		$stringarray = str_replace($search, $replace, $stringarray);
2265		$stringarray = strip_tags($stringarray);
2266
2267		$trans_tbl = get_html_translation_table(HTML_ENTITIES);
2268		$trans_tbl = array_flip($trans_tbl);
2269
2270		return strtr($stringarray, $trans_tbl);
2271	}
2272
2273
2274	/**
2275	 * Converts a PHP variable into its JavaScript equivalent.
2276	 * We use HTML-safe strings, with several characters escaped.
2277	 *
2278	 * @param mixed $var
2279	 *  PHP variable.
2280	 * @param bool $force_object
2281	 *  True: Outputs an object rather than an array when a non-associative
2282	 *  array is used. Especially useful when the recipient of the output
2283	 *  is expecting an object and the array is empty.
2284	 *
2285	 * @return string
2286	 */
2287	public function toJSON($var, $force_object = false)
2288	{
2289		// The PHP version cannot change within a request.
2290		static $php530;
2291
2292		if(!isset($php530))
2293		{
2294			$php530 = version_compare(PHP_VERSION, '5.3.0', '>=');
2295		}
2296
2297		if($php530)
2298		{
2299			if($force_object === true)
2300			{
2301				// Encode <, >, ', &, and " using the json_encode() options parameter.
2302				return json_encode($var, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT | JSON_FORCE_OBJECT);
2303			}
2304
2305			// Encode <, >, ', &, and " using the json_encode() options parameter.
2306			return json_encode($var, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT);
2307		}
2308
2309		return $this->toJSONhelper($var);
2310	}
2311
2312
2313	/**
2314	 * Encodes a PHP variable to HTML-safe JSON for PHP versions below 5.3.0.
2315	 *
2316	 * @param mixed $var
2317	 * @return string
2318	 */
2319	public function toJSONhelper($var)
2320	{
2321		switch(gettype($var))
2322		{
2323			case 'boolean':
2324				return $var ? 'true' : 'false'; // Lowercase necessary!
2325
2326			case 'integer':
2327			case 'double':
2328				return $var;
2329
2330			case 'resource':
2331			case 'string':
2332				// Always use Unicode escape sequences (\u0022) over JSON escape
2333				// sequences (\") to prevent browsers interpreting these as
2334				// special characters.
2335				$replace_pairs = array(
2336					// ", \ and U+0000 - U+001F must be escaped according to RFC 4627.
2337					'\\'           => '\u005C',
2338					'"'            => '\u0022',
2339					"\x00"         => '\u0000',
2340					"\x01"         => '\u0001',
2341					"\x02"         => '\u0002',
2342					"\x03"         => '\u0003',
2343					"\x04"         => '\u0004',
2344					"\x05"         => '\u0005',
2345					"\x06"         => '\u0006',
2346					"\x07"         => '\u0007',
2347					"\x08"         => '\u0008',
2348					"\x09"         => '\u0009',
2349					"\x0a"         => '\u000A',
2350					"\x0b"         => '\u000B',
2351					"\x0c"         => '\u000C',
2352					"\x0d"         => '\u000D',
2353					"\x0e"         => '\u000E',
2354					"\x0f"         => '\u000F',
2355					"\x10"         => '\u0010',
2356					"\x11"         => '\u0011',
2357					"\x12"         => '\u0012',
2358					"\x13"         => '\u0013',
2359					"\x14"         => '\u0014',
2360					"\x15"         => '\u0015',
2361					"\x16"         => '\u0016',
2362					"\x17"         => '\u0017',
2363					"\x18"         => '\u0018',
2364					"\x19"         => '\u0019',
2365					"\x1a"         => '\u001A',
2366					"\x1b"         => '\u001B',
2367					"\x1c"         => '\u001C',
2368					"\x1d"         => '\u001D',
2369					"\x1e"         => '\u001E',
2370					"\x1f"         => '\u001F',
2371					// Prevent browsers from interpreting these as as special.
2372					"'"            => '\u0027',
2373					'<'            => '\u003C',
2374					'>'            => '\u003E',
2375					'&'            => '\u0026',
2376					// Prevent browsers from interpreting the solidus as special and
2377					// non-compliant JSON parsers from interpreting // as a comment.
2378					'/'            => '\u002F',
2379					// While these are allowed unescaped according to ECMA-262, section
2380					// 15.12.2, they cause problems in some JSON parsers.
2381					"\xe2\x80\xa8" => '\u2028', // U+2028, Line Separator.
2382					"\xe2\x80\xa9" => '\u2029', // U+2029, Paragraph Separator.
2383				);
2384
2385				return '"' . strtr($var, $replace_pairs) . '"';
2386
2387			case 'array':
2388				// Arrays in JSON can't be associative. If the array is empty or if it
2389				// has sequential whole number keys starting with 0, it's not associative
2390				// so we can go ahead and convert it as an array.
2391				if(empty($var) || array_keys($var) === range(0, sizeof($var) - 1))
2392				{
2393					$output = array();
2394					foreach($var as $v)
2395					{
2396						$output[] = $this->toJSONhelper($v);
2397					}
2398					return '[ ' . implode(', ', $output) . ' ]';
2399				}
2400				break;
2401
2402			// Otherwise, fall through to convert the array as an object.
2403			case 'object':
2404				$output = array();
2405				foreach($var as $k => $v)
2406				{
2407					$output[] = $this->toJSONhelper(strval($k)) . ':' . $this->toJSONhelper($v);
2408				}
2409				return '{' . implode(', ', $output) . '}';
2410
2411			default:
2412				return 'null';
2413		}
2414	}
2415
2416
2417	/**
2418	 * Convert Text for RSS/XML use.
2419	 *
2420	 * @param string $text
2421	 * @param boolean $tags [optional]
2422	 * @return string
2423	 */
2424	function toRss($text, $tags = false)
2425	{
2426		if($tags != true)
2427		{
2428			$text = $this->toHTML($text, true);
2429			$text = strip_tags($text);
2430		}
2431
2432		$text = $this->toEmail($text);
2433
2434		$search = array("&amp;#039;", "&amp;#036;", "&#039;", "&#036;", e_BASE, "href='request.php","<!-- bbcode-html-start -->","<!-- bbcode-html-end -->");
2435		$replace = array("'", '$', "'", '$', SITEURL, "href='".SITEURL."request.php", '', '' );
2436		$text = str_replace($search, $replace, $text);
2437
2438		$text = $this->ampEncode($text);
2439
2440		if($tags == true && ($text))
2441		{
2442			$text = "<![CDATA[".$text."]]>";
2443		}
2444
2445		return $text;
2446	}
2447
2448
2449	/**
2450	 * Convert a string to a number (int/float)
2451	 *
2452	 * @param string $value
2453	 * @return int|float
2454	 */
2455	function toNumber($value)
2456	{
2457		// adapted from: https://secure.php.net/manual/en/function.floatval.php#114486
2458		$dotPos = strrpos($value, '.');
2459		$commaPos = strrpos($value, ',');
2460		$sep = (($dotPos > $commaPos) && $dotPos) ? $dotPos :
2461			((($commaPos > $dotPos) && $commaPos) ? $commaPos : false);
2462
2463		if (!$sep) {
2464			return preg_replace("/[^-0-9]/", "", $value);
2465		}
2466
2467		return (
2468			preg_replace("/[^-0-9]/", "", substr($value, 0, $sep)) . '.' .
2469			preg_replace("/[^0-9]/", "", substr($value, $sep+1, strlen($value)))
2470		);
2471	}
2472
2473
2474
2475	/**
2476	 * Clean and Encode Ampersands '&' for output to browser.
2477	 * @param string $text
2478	 * @return mixed|string
2479	 */
2480	function ampEncode($text='')
2481	{
2482		// Fix any left-over '&'
2483		$text = str_replace('&amp;', '&', $text); //first revert any previously converted.
2484		$text = str_replace('&', '&amp;', $text);
2485
2486		return $text;
2487	}
2488
2489
2490	/**
2491	 * Convert any string back to plain text.
2492	 * @param $text
2493	 * @return mixed|string
2494	 */
2495	function toText($text)
2496	{
2497
2498		if($this->isBBcode($text) === true) // convert any bbcodes to html
2499		{
2500			$text = $this->toHTML($text,true);
2501		}
2502
2503		if($this->isHtml($text) === true) // strip any html.
2504		{
2505			$text = $this->toHTML($text,true);
2506			$text = str_replace("\n","",$text); // clean-out line-breaks.
2507			$text = str_ireplace( array("<br>","<br />","<br/>"), "\n", $text);
2508			$text = strip_tags($text);
2509		}
2510
2511		$search = array("&amp;#039;", "&amp;#036;", "&#039;", "&#036;", "&#092;", "&amp;#092;");
2512		$replace = array("'", '$', "'", '$', "\\", "\\");
2513		$text = str_replace($search, $replace, $text);
2514		return $text;
2515	}
2516
2517
2518	/**
2519	 * Set the dimensions of a thumbNail (generated by thumbUrl)
2520	 */
2521	public function setThumbSize($w=null,$h=null,$crop=null)
2522	{
2523		if($w !== null)
2524		{
2525			$this->thumbWidth = intval($w);
2526		}
2527
2528		if($h !== null)
2529		{
2530			$this->thumbHeight = intval($h);
2531		}
2532
2533		if($crop !== null)
2534		{
2535			$this->thumbCrop = intval($crop);
2536		}
2537
2538	}
2539
2540	public function thumbEncode($val = null)
2541	{
2542
2543		if($val !== null)
2544		{
2545			$this->thumbEncode = intval($val);
2546			return null;
2547		}
2548
2549		return $this->thumbEncode;
2550	}
2551
2552
2553	/**
2554	 * Retrieve img tag width and height attributes for current thumbnail.
2555	 * @return string
2556	 */
2557	public function thumbDimensions($type = 'single')
2558	{
2559		if(!empty($this->thumbCrop) && !empty($this->thumbWidth) && !empty($this->thumbHeight)) // dimensions are known.
2560		{
2561			return ($type === 'double') ? 'width="'.$this->thumbWidth.'" height="'.$this->thumbHeight.'"' : "width='".$this->thumbWidth."' height='".$this->thumbHeight."'";
2562		}
2563
2564		return null;
2565	}
2566
2567
2568	/**
2569	 * Set or Get the value of the thumbNail Width.
2570	 * @param $width (optional)
2571	 */
2572	public function thumbWidth($width=null)
2573	{
2574		if($width !== null)
2575		{
2576			$this->thumbWidth = intval($width);
2577		}
2578
2579		return $this->thumbWidth;
2580	}
2581
2582	/**
2583	 * Set or Get the value of the thumbNailbCrop.
2584	 * @param bool $status = true/false
2585	 */
2586	public function thumbCrop($status=false)
2587	{
2588		if($status !== false)
2589		{
2590			$this->thumbCrop = intval($status);
2591		}
2592
2593		return $this->thumbCrop;
2594	}
2595
2596
2597
2598	/**
2599	 * Set or Get the value of the thumbNail height.
2600	 * @param $height (optional)
2601	 */
2602	public function thumbHeight($height= null)
2603	{
2604		if($height !== null)
2605		{
2606			$this->thumbHeight = intval($height);
2607		}
2608
2609		return $this->thumbHeight;
2610
2611	}
2612
2613	/**
2614	 * Generated a Thumb Cache File Name from path and options.
2615	 * @param string $path
2616	 * @param array $options
2617	 * @return null|string
2618	 */
2619	public function thumbCacheFile($path, $options=null, $log=null)
2620	{
2621		if(empty($path))
2622		{
2623			return null;
2624		}
2625
2626		if(is_string($options))
2627		{
2628			parse_str($options,$options);
2629		}
2630
2631		$path = str_replace($this->getUrlConstants('raw'), $this->getUrlConstants('sc'), $path);
2632		$path = $this->replaceConstants(str_replace('..', '', $path));
2633
2634		$filename   = basename($path);
2635		$tmp        = explode('.',$filename);
2636		$ext        = end($tmp);
2637		$len        = strlen($ext) + 1;
2638		$start      = substr($filename,0,- $len);
2639
2640
2641		// cleanup.
2642		$newOpts = array(
2643			'w'     => (string) intval($options['w']),
2644			'h'     => (string) intval($options['h']),
2645			'aw'    => (string) intval($options['aw']),
2646			'ah'    => (string) intval($options['ah']),
2647			'c'     => strtoupper(vartrue($options['c'],'0'))
2648		);
2649
2650		if($log !== null)
2651		{
2652			file_put_contents(e_LOG.$log, "\n++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n\n", FILE_APPEND);
2653			$message = $path."\n".print_r($newOpts,true)."\n\n\n";
2654			file_put_contents(e_LOG.$log, $message, FILE_APPEND);
2655
2656		//	file_put_contents(e_LOG.$log, "\t\tFOUND!!\n\n\n", FILE_APPEND);
2657		}
2658
2659
2660		if(!empty($options['aw']))
2661		{
2662			$options['w'] = $options['aw'];
2663		}
2664
2665		if(!empty($options['ah']))
2666		{
2667			$options['h'] = $options['ah'];
2668		}
2669
2670
2671		$size = varset($options['w'],0).'x'.varset($options['h'],0);
2672
2673		$thumbQuality = e107::getPref('thumbnail_quality',65);
2674
2675		$cache_str = md5(serialize($newOpts).$path. $thumbQuality);
2676
2677		$pre = 'thumb_';
2678		$post = '.cache.bin';
2679
2680	//	$cache_str = http_build_query($newOpts,null,'_'); // testing files.
2681
2682		if(defined('e_MEDIA_STATIC')) // experimental - subject to change.
2683		{
2684			$pre = '';
2685			$post = '';
2686		}
2687
2688		$fname = $pre.strtolower($start.'_'.$cache_str.'_'.$size.'.'.$ext).$post;
2689
2690		return $fname;
2691	}
2692
2693
2694
2695	private function staticCount($val=false)
2696	{
2697
2698		$count = $this->staticCount;
2699
2700		if($val === 0)
2701		{
2702			$this->staticCount = 0;
2703		}
2704		elseif($val !== false)
2705		{
2706			$this->staticCount = $this->staticCount + (int) $val;
2707		}
2708
2709		return (int) $count;
2710
2711	}
2712
2713
2714	/**
2715	 * @todo Move to e107_class ?
2716	 * @param string $path - absolute path
2717	 * @return string - static path.
2718	 */
2719	public function staticUrl($path=null, $opts=array())
2720	{
2721		if(!defined('e_HTTP_STATIC') || deftrue('e_ADMIN_AREA'))
2722		{
2723			// e107::getDebug()->log("e_HTTP_STATIC not defined");
2724			if($path === null)
2725			{
2726				return !empty($opts['full']) ? SITEURL : e_HTTP;
2727			}
2728			else
2729			{
2730				return $path;
2731			}
2732		}
2733
2734
2735		$staticArray = e_HTTP_STATIC;
2736
2737		if(is_array($staticArray))
2738		{
2739			$cnt = count($staticArray);
2740			$staticCount = $this->staticCount();
2741			if($staticCount > ($cnt -1))
2742			{
2743				$staticCount = 0;
2744				$this->staticCount(0);
2745			}
2746
2747			$http = !empty($staticArray[$staticCount]) ? $staticArray[$staticCount] : e_HTTP;
2748
2749		}
2750		else
2751		{
2752			$http = e_HTTP_STATIC;
2753		}
2754
2755		$this->staticCount(1);
2756
2757		if(empty($path))
2758		{
2759			return $http;
2760		}
2761
2762		$base = '';
2763
2764		$srch = array(
2765		//
2766			e_PLUGIN_ABS,
2767			e_THEME_ABS,
2768			e_WEB_ABS,
2769			e_CACHE_IMAGE_ABS,
2770		);
2771
2772
2773		$repl = array(
2774
2775			$http.$base.e107::getFolder('plugins'),
2776			$http.$base.e107::getFolder('themes'),
2777			$http.$base.e107::getFolder('web'),
2778			$http.$base.str_replace('../', '', e_CACHE_IMAGE),
2779		);
2780
2781		$ret = str_replace($srch,$repl,$path);
2782
2783		if(strpos($ret, 'http') !== 0) // if not converted, check media folder also.
2784		{
2785			$ret = str_replace(e_MEDIA_ABS,$http.$base.e107::getFolder('media'),$ret);
2786		}
2787
2788		return $ret;
2789
2790	}
2791
2792
2793	/**
2794	 * Generate an auto-sized Image URL.
2795	 * @param $url - path to image or leave blank for a placeholder. eg. {e_MEDIA}folder/my-image.jpg
2796	 * @param array $options - width and height, but leaving this empty and using $this->thumbWidth() and $this->thumbHeight() is preferred. ie. {SETWIDTH: w=x&y=x}
2797	 * @param int $options ['w'] width (optional)
2798	 * @param int $options ['h'] height (optional)
2799	 * @param bool|string $options ['crop'] true/false or A(auto) or T(op) or B(ottom) or C(enter) or L(eft) or R(right)
2800	 * @param string $options ['scale'] '2x' (optional)
2801	 * @param bool $options ['x'] encode/mask the url parms (optional)
2802	 * @param bool $options ['nosef'] when set to true disabled SEF Url being returned (optional)
2803	 * @param bool $raw set to true when the $url does not being with an e107 variable ie. "{e_XXXX}" eg. {e_MEDIA} (optional)
2804	 * @param bool $full when true returns full http:// url. (optional)
2805	 * @return string
2806	 */
2807	public function thumbUrl($url=null, $options = array(), $raw = false, $full = false)
2808	{
2809		$this->staticCount++; // increment counter.
2810
2811		$ext = pathinfo($url, PATHINFO_EXTENSION);
2812
2813		if($ext === 'svg')
2814		{
2815			return $this->replaceConstants($url, 'abs');
2816		}
2817
2818		if(strpos($url,"{e_") === 0) // Fix for broken links that use {e_MEDIA} etc.
2819		{
2820			//$url = $this->replaceConstants($url,'abs');
2821			// always switch to 'nice' urls when SC is used
2822			$url = str_replace($this->getUrlConstants('sc'), $this->getUrlConstants('raw'), $url);
2823		}
2824
2825		if(is_string($options))
2826		{
2827			parse_str($options, $options);
2828		}
2829
2830		if(!empty($options['scale'])) // eg. scale the width height 2x 3x 4x. etc.
2831		{
2832			$options['return'] = 'src';
2833			$options['size'] = $options['scale'];
2834			unset($options['scale']);
2835			return $this->thumbSrcSet($url,$options);
2836		}
2837
2838
2839
2840
2841
2842		if(strstr($url,e_MEDIA) || strstr($url,e_SYSTEM)) // prevent disclosure of 'hashed' path.
2843		{
2844			$raw = true;
2845		}
2846
2847		if($raw) $url = $this->createConstants($url, 'mix');
2848
2849		$baseurl = ($full ? SITEURL : e_HTTP).'thumb.php?';
2850
2851		if(defined('e_HTTP_STATIC'))
2852		{
2853			$baseurl = $this->staticUrl().'thumb.php?';
2854		}
2855
2856		$thurl = 'src='.urlencode($url).'&amp;';
2857
2858	//	e107::getDebug()->log("Thumb: ".basename($url). print_a($options,true), E107_DBG_BASIC);
2859
2860		if(!empty($options) && (isset($options['w']) || isset($options['aw']) || isset($options['h'])))
2861		{
2862			$options['w']       = varset($options['w']);
2863			$options['h']       = varset($options['h']);
2864			$options['crop']    = (isset($options['aw']) || isset($options['ah'])) ? 1 : varset($options['crop']);
2865			$options['aw']      = varset($options['aw']);
2866			$options['ah']      = varset($options['ah']);
2867			$options['x']       = varset($options['x']);
2868		}
2869		else
2870		{
2871			$options['w']       = $this->thumbWidth;
2872			$options['h']       = $this->thumbHeight;
2873			$options['crop']    = $this->thumbCrop;
2874			$options['aw']      = null;
2875			$options['ah']      = null;
2876			$options['x']       = $this->thumbEncode;
2877
2878		}
2879
2880
2881		if(!empty($options['crop']))
2882		{
2883			if(!empty($options['aw']) || !empty($options['ah']))
2884			{
2885				$options['w']	= $options['aw'] ;
2886				$options['h']	= $options['ah'] ;
2887			}
2888
2889			$thurl .= 'aw='.intval($options['w']).'&amp;ah='.intval($options['h']);
2890
2891			if(!is_numeric($options['crop']))
2892			{
2893				$thurl .= '&amp;c='.$options['crop'];
2894				$options['nosef'] = true;
2895			}
2896
2897		}
2898		else
2899		{
2900
2901			$thurl .= 'w='.intval($options['w']).'&amp;h='.intval($options['h']);
2902
2903		}
2904
2905
2906		if(defined('e_MEDIA_STATIC')) // experimental - subject to change.
2907		{
2908			$opts = str_replace('&amp;', '&', $thurl);
2909
2910			$staticFile = $this->thumbCacheFile($url, $opts);
2911
2912
2913
2914			if(!empty($staticFile) && is_readable(e_CACHE_IMAGE.$staticFile))
2915			{
2916				$staticImg = $this->staticUrl(e_CACHE_IMAGE_ABS.$staticFile);
2917			//	var_dump($staticImg);
2918				return $staticImg;
2919			}
2920
2921		//	echo "<br />static-not-found: ".$staticFile;
2922
2923			$options['nosef'] = true;
2924			$options['x'] = null;
2925			// file_put_contents(e_LOG."thumb.log", "\n++++++++++++++++++++++++++++++++++\n\n", FILE_APPEND);
2926		}
2927
2928
2929		if(e_MOD_REWRITE_MEDIA == true && empty($options['nosef']) )// Experimental SEF URL support.
2930		{
2931			$options['full'] = $full;
2932			$options['ext'] = substr($url,-3);
2933			$options['thurl'] = $thurl;
2934		//	$options['x'] = $this->thumbEncode();
2935
2936			if($sefUrl = $this->thumbUrlSEF($url,$options))
2937			{
2938				return $sefUrl;
2939			}
2940		}
2941
2942		if(!empty($options['x'] ))//base64 encode url
2943		{
2944			$thurl = 'id='.base64_encode($thurl);
2945		}
2946
2947		return $baseurl.$thurl;
2948	}
2949
2950
2951
2952	/**
2953	 * Split a thumb.php url into an array which can be parsed back into the thumbUrl method. .
2954	 * @param $src
2955	 * @return array
2956	 */
2957	function thumbUrlDecode($src)
2958	{
2959		list($url,$qry) = array_pad(explode("?",$src), 2, null);
2960
2961		$ret = array();
2962
2963		if(strstr($url,"thumb.php") && !empty($qry)) // Regular
2964		{
2965			parse_str($qry,$val);
2966			$ret = $val;
2967		}
2968		elseif(preg_match('/media\/img\/(a)?([\d]*)x(a)?([\d]*)\/(.*)/',$url,$match)) // SEF
2969		{
2970			$wKey = $match[1].'w';
2971			$hKey = $match[3].'h';
2972
2973			$ret = array(
2974				'src'=> 'e_MEDIA_IMAGE/'.$match[5],
2975				$wKey => $match[2],
2976				$hKey => $match[4]
2977			);
2978		}
2979		elseif(preg_match('/theme\/img\/(a)?([\d]*)x(a)?([\d]*)\/(.*)/', $url, $match)) // Theme-image SEF Urls
2980		{
2981			$wKey = $match[1].'w';
2982			$hKey = $match[3].'h';
2983
2984			$ret = array(
2985				'src'=> 'e_THEME/'.$match[5],
2986				$wKey => $match[2],
2987				$hKey => $match[4]
2988			);
2989
2990		}
2991		elseif(defined('TINYMCE_DEBUG'))
2992		{
2993			print_a("thumbUrlDecode: No Matches");
2994
2995		}
2996
2997
2998		return $ret;
2999	}
3000
3001
3002
3003	/**
3004	 * Experimental: Generate a Thumb URL for use in the img srcset attribute.
3005	 * @param string $src eg. {e_MEDIA_IMAGE}myimage.jpg
3006	 * @param int|string|array $width - desired size in px or '2x' or '3x' or null for all or array (
3007	 * @return string
3008	 */
3009	function thumbSrcSet($src='', $width=null)
3010	{
3011		$multiply = null;
3012		$encode = false;
3013
3014		if(is_array($width))
3015		{
3016			$parm = $width;
3017			$multiply = $width['size'];
3018			$encode = (!empty($width['x'])) ? $width['x'] : false;
3019			$width = $width['size'];
3020		}
3021
3022
3023	//	$encode =  $this->thumbEncode();;
3024		if($width == null || $width=='all')
3025		{
3026			$links = array();
3027			$mag = ($width == null) ? array(1, 2) : array(160,320,460,600,780,920,1100);
3028			foreach($mag as $v)
3029			{
3030				$w = ($this->thumbWidth * $v);
3031				$h =  ($this->thumbHeight * $v);
3032
3033				$att = (!empty($this->thumbCrop)) ? array('aw' => $w, 'ah' => $h) : array('w' => $w, 'h' => $h);
3034				$att['x'] = $encode;
3035
3036				$add = ($width == null) ? " ".$v."x" : " ".$v."w";
3037				$links[] = $this->thumbUrl($src, $att).$add; // " w".$width; //
3038			}
3039
3040			return implode(", ",$links);
3041
3042		}
3043		elseif($multiply === '2x' || $multiply === '3x' || $multiply === '4x')
3044		{
3045			$multiInt = (int) $multiply;
3046
3047			if(empty($parm['w']) && isset($parm['h']))
3048			{
3049				$parm['h'] = ($parm['h'] * $multiInt) ;
3050				return $this->thumbUrl($src, $parm)." ".$multiply;
3051			}
3052
3053			if(isset($parm['w']) && !isset($parm['h'])) // if w set, assume h value of 0 is set.
3054			{
3055				$parm['h'] = 0;
3056			}
3057
3058			$width = !empty($parm['w']) ? (intval($parm['w']) * $multiInt) : (intval($this->thumbWidth) * $multiInt);
3059			$height = isset($parm['h']) ? (intval($parm['h']) * $multiInt) : (intval($this->thumbHeight) * $multiInt);
3060
3061		}
3062		else
3063		{
3064			$height = (($this->thumbHeight * $width) / $this->thumbWidth);
3065
3066		}
3067
3068
3069
3070		if(!isset($parm['aw']))
3071		{
3072			$parm['aw'] = null;
3073		}
3074
3075		if(!isset($parm['ah']))
3076		{
3077			$parm['ah'] = null;
3078		}
3079
3080		if(!isset($parm['x']))
3081		{
3082			$parm['x'] = null;
3083		}
3084
3085		if(!isset($parm['crop']))
3086		{
3087			$parm['crop'] = null;
3088		}
3089
3090		$parms = array('w'=>$width,'h'=>$height,'crop'=> $parm['crop'],'x'=>$parm['x'], 'aw'=>$parm['aw'],'ah'=>$parm['ah']);
3091
3092	//	$parms = !empty($this->thumbCrop) ? array('aw' => $width, 'ah' => $height, 'x'=>$encode) : array('w'  => $width,	'h'  => $height, 'x'=>$encode	);
3093
3094		// $parms['x'] = $encode;
3095
3096		if(!empty($parm['return']) && $parm['return'] === 'src')
3097		{
3098			return $this->thumbUrl($src, $parms);
3099		}
3100
3101		$ret = $this->thumbUrl($src, $parms);
3102
3103		$ret .= ($multiply) ? " ".$multiply : " ".$width."w";
3104
3105        return $ret;
3106
3107	}
3108
3109
3110	public function thumbUrlScale($src,$parm)
3111	{
3112
3113
3114
3115	}
3116
3117	/**
3118	 * Used by thumbUrl when SEF Image URLS is active. @see e107.htaccess
3119	 * @param $url
3120	 * @param array $options
3121	 * @return string
3122	 */
3123	private function thumbUrlSEF($url='', $options=array())
3124	{
3125		if(!empty($options['full']))
3126		{
3127			$base = SITEURL;
3128		}
3129		else
3130		{
3131			$base = (!empty($options['ebase'])) ? '{e_BASE}' : e_HTTP;
3132		}
3133
3134		if(defined('e_HTTP_STATIC'))
3135		{
3136			$base = $this->staticUrl();
3137		}
3138	//	$base = (!empty($options['full'])) ? SITEURL : e_HTTP;
3139
3140		if(!empty($options['x'])  && !empty($options['ext'])) // base64 encoded. Build URL for:  RewriteRule ^media\/img\/([-A-Za-z0-9+/]*={0,3})\.(jpg|gif|png)?$ thumb.php?id=$1
3141		{
3142			$ext = strtolower($options['ext']);
3143			return $base.'media/img/'.base64_encode($options['thurl']).'.'.str_replace("jpeg", "jpg", $ext);
3144		}
3145		elseif(strstr($url, 'e_MEDIA_IMAGE')) // media images.
3146		{
3147			$sefPath = 'media/img/';
3148			$clean = array('{e_MEDIA_IMAGE}','e_MEDIA_IMAGE/');
3149		}
3150		elseif(strstr($url, 'e_AVATAR')) // avatars
3151		{
3152			$sefPath = 'media/avatar/';
3153			$clean = array('{e_AVATAR}','e_AVATAR/');
3154		}
3155		elseif(strstr($url, 'e_THEME')) // theme folder images.
3156		{
3157			$sefPath = 'theme/img/';
3158			$clean = array('{e_THEME}','e_THEME/');
3159		}
3160		else
3161		{
3162			// e107::getDebug()->log("SEF URL False: ".$url);
3163			return false;
3164		}
3165
3166		// Build URL for ReWriteRule ^media\/img\/(a)?([\d]*)x(a)?([\d]*)\/(.*)?$ thumb.php?src=e_MEDIA_IMAGE/$5&$1w=$2&$3h=$4
3167		$sefUrl =  $base.$sefPath;
3168
3169		if(vartrue($options['aw']) || vartrue($options['ah']))
3170		{
3171			$sefUrl .= 'a'.intval($options['aw']) .'xa'. intval($options['ah']);
3172		}
3173		elseif(!empty($options['crop']))
3174		{
3175
3176			if(!is_numeric($options['crop']))
3177			{
3178				$sefUrl .= strtolower($options['crop']).intval($options['w']) .'x'.strtolower($options['crop']). intval($options['h']);
3179			}
3180			else
3181			{
3182				$sefUrl .= 'a'.intval($options['w']) .'xa'. intval($options['h']);
3183			}
3184
3185
3186		}
3187		else
3188		{
3189			$sefUrl .= intval($options['w']) .'x'. intval($options['h']);
3190		}
3191
3192		$sefUrl .= '/';
3193		$sefUrl .= str_replace($clean,'',$url);
3194
3195		return $sefUrl;
3196
3197	}
3198
3199	/**
3200	 * Help for converting to more safe URLs
3201	 * e.g. {e_MEDIA_FILE}path/to/video.flv => e_MEDIA_FILE/path/to/video.flv
3202	 *
3203	 * @todo support for ALL URL shortcodes (replacement methods)
3204	 * @param string $type sc|raw|rev|all
3205	 * @return array
3206	 */
3207	public function getUrlConstants($type = 'sc')
3208	{
3209		// sub-folders first!
3210		static $array = array(
3211			'e_MEDIA_FILE/' 	=> '{e_MEDIA_FILE}',
3212			'e_MEDIA_VIDEO/' 	=> '{e_MEDIA_VIDEO}',
3213			'e_MEDIA_IMAGE/' 	=> '{e_MEDIA_IMAGE}',
3214			'e_MEDIA_ICON/' 	=> '{e_MEDIA_ICON}',
3215			'e_AVATAR/' 		=> '{e_AVATAR}',
3216			'e_AVATAR_DEFAULT/' => '{e_AVATAR_DEFAULT}',
3217			'e_AVATAR_UPLOAD/' => '{e_AVATAR_UPLOAD}',
3218			'e_WEB_JS/' 		=> '{e_WEB_JS}',
3219			'e_WEB_CSS/' 		=> '{e_WEB_CSS}',
3220			'e_WEB_IMAGE/' 		=> '{e_WEB_IMAGE}',
3221			'e_IMPORT/' 		=> '{e_IMPORT}',
3222		//	'e_WEB_PACK/' 		=> '{e_WEB_PACK}',
3223
3224			'e_BASE/' 			=> '{e_BASE}',
3225			'e_ADMIN/' 			=> '{e_ADMIN}',
3226			'e_IMAGE/' 			=> '{e_IMAGE}',
3227			'e_THEME/' 			=> '{e_THEME}',
3228			'e_PLUGIN/' 		=> '{e_PLUGIN}',
3229			'e_HANDLER/' 		=> '{e_HANDLER}', // BC
3230			'e_MEDIA/' 			=> '{e_MEDIA}',
3231			'e_WEB/' 			=> '{e_ADMIN}',
3232	//		'THEME/'			=> '{THEME}',
3233		);
3234
3235
3236		switch ($type)
3237		{
3238			case 'sc':
3239				return array_values($array);
3240			break;
3241
3242			case 'raw':
3243				return array_keys($array);
3244			break;
3245
3246			case 'rev':
3247				return array_reverse($array, true);
3248			break;
3249
3250			case 'all':
3251				return $array;
3252			break;
3253		}
3254		return array();
3255	}
3256
3257
3258	function getEmotes()
3259	{
3260		return $this->e_emote->emotes;
3261	}
3262
3263
3264	/**
3265	 * Replace e107 path constants
3266	 * Note: only an ADMIN user can convert {e_ADMIN}
3267	 * TODO - runtime cache of search/replace arrays (object property) when $mode !== ''
3268	 * @param string $text
3269	 * @param string $mode [optional]	abs|full "full" = produce absolute URL path, e.g. http://sitename.com/e107_plugins/etc
3270	 * 									'abs' = produce truncated URL path, e.g. e107plugins/etc
3271	 * 									"" (default) = URL's get relative path e.g. ../e107_plugins/etc
3272	 * @param mixed $all [optional] 	if TRUE, then when $mode is "full" or TRUE, USERID is also replaced...
3273	 * 									when $mode is "" (default), ALL other e107 constants are replaced
3274	 * @return array|string
3275	 */
3276	public function replaceConstants($text, $mode = '', $all = FALSE)
3277	{
3278		if(is_array($text))
3279		{
3280			$new = array();
3281			foreach($text as $k=>$v)
3282			{
3283				$new[$k] = $this->replaceConstants($v,$mode,$all);
3284			}
3285
3286			return $new;
3287		}
3288
3289		if($mode != "")
3290		{
3291			$e107 = e107::getInstance();
3292
3293			$replace_relative = array(
3294				$e107->getFolder('media_files'),
3295				$e107->getFolder('media_video'),
3296				$e107->getFolder('media_image'),
3297				$e107->getFolder('media_icon'),
3298				$e107->getFolder('avatars'),
3299				$e107->getFolder('web_js'),
3300				$e107->getFolder('web_css'),
3301				$e107->getFolder('web_image'),
3302				//$e107->getFolder('web_pack'),
3303				e_IMAGE_ABS,
3304				e_THEME_ABS,
3305				$e107->getFolder('images'),
3306				$e107->getFolder('plugins'),
3307				$e107->getFolder('files'),
3308				$e107->getFolder('themes'),
3309			//	$e107->getFolder('downloads'),
3310				$e107->getFolder('handlers'),
3311				$e107->getFolder('media'),
3312				$e107->getFolder('web'),
3313				$e107->site_theme ? $e107->getFolder('themes').$e107->site_theme.'/' : '',
3314				defset('THEME_ABS'),
3315				(ADMIN ? $e107->getFolder('admin') : ''),
3316				'',
3317				$e107->getFolder('core'),
3318				$e107->getFolder('system'),
3319			);
3320
3321			switch ($mode)
3322			{
3323				case 'abs':
3324					$replace_absolute = array(
3325						e_MEDIA_FILE_ABS,
3326						e_MEDIA_VIDEO_ABS,
3327						e_MEDIA_IMAGE_ABS,
3328						e_MEDIA_ICON_ABS,
3329						e_AVATAR_ABS,
3330						e_JS_ABS,
3331						e_CSS_ABS,
3332						e_WEB_IMAGE_ABS,
3333				//		e_PACK_ABS,
3334						e_IMAGE_ABS,
3335						e_THEME_ABS,
3336						e_IMAGE_ABS,
3337						e_PLUGIN_ABS,
3338						e_FILE_ABS,
3339						e_THEME_ABS,
3340				//		e_DOWNLOAD_ABS, //impossible when download is done via php.
3341						'', // handlers - no ABS path available
3342						e_MEDIA_ABS,
3343						e_WEB_ABS,
3344						defset('THEME_ABS'),
3345						defset('THEME_ABS'),
3346						(ADMIN ? e_ADMIN_ABS : ''),
3347						$e107->server_path,
3348						'', // no e_CORE absolute path
3349						'', // no e_SYSTEM absolute path
3350					);
3351				break;
3352
3353				case 'full':
3354					$replace_absolute = array(
3355						SITEURLBASE.e_MEDIA_FILE_ABS,
3356						SITEURLBASE.e_MEDIA_VIDEO_ABS,
3357						SITEURLBASE.e_MEDIA_IMAGE_ABS,
3358						SITEURLBASE.e_MEDIA_ICON_ABS,
3359						SITEURLBASE.e_AVATAR_ABS,
3360						SITEURLBASE.e_JS_ABS,
3361						SITEURLBASE.e_CSS_ABS,
3362						SITEURLBASE.e_WEB_IMAGE_ABS,
3363				//		SITEURLBASE.e_PACK_ABS,
3364						SITEURLBASE.e_IMAGE_ABS,
3365						SITEURLBASE.e_THEME_ABS,
3366						SITEURLBASE.e_IMAGE_ABS,
3367						SITEURLBASE.e_PLUGIN_ABS,
3368						SITEURLBASE.e_FILE_ABS, // deprecated
3369						SITEURLBASE.e_THEME_ABS,
3370						//SITEURL.$e107->getFolder('downloads'),
3371						'', //  handlers - no ABS path available
3372						SITEURLBASE.e_MEDIA_ABS,
3373						SITEURLBASE.e_WEB_ABS,
3374						defset('THEME_ABS') ? SITEURLBASE.THEME_ABS : '',
3375						defset('THEME_ABS') ? SITEURLBASE.THEME_ABS : '',
3376						(ADMIN ? SITEURLBASE.e_ADMIN_ABS : ''),
3377						SITEURL,
3378						'', // no e_CORE absolute path
3379						'', // no e_SYSTEM absolute path
3380					);
3381				break;
3382			}
3383			// sub-folders first!
3384			$search = array(
3385				'{e_MEDIA_FILE}',
3386				'{e_MEDIA_VIDEO}',
3387				'{e_MEDIA_IMAGE}',
3388				'{e_MEDIA_ICON}',
3389				'{e_AVATAR}',
3390				'{e_WEB_JS}',
3391				'{e_WEB_CSS}',
3392				'{e_WEB_IMAGE}',
3393		//		'{e_WEB_PACK}',
3394				"{e_IMAGE_ABS}",
3395				"{e_THEME_ABS}",
3396				"{e_IMAGE}",
3397				"{e_PLUGIN}",
3398				"{e_FILE}",
3399				"{e_THEME}",
3400				//,"{e_DOWNLOAD}"
3401				"{e_HANDLER}",
3402				"{e_MEDIA}",
3403				"{e_WEB}",
3404				"{THEME}",
3405				"{THEME_ABS}",
3406				"{e_ADMIN}",
3407				"{e_BASE}",
3408				"{e_CORE}",
3409				"{e_SYSTEM}",
3410			);
3411
3412			/*if (ADMIN)
3413			{
3414				$replace_relative[] = $e107->getFolder('admin');
3415				$replace_absolute[] = SITEURL.$e107->getFolder('admin');
3416				$search[] = "{e_ADMIN}";
3417			}*/
3418
3419			if ($all)
3420			{
3421				if (USER)
3422				{  // Can only replace with valid number for logged in users
3423					$replace_relative[] = USERID;
3424					$replace_absolute[] = USERID;
3425				}
3426				else
3427				{
3428					$replace_relative[] = '';
3429					$replace_absolute[] = '';
3430				}
3431				$search[] = "{USERID}";
3432			}
3433
3434			// current THEME
3435			/*if(!defined('THEME'))
3436			{
3437				//if not already parsed by doReplace
3438				$text = str_replace(array('{THEME}', '{THEME_ABS}'), '', $text);
3439			}
3440			else
3441			{
3442				$replace_relative[] = THEME;
3443				$replace_absolute[] = THEME_ABS;
3444				$search[] = "{THEME}";
3445				$replace_relative[] = THEME;
3446				$replace_absolute[] = THEME_ABS;
3447				$search[] = "{THEME_ABS}";
3448			}*/
3449
3450			$replace = ((string)$mode == "full" || (string)$mode=='abs' ) ? $replace_absolute : $replace_relative;
3451			return str_replace($search,$replace,$text);
3452		}
3453
3454//		$pattern = ($all ? "#\{([A-Za-z_0-9]*)\}#s" : "#\{(e_[A-Z]*)\}#s");
3455		$pattern = ($all ? '#\{([A-Za-z_0-9]*)\}#s' : '#\{(e_[A-Z]*(?:_IMAGE|_VIDEO|_FILE|_CONTENT|_ICON|_AVATAR|_JS|_CSS|_PACK|_DB|_ABS){0,1})\}#s');
3456		$text = preg_replace_callback($pattern, array($this, 'doReplace'), $text);
3457
3458		if(!defined('THEME'))
3459		{
3460			//if not already parsed by doReplace
3461			$text = str_replace(array('{THEME}', '{THEME_ABS}'), '', $text);
3462		}
3463		else
3464		{
3465			$srch = array('{THEME}', '{THEME_ABS}');
3466			$repl = array(THEME, THEME_ABS);
3467			$text = str_replace($srch, $repl, $text);
3468		}
3469
3470		return $text;
3471	}
3472
3473
3474	function doReplace($matches)
3475	{
3476		if(defined($matches[1]) && (deftrue('ADMIN') || strpos($matches[1], 'ADMIN') === FALSE))
3477		{
3478			return constant($matches[1]);
3479		}
3480		return $matches[1];
3481	}
3482
3483	/**
3484	 * Create and substitute e107 constants in passed URL
3485	 *
3486	 * @param string $url
3487	 * @param integer $mode 0-folders, 1-relative ('rel'), 2-absolute ('abs'), 3-full ('full') (with domain), 4-absolute & relative ('mix') (combination of 1,2,3)
3488	 * @return string
3489	 */
3490	public function createConstants($url, $mode = 0)
3491	{
3492
3493		//FIXME - create constants for absolute paths and site URL's
3494		if (!is_numeric($mode))
3495		{
3496			switch ($mode)
3497			{
3498				case 'rel' : $mode = 1; break;
3499				case 'abs' : $mode = 2; break;
3500				case 'full' : $mode = 3; break;
3501				case 'mix' : $mode = 4; break;
3502				case 'nice': $mode = 5; break;
3503			}
3504		}
3505		$e107 = e107::getInstance();
3506		switch($mode)
3507		{
3508			case 0: // folder name only.
3509				$tmp = array(
3510					'{e_MEDIA_FILE}'	=> $e107->getFolder('media_files'),
3511					'{e_MEDIA_VIDEO}'	=> $e107->getFolder('media_videos'),
3512					'{e_MEDIA_IMAGE}'	=> $e107->getFolder('media_images'),
3513					'{e_MEDIA_ICON}'	=> $e107->getFolder('media_icons'),
3514					'{e_AVATAR}'		=> $e107->getFolder('avatars'),
3515					'{e_WEB_JS}'		=> $e107->getFolder('web_js'),
3516					'{e_WEB_CSS}'		=> $e107->getFolder('web_css'),
3517					'{e_WEB_IMAGE}'		=> $e107->getFolder('web_images'),
3518			//		'{e_WEB_PACK}'		=> $e107->getFolder('web_packs'),
3519
3520					'{e_IMAGE}' 	=> $e107->getFolder('images'),
3521					'{e_PLUGIN}'	=> $e107->getFolder('plugins'),
3522					'{e_FILE}'		=> $e107->getFolder('files'),
3523					'{e_THEME}'		=> $e107->getFolder('themes'),
3524					'{e_DOWNLOAD}'	=> $e107->getFolder('downloads'),
3525					'{e_ADMIN}'		=> $e107->getFolder('admin'),
3526					'{e_HANDLER}'	=> $e107->getFolder('handlers'),
3527					'{e_MEDIA}'		=> $e107->getFolder('media'),
3528					'{e_WEB}'		=> $e107->getFolder('web'),
3529					'{e_UPLOAD}'	=> $e107->getFolder('uploads'),
3530					);
3531
3532			break;
3533
3534
3535
3536			case 1: // relative path only
3537				$tmp = array(
3538					'{e_MEDIA_FILE}'	=> e_MEDIA_FILE,
3539					'{e_MEDIA_VIDEO}'	=> e_MEDIA_VIDEO,
3540					'{e_MEDIA_IMAGE}'	=> e_MEDIA_IMAGE,
3541					'{e_MEDIA_ICON}'	=> e_MEDIA_ICON,
3542					'{e_AVATAR}'		=> e_AVATAR,
3543					'{e_IMPORT}'		=> e_IMPORT,
3544					'{e_WEB_JS}'		=> e_WEB_JS,
3545					'{e_WEB_CSS}'		=> e_WEB_CSS,
3546					'{e_WEB_IMAGE}'		=> e_WEB_IMAGE,
3547				//	'{e_WEB_PACK}'		=> e_WEB_PACK,
3548
3549					'{e_IMAGE}'		=> e_IMAGE,
3550					'{e_PLUGIN}'	=> e_PLUGIN,
3551					'{e_FILE}'		=> e_FILE,
3552					'{e_THEME}'		=> e_THEME,
3553					'{e_DOWNLOAD}'	=> e_DOWNLOAD,
3554					'{e_ADMIN}'		=> e_ADMIN,
3555					'{e_HANDLER}'	=> e_HANDLER,
3556					'{e_MEDIA}'		=> e_MEDIA,
3557					'{e_WEB}'		=> e_WEB,
3558					'{e_UPLOAD}'	=> e_UPLOAD,
3559				);
3560			break;
3561
3562			case 2: // absolute path only
3563				$tmp = array(
3564					'{e_MEDIA_FILE}'	=> e_MEDIA_FILE_ABS,
3565					'{e_MEDIA_VIDEO}'	=> e_MEDIA_VIDEO_ABS,
3566					'{e_MEDIA_IMAGE}'	=> e_MEDIA_IMAGE_ABS,
3567					'{e_MEDIA_ICON}'	=> e_MEDIA_ICON_ABS,
3568					'{e_AVATAR}'		=> e_AVATAR_ABS,
3569					'{e_WEB_JS}'		=> e_JS_ABS,
3570					'{e_WEB_CSS}'		=> e_CSS_ABS,
3571					'{e_WEB_IMAGE}'		=> e_WEB_IMAGE_ABS,
3572			//		'{e_WEB_PACK}'		=> e_PACK_ABS,
3573
3574					'{e_IMAGE}'		=> e_IMAGE_ABS,
3575					'{e_PLUGIN}'	=> e_PLUGIN_ABS,
3576					'{e_FILE}'		=> e_FILE_ABS, // deprecated
3577					'{e_THEME}'		=> e_THEME_ABS,
3578					'{e_DOWNLOAD}'	=> e_HTTP.'request.php?',// FIXME - we need solution!
3579					'{e_ADMIN}'		=> e_ADMIN_ABS,
3580					//'{e_HANDLER}'	=> e_HANDLER_ABS, - no ABS path available
3581					'{e_MEDIA}'		=> e_MEDIA_ABS,
3582					'{e_WEB}'		=> e_WEB_ABS,
3583					'{e_BASE}'		=> e_HTTP,
3584				);
3585			break;
3586
3587			case 3: // full path (e.g http://domain.com/e107_images/)
3588				$tmp = array(
3589					'{e_MEDIA_FILE}'	=> SITEURLBASE.e_MEDIA_FILE_ABS,
3590					'{e_MEDIA_VIDEO}'	=> SITEURLBASE.e_MEDIA_VIDEO_ABS,
3591					'{e_MEDIA_IMAGE}'	=> SITEURLBASE.e_MEDIA_IMAGE_ABS,
3592					'{e_MEDIA_ICON}'	=> SITEURLBASE.e_MEDIA_ICON_ABS,
3593					'{e_AVATAR}'		=> SITEURLBASE.e_AVATAR_ABS,
3594					'{e_WEB_JS}'		=> SITEURLBASE.e_JS_ABS,
3595					'{e_WEB_CSS}'		=> SITEURLBASE.e_CSS_ABS,
3596					'{e_WEB_IMAGE}'		=> SITEURLBASE.e_WEB_IMAGE_ABS,
3597			//		'{e_WEB_PACK}'		=> SITEURLBASE.e_PACK_ABS,
3598
3599					'{e_IMAGE}'		=> SITEURLBASE.e_IMAGE_ABS,
3600					'{e_PLUGIN}'	=> SITEURLBASE.e_PLUGIN_ABS,
3601					'{e_FILE}'		=> SITEURLBASE.e_FILE_ABS, // deprecated
3602					'{e_THEME}'		=> SITEURLBASE.e_THEME_ABS,
3603					'{e_DOWNLOAD}'	=> SITEURLBASE.e_HTTP.'request.php?',// FIXME - we need solution!
3604					'{e_ADMIN}'		=> SITEURLBASE.e_ADMIN_ABS,
3605					//'{e_HANDLER}'	=> e_HANDLER_ABS, - no ABS path available
3606					'{e_MEDIA}'		=> SITEURLBASE.e_MEDIA_ABS,
3607					'{e_WEB}'		=> SITEURLBASE.e_WEB_ABS,
3608					'{e_BASE}'		=> SITEURL,
3609				);
3610			break;
3611
3612			case 4: // absolute & relative paths
3613				$url = $this->createConstants($url, 3);
3614				$url = $this->createConstants($url, 2);
3615				$url = $this->createConstants($url, 1);
3616				return $url;
3617			break;
3618
3619			case 5: // nice urls - e.g. e_MEDIA_VIDEO/mystream.flv
3620				$url = $this->createConstants($url, 4);
3621				return str_replace($this->getUrlConstants('sc'), $this->getUrlConstants('raw'), $url);
3622			break;
3623
3624			default:
3625				$tmp = array();
3626			break;
3627		}
3628
3629		$hasCDN = strpos($url, '//') === 0;
3630
3631		foreach($tmp as $key=>$val)
3632		{
3633			// Fix - don't break the CDN '//cdn.com' URLs
3634			if ($hasCDN && $val === '/') {
3635				continue;
3636			}
3637
3638			$len = strlen($val);
3639			if(substr($url, 0, $len) == $val)
3640			{
3641				// replace the first instance only
3642				return substr_replace($url, $key, 0, $len);
3643			}
3644		}
3645
3646		return $url;
3647	}
3648
3649
3650	//FIXME - $match not used?
3651	function e_highlight($text, $match)
3652	{
3653		$tags = array();
3654		preg_match_all('#<[^>]+>#', $text, $tags);
3655		$text = preg_replace('#<[^>]+>#', '<|>', $text);
3656		$text = preg_replace('#(\b".$match."\b)#i', '<span class="searchhighlight">\\1</span>', $text);
3657		foreach ($tags[0] as $tag)
3658		{
3659			$text = preg_replace('#<\|>#', $tag, $text, 1);
3660		}
3661		return $text;
3662	}
3663
3664
3665
3666
3667	/**
3668	 * Convert Text to a suitable format for use in emails. eg. relative links will be replaced with full links etc.
3669	 * @param string $text
3670	 * @param boolean $posted - if the text has been posted. (uses stripslashes etc)
3671	 * @param string $mods - flags for text transformation.
3672	 */
3673	public function toEmail($text, $posted = "", $mods = "parse_sc, no_make_clickable")
3674	{
3675		if ($posted === TRUE)
3676		{
3677			if (MAGIC_QUOTES_GPC)
3678			{
3679				$text = stripslashes($text);
3680			}
3681			$text = preg_replace('#\[(php)#i', '&#91;\\1', $text);
3682		}
3683
3684		$text = (strtolower($mods) != "rawtext") ? $this->replaceConstants($text, "full") : $text;
3685
3686		if($this->isHtml($text))
3687		{
3688			$text = str_replace(array("[html]","[/html]"), "", $text);
3689			$text = html_entity_decode( $text, ENT_COMPAT, 'UTF-8');
3690		}
3691		else
3692		{
3693
3694			$text = $this->toHTML($text, true, $mods);
3695		}
3696
3697		return $text;
3698	}
3699
3700
3701
3702	/**
3703	 * Given an email address, returns a link including with obfuscated text.
3704	 * e-email css in e107.css inserts the user/domain data for display.
3705	 *
3706	 * @param string $email
3707	 * @param string $words [optional] text to display
3708	 * @param null $subject [optional] default subject for email.
3709	 * @return string
3710	 */
3711	function emailObfuscate($email, $words = null, $subject =null)
3712	{
3713		if(strpos($email, '@') === false)
3714		{
3715			return '';
3716		}
3717
3718		if ($subject)
3719		{
3720			$subject = '?subject='.$subject;
3721		}
3722
3723		list($name, $address) = explode('@', $email, 2);
3724
3725		if(empty($words))
3726		{
3727			$words = "&#64;";
3728			$user = "data-user='".$this->obfuscate($name)."'";
3729			$dom =  "data-dom='".$this->obfuscate($address)."'";
3730		}
3731		else
3732		{
3733			$user = '';
3734			$dom = '';
3735		}
3736
3737		$url = "mailto:".$email.$subject;
3738
3739		$safe = $this->obfuscate($url);
3740
3741		return "<a class='e-email' {$user} {$dom} rel='external' href='".$safe."'>".$words.'</a>';
3742	}
3743
3744
3745
3746	/**
3747	 * Obfuscate text from bots using Randomized encoding.
3748	 * @param $text
3749	 * @return string
3750	 */
3751	public function obfuscate($text)
3752	{
3753		$ret = '';
3754		foreach (str_split($text) as $letter)
3755		{
3756			switch (rand(1, 3))
3757			{
3758				// HTML entity code
3759				case 1:
3760					$ret .= '&#'.ord($letter).';';
3761				break;
3762
3763				// Hex character code
3764				case 2:
3765					$ret .= '&#x'.dechex(ord($letter)).';';
3766				break;
3767
3768				// Raw (no) encoding
3769				case 3:
3770					$ret .= $letter;
3771			}
3772		}
3773
3774		return $ret;
3775	}
3776
3777
3778
3779
3780	public function __get($name)
3781	{
3782		switch($name)
3783		{
3784			case 'e_sc':
3785				$ret = e107::getScParser();
3786			break;
3787
3788
3789			default:
3790				trigger_error('$e107->$'.$name.' not defined', E_USER_WARNING);
3791				return NULL;
3792			break;
3793		}
3794
3795
3796		$this->$name = $ret;
3797		return $ret;
3798	}
3799}
3800
3801
3802	/**
3803	 * New v2 Parser
3804	 * Start Fresh and Build on it over time to become eventual replacement to e_parse.
3805	 * Cameron's DOM-based parser.
3806	 *
3807	 * @method replaceConstants($text, $mode = '', $all = false)
3808	 * @method toAttribute($title)
3809	 * @method thumbUrl($icon)
3810	 * @method thumbDimensions()
3811	 */
3812class e_parser
3813{
3814    /**
3815     * @var DOMDocument
3816     */
3817    public $domObj                  = null;
3818	public $isHtml                  = false;
3819
3820
3821	protected $bootstrap            = null;
3822	protected $fontawesome          = null;
3823
3824    protected $removedList          = array();
3825    protected $nodesToDelete        = array();
3826    protected $nodesToConvert       = array();
3827    protected $nodesToDisableSC     = array();
3828    protected $pathList             = array();
3829    protected $allowedAttributes    = array(
3830                                    'default'   => array('id', 'style', 'class', 'title', 'lang', 'accesskey'),
3831                                    'img'       => array('src', 'alt', 'width', 'height'),
3832                                    'a'         => array('href', 'target', 'rel'),
3833                                    'script'	=> array('type', 'src', 'language', 'async'),
3834                                    'iframe'	=> array('src', 'frameborder', 'width', 'height'),
3835	                                'input'     => array('type','name','value'),
3836	                                'form'      => array('action','method','target'),
3837	                                'audio'     => array('src','controls', 'autoplay', 'loop', 'muted', 'preload' ),
3838	                                'video'     => array('autoplay', 'controls', 'height', 'loop', 'muted', 'poster', 'preload', 'src', 'width'),
3839	                                'td'        => array('colspan', 'rowspan'),
3840	                                'th'        => array('colspan', 'rowspan'),
3841	                                'col'       => array('span'),
3842		                            'embed'     => array('src', 'wmode', 'type', 'width', 'height'),
3843									'x-bbcode'  => array('alt'),
3844									'label'		=> array('for'),
3845									'source'    => array('media', 'sizes', 'src', 'srcset', 'type'),
3846
3847                                  );
3848
3849    protected $badAttrValues     = array('javascript[\s]*?:','alert\(','vbscript[\s]*?:','data:text\/html', 'mhtml[\s]*?:', 'data:[\s]*?image');
3850
3851    protected $replaceAttrValues = array(
3852        'default' => array()
3853    );
3854
3855    protected $allowedTags        = array('html', 'body','div','a','img','table','tr', 'td', 'th', 'tbody', 'thead', 'colgroup', 'b',
3856                                        'i', 'pre','code', 'strong', 'u', 'em','ul', 'ol', 'li','img','h1','h2','h3','h4','h5','h6','p',
3857                                        'div','pre','section','article', 'blockquote','hgroup','aside','figure','figcaption', 'abbr','span', 'audio', 'video', 'source', 'br',
3858                                        'small', 'caption', 'noscript', 'hr', 'section', 'iframe', 'sub', 'sup', 'cite', 'x-bbcode', 'label'
3859                                   );
3860    protected $scriptTags 		= array('script','applet','form','input','button', 'embed', 'object', 'ins', 'select','textarea'); //allowed when $pref['post_script'] is enabled.
3861
3862    protected $scriptAttributes = array('onclick', 'onchange', 'onblur', 'onload', 'onfocus', 'onkeydown', 'onkeypress', 'onkeyup',
3863                                        'ondblclick', 'onmousedown', 'onmousemove', 'onmouseout', 'onmouseover', 'onmouseup', 'onmousewheel',
3864                                        'onwheel', 'oncopy', 'oncut', 'onpaste'
3865                                    );
3866
3867	protected $blockTags		= array('pre','div','h1','h2','h3','h4','h5','h6','blockquote'); // element includes its own line-break.
3868
3869
3870    private $scriptAccess      = false; // nobody.
3871
3872	/**
3873	 * e_parser constructor.
3874	 */
3875	public function __construct()
3876    {
3877
3878		$this->init();
3879		$this->compileAttributeDefaults();
3880
3881         /*
3882        $meths = get_class_methods('DomDocument');
3883        sort($meths);
3884        print_a($meths);
3885        */
3886    }
3887
3888    /**
3889     * Merge default 'global' attributes into assigned tags.
3890     */
3891    private function compileAttributeDefaults()
3892    {
3893        foreach($this->allowedAttributes as $tag=>$array)
3894        {
3895            if($tag === 'default')
3896            {
3897                continue;
3898            }
3899
3900            foreach($this->allowedAttributes['default'] as $def)
3901            {
3902                $this->allowedAttributes[$tag][] = $def;
3903            }
3904
3905        }
3906
3907    }
3908
3909    /**
3910     * Used by e_parse to start
3911     */
3912    function init()
3913    {
3914        $this->domObj = new DOMDocument('1.0', 'utf-8');
3915
3916		if(defined('FONTAWESOME'))
3917		{
3918			$this->fontawesome = (int) FONTAWESOME;
3919		}
3920
3921		if(defined('BOOTSTRAP'))
3922		{
3923			$this->bootstrap = (int) BOOTSTRAP;
3924
3925		}
3926
3927    }
3928
3929	/**
3930	 * Add Allowed Tags.
3931	 * @param string
3932	 */
3933	public function addAllowedTag($tag)
3934	{
3935		$this->allowedTags[] = $tag;
3936	}
3937
3938
3939	/**
3940	 * @param $tag - html tag.
3941	 * @param $attArray - array of attributes. eg. array('style', 'id', 'class') etc.
3942	 */
3943	public function addAllowedAttribute($tag, $attArray)
3944	{
3945		$this->allowedAttributes[$tag] = (array) $attArray;
3946	}
3947
3948
3949	/**
3950     * Set Allowed Tags.
3951     * @param $array
3952     */
3953    public function setAllowedTags($array=array())
3954    {
3955        $this->allowedTags = $array;
3956    }
3957
3958	/**
3959	 * Set Script Access
3960	 * @param $val int e_UC_MEMBER, e_UC_NOBODY, e_UC_MAINADMIN or userclass number.
3961	 */
3962	public function setScriptAccess($val)
3963	{
3964		$this->scriptAccess = $val;
3965	}
3966
3967	public function getAllowedTags()
3968	{
3969		return $this->allowedTags;
3970	}
3971
3972    public function getAllowedAttributes()
3973	{
3974		return $this->allowedAttributes;
3975	}
3976
3977
3978	public function getScriptAccess()
3979	{
3980		return $this->scriptAccess;
3981	}
3982
3983	public function getRemoved()
3984    {
3985        return $this->removedList;
3986    }
3987
3988	/**
3989     * Set Allowed Attributes.
3990     * @param $array
3991     */
3992    public function setAllowedAttributes($array=array())
3993    {
3994        $this->allowedAttributes = $array;
3995    }
3996
3997     /**
3998     * Set Script Tags.
3999     * @param $array
4000     */
4001    public function setScriptTags($array=array())
4002    {
4003        $this->scriptTags = $array;
4004    }
4005
4006
4007	/**
4008	 * @param int $version
4009	 */
4010	public function setFontAwesome($version)
4011    {
4012        $this->fontawesome = (int) $version;
4013    }
4014
4015	/**
4016	 * @param int $version
4017	 */
4018	public function setBootstrap($version)
4019    {
4020        $this->bootstrap = (int) $version;
4021    }
4022
4023
4024	/**
4025	 * Add leading zeros to a number. eg. 3 might become 000003
4026	 * @param $num integer
4027	 * @param $numDigits - total number of digits
4028	 * @return string number with leading zeros.
4029	 */
4030	public function leadingZeros($num,$numDigits)
4031	{
4032		return (string) sprintf("%0".$numDigits."d",$num);
4033	}
4034
4035	/**
4036	 * Generic variable translator for LAN definitions.
4037	 * @param $lan - string LAN
4038	 * @param string | array $vals - either a single value, which will replace '[x]' or an array with key=>value pairs.
4039	 * @example $tp->lanVars("My name is [x] and I own a [y]", array('x'=>"John", 'y'=>"Cat"));
4040	 * @example $tp->lanVars("My name is [x] and I own a [y]", array("John","Cat"));
4041	 * @return string
4042	 */
4043	function lanVars($lan, $vals, $bold=false)
4044	{
4045
4046		$array = (!is_array($vals)) ? array('x'=>$vals) : $vals;
4047
4048		$search = array();
4049		$replace = array();
4050
4051		$defaults = array('x', 'y', 'z');
4052
4053		foreach($array as $k=>$v)
4054		{
4055			if(is_numeric($k)) // convert array of numeric to x,y,z
4056			{
4057				$k = $defaults[$k];
4058			}
4059
4060			$search[] = "[".$k."]";
4061			$replace[] = ($bold===true) ? "<strong>".$v."</strong>" : $v;
4062		}
4063
4064		return str_replace($search, $replace, $lan);
4065	}
4066
4067	/**
4068	 * Return an Array of all specific tags found in an HTML document and their attributes.
4069	 * @param $html - raw html code
4070	 * @param $taglist - comma separated list of tags to search or '*' for all.
4071	 * @param $header - if the $html includes the html head or body tags - it should be set to true.
4072	 */
4073	public function getTags($html, $taglist='*', $header = false)
4074	{
4075
4076		if($header == false)
4077		{
4078			$html = "<html><body>".$html."</body></html>";
4079		}
4080
4081		$doc = $this->domObj;
4082
4083		$doc->preserveWhiteSpace = true;
4084		libxml_use_internal_errors(true);
4085        $doc->loadHTML($html);
4086
4087		$tg = explode(",", $taglist);
4088		$ret = array();
4089
4090		foreach($tg as $find)
4091		{
4092	        $tmp = $doc->getElementsByTagName($find);
4093
4094			 /**
4095			  * @var  $k
4096			  * @var DOMDocument $node
4097			  */
4098			foreach($tmp as $k=>$node)
4099			{
4100				$tag = $node->nodeName;
4101				$inner = $node->C14N();
4102				 $inner = str_replace("&#xD;","",$inner);
4103
4104				foreach ($node->attributes as $attr)
4105	            {
4106					$name = $attr->nodeName;
4107	           		$value = $attr->nodeValue;
4108					$ret[$tag][$k][$name] = $value;
4109				}
4110
4111				$ret[$tag][$k]['@value'] = $inner;
4112
4113
4114			}
4115		}
4116
4117		if($header == false)
4118		{
4119			unset($ret['html'],$ret['body']);
4120		}
4121
4122
4123		return $ret;
4124	}
4125
4126
4127
4128	/**
4129	 * Parse xxxxx.glyph file to bootstrap glyph format.
4130	 * @param string $text
4131	 * @param array|string $options
4132	 * @param bool $options['size'] 2x, 3x, 4x, or 5x
4133	 * @param bool $options['fw'] Fixed-Width
4134	 * @param bool $options['spin'] Spin
4135	 * @param int $options['rotate'] Rotate in Degrees.
4136	 * @example $tp->toGlyph('fa-spinner', 'spin=1');
4137	 * @example $tp->toGlyph('fa-spinner', array('spin'=>1));
4138	 * @example $tp->toGlyph('fa-shield', array('rotate'=>90, 'size'=>'2x'));
4139	 */
4140	public function toGlyph($text, $options=" ")
4141	{
4142
4143		if(empty($text))
4144		{
4145			return false;
4146		}
4147
4148		if(is_array($options))
4149		{
4150			$parm = $options;
4151			$options = varset($parm['space'],'');
4152		}
4153		elseif(strpos($options,'='))
4154		{
4155			parse_str($options,$parm);
4156			$options = varset($parm['space'],'');
4157		}
4158		else
4159		{
4160			$parm = array();
4161		}
4162
4163		if(substr($text,0,2) === 'e-') 	// e107 admin icon.
4164		{
4165			$size = (substr($text,-3) === '-32') ? 'S32' : 'S16';
4166
4167			if(substr($text,-3) === '-24')
4168			{
4169				$size = 'S24';
4170			}
4171
4172			return "<i class='".$size." ".$text."'></i>";
4173		}
4174
4175		// Get Glyph names.
4176	//	$bs3 = e107::getMedia()->getGlyphs('bs3','');
4177	//	$fa4 = e107::getMedia()->getGlyphs('fa4','');
4178
4179
4180
4181		list($id) = explode('.glyph',$text,2);
4182	//	list($type, $tmp2) = explode("-",$text,2);
4183
4184	//	return $cls;
4185
4186	//	$removePrefix = array('glyphicon-','icon-','fa-');
4187
4188	//	$id = str_replace($removePrefix, "", $cls);
4189
4190
4191		$spin       = null;
4192		$rotate     = null;
4193		$fixedW     = null;
4194		$prefix     = 'glyphicon glyphicon-'; // fallback
4195		$size       = null;
4196		$tag        = 'i';
4197
4198	//	return print_r($fa4,true);
4199/*
4200		if(deftrue('FONTAWESOME') &&  in_array($id ,$fa4)) // Contains FontAwesome 3 set also.
4201		{
4202			$prefix = 'fa fa-';
4203			$size 	= (vartrue($parm['size'])) ?  ' fa-'.$parm['size'] : '';
4204			$tag 	= 'i';
4205			$spin   = !empty($parm['spin']) ? ' fa-spin' : '';
4206			$rotate = !empty($parm['rotate']) ? ' fa-rotate-'.intval($parm['rotate']) : '';
4207			$fixedW = !empty($parm['fw']) ? ' fa-fw' : "";
4208		}
4209		elseif(deftrue("BOOTSTRAP"))
4210		{
4211			if(BOOTSTRAP === 3 && in_array($id ,$bs3))
4212			{
4213				$prefix = 'glyphicon glyphicon-';
4214				$tag = 'span';
4215			}
4216			else
4217			{
4218		//		$prefix = 'icon-';
4219				$tag = 'i';
4220			}
4221
4222			$size = '';
4223
4224		}
4225		*/
4226		if(strpos($text, 'fa-') === 0) // Font-Awesome 4 & 5
4227		{
4228			$prefix = 'fa ';
4229			$size 	= (vartrue($parm['size'])) ?  ' fa-'.$parm['size'] : '';
4230			$tag 	= 'i';
4231			$spin   = !empty($parm['spin']) ? ' fa-spin' : '';
4232			$rotate = !empty($parm['rotate']) ? ' fa-rotate-'.intval($parm['rotate']) : '';
4233			$fixedW = !empty($parm['fw']) ? ' fa-fw' : "";
4234
4235			if($this->fontawesome === 5)
4236			{
4237				$fab = e107::getMedia()->getGlyphs('fab');
4238				$fas = e107::getMedia()->getGlyphs('fas');;
4239
4240				$code = substr($id,3);
4241
4242				if(in_array($code,$fab))
4243				{
4244					$prefix = "fab ";
4245				}
4246				elseif(in_array($code,$fas))
4247				{
4248					$prefix = "fas ";
4249				}
4250
4251			}
4252
4253		}
4254		elseif(strpos($text, 'glyphicon-') === 0) // Bootstrap 3
4255		{
4256			$prefix = 'glyphicon ';
4257			$tag = 'span';
4258
4259		}
4260		elseif(strpos($text, 'icon-') === 0) // Bootstrap 2
4261		{
4262			if($this->bootstrap !== 2) // bootrap 2 icon but running bootstrap3.
4263			{
4264				$prefix = 'glyphicon ';
4265				$tag = 'span';
4266				$id = str_replace("icon-", "glyphicon-", $id);
4267			}
4268			else
4269			{
4270				$prefix = '';
4271				$tag = 'i';
4272			}
4273
4274		}
4275		elseif($custom = e107::getThemeGlyphs()) // Custom Glyphs
4276		{
4277			foreach($custom as $glyphConfig)
4278			{
4279				if(strpos($text, $glyphConfig['prefix']) === 0)
4280				{
4281					$prefix = $glyphConfig['class'] . " ";
4282					$tag = $glyphConfig['tag'];
4283					continue;
4284				}
4285			}
4286
4287		}
4288
4289
4290		$idAtt = (!empty($parm['id'])) ? "id='".$parm['id']."' " : '';
4291		$style = (!empty($parm['style'])) ? "style='".$parm['style']."' " : '';
4292		$class = (!empty($parm['class'])) ? $parm['class']." " : '';
4293		$placeholder = isset($parm['placeholder']) ? $parm['placeholder'] : "<!-- -->";
4294		$title = (!empty($parm['title'])) ? " title='".$this->toAttribute($parm['title'])."' " : '';
4295
4296		$text = "<".$tag." {$idAtt}class='".$class.$prefix.$id.$size.$spin.$rotate.$fixedW."' ".$style.$title.">".$placeholder."</".$tag.">" ;
4297		$text .= ($options !== false) ? $options : "";
4298
4299		return $text;
4300
4301
4302	}
4303
4304
4305	/**
4306	 * Return a Bootstrap Badge tag
4307	 * @param $text
4308	 * @return string
4309	 */
4310	public function toBadge($text, $parm=null)
4311	{
4312		$class = !empty($parm['class']) ? " ".$parm['class'] : ' badge-secondary';
4313
4314		return "<span class='badge".$class."'>".$text."</span>";
4315	}
4316
4317
4318	/**
4319	 * Return a Bootstrap Label tag
4320	 * @param $text
4321	 * @return string
4322	 */
4323	public function toLabel($text, $type = null)
4324	{
4325		if($type === null)
4326		{
4327			$type = 'default';
4328		}
4329
4330		$tmp = explode(",",$text);
4331
4332		$opt = array();
4333		foreach($tmp as $v)
4334		{
4335			$opt[] = "<span class='label label-".$type."'>".$v."</span>";
4336		}
4337
4338		return implode(" ",$opt);
4339	}
4340
4341	/**
4342	 * Take a file-path and convert it to a download link.
4343	 * @param $text
4344	 * @return string
4345	 */
4346	public function toFile($text, $parm=array())
4347	{
4348		$srch = array(
4349			'{e_MEDIA_FILE}' => 'e_MEDIA_FILE/',
4350			'{e_PLUGIN}' => 'e_PLUGIN/'
4351		);
4352
4353		$link = e_HTTP."request.php?file=". str_replace(array_keys($srch), $srch,$text);
4354
4355		if(!empty($parm['raw']))
4356		{
4357			return $link;
4358		}
4359
4360		return "<a href='".$link."'>-attachment-</a>"; //TODO Add pref for this.
4361	}
4362
4363	/**
4364	 * Render an avatar based on supplied user data or current user when missing.
4365	 * @param array $userData  - user data from e107_user. ie. user_image, user_id etc.
4366	 * @param array $options
4367	 * @param int $options['w'] - image width in px
4368	 * @param int $options['h'] - image height in px
4369	 * @param int|bool $options['crop'] = enables cropping when true
4370	 * @param string $options['shape'] - (optional) rounded|circle|thumbnail
4371	 * @param string $options['id'] - 'id' attribute will be added to tag.
4372	 * @param string $options['class'] - override default 'class' attribute in tag.
4373	 * @param string $options['alt'] - override default 'alt' attribute in tag.
4374	 * @param bool $options['base64'] - use embedded base64 for image src.
4375	 * @param bool $options['hd'] - double the resolution of the image. Useful for retina displays.
4376	 * @param string $options['type'] - when set to 'url' returns the URL value instead of the tag.
4377	 * @param string $options['style'] - sets the style attribute.
4378     * @param string $options['mode'] - 'full' url mode.
4379	 * @return string <img> tag of avatar.
4380	 */
4381	public function toAvatar($userData=null, $options=array())
4382	{
4383		$tp 		= e107::getParser();
4384		$width 		= !empty($options['w']) ? intval($options['w']) : $tp->thumbWidth;
4385		$height 	= ($tp->thumbHeight !== 0) ? $tp->thumbHeight : "";
4386		$crop       = !empty($options['crop']) ? $options['crop'] : $tp->thumbCrop;
4387		$linkStart  = '';
4388		$linkEnd    =  '';
4389		$full       = !empty($options['base64']) ? true : false;
4390
4391		if(!empty($options['mode']) && $options['mode'] === 'full')
4392		{
4393			$full = true;
4394		}
4395
4396		if(!empty($options['h']))
4397		{
4398			$height = intval($options['h']);
4399		}
4400
4401		if(!empty($options['hd'])) // Fix resolution on Retina display.
4402		{
4403			$width = $width * 2;
4404			$height = $height * 2;
4405		}
4406
4407
4408		if($userData === null && USERID)
4409		{
4410			$userData = array();
4411			$userData['user_id']    = USERID;
4412			$userData['user_image']	= USERIMAGE;
4413			$userData['user_name']	= USERNAME;
4414			$userData['user_currentvisit'] = USERCURRENTVISIT;
4415		}
4416
4417
4418		$image = (!empty($userData['user_image'])) ? varset($userData['user_image']) : null;
4419
4420		$genericFile = e_IMAGE."generic/blank_avatar.jpg";
4421		$genericImg = $tp->thumbUrl($genericFile,"w=".$width."&h=".$height,true, $full);
4422
4423		if (!empty($image))
4424		{
4425
4426			if(strpos($image,"://")!==false) // Remote Image
4427			{
4428				$url = $image;
4429			}
4430			elseif(substr($image,0,8) == "-upload-")
4431			{
4432
4433				$image = substr($image,8); // strip the -upload- from the beginning.
4434				if(file_exists(e_AVATAR_UPLOAD.$image))
4435				{
4436					$file  = e_AVATAR_UPLOAD.$image;
4437					$url = $tp->thumbUrl($file,"w=".$width."&h=".$height."&crop=".$crop, false, $full);
4438				}
4439				else
4440				{
4441					$file = $genericFile;
4442					$url = $genericImg;
4443				}
4444			}
4445			elseif(file_exists(e_AVATAR_DEFAULT.$image))  // User-Uplaoded Image
4446			{
4447				$file = e_AVATAR_DEFAULT.$image;
4448				$url =	$tp->thumbUrl($file,"w=".$width."&h=".$height."&crop=".$crop, false, $full);
4449			}
4450			else // Image Missing.
4451			{
4452				$url = $genericImg;
4453				$file = $genericFile;
4454			}
4455		}
4456		else // No image provided - so send generic.
4457		{
4458			$url = $genericImg;
4459			$file = $genericFile;
4460		}
4461
4462		if(!empty($options['base64'])) // embed image data into URL.
4463		{
4464			$content = e107::getFile()->getRemoteContent($url); // returns false during unit tests, works otherwise.
4465			if(!empty($content))
4466			{
4467				$ext = strtolower(pathinfo($file,PATHINFO_EXTENSION));
4468				$url = 'data:image/'.$ext.';base64,'.base64_encode($content);
4469			}
4470		}
4471
4472		if(!empty($options['hd'])) // Fix resolution on Retina display.
4473		{
4474			$width = $width / 2;
4475			$height = ($height / 2);
4476		}
4477
4478		if(($url == $genericImg) && !empty($userData['user_id'] ) && (($userData['user_id'] == USERID)) && !empty($options['link']))
4479		{
4480			$linkStart = "<a class='e-tip' title=\"".LAN_EDIT."\" href='".e107::getUrl()->create('user/myprofile/edit')."'>";
4481			$linkEnd = "</a>";
4482		}
4483
4484		$title = (ADMIN) ? $image : $tp->toAttribute($userData['user_name']);
4485		$shape = (!empty($options['shape'])) ? "img-".$options['shape'] : "img-rounded rounded";
4486
4487
4488		if(!empty($options['type']) && $options['type'] === 'url')
4489		{
4490			return $url;
4491		}
4492
4493		if(!empty($options['alt']))
4494		{
4495			$title =  $tp->toAttribute($options['alt']);
4496		}
4497
4498		$heightInsert = empty($height) ? '' : "height='".$height."'";
4499		$id = (!empty($options['id'])) ? "id='".$options['id']."' " : "";
4500
4501		$classOnline = (!empty($userData['user_currentvisit']) && intval($userData['user_currentvisit']) > (time() - 300)) ? " user-avatar-online" : '';
4502
4503		$class = !empty($options['class']) ? $options['class'] : $shape." user-avatar";
4504		$style = !empty($options['style']) ? " style='".$options['style']."'" : '';
4505
4506		$text = $linkStart;
4507		$text .= "<img ".$id."class='".$class.$classOnline."' alt=\"".$title."\" src='".$url."'  width='".$width."' ".$heightInsert.$style." />";
4508		$text .= $linkEnd;
4509	//	return $url;
4510		return $text;
4511
4512	}
4513
4514
4515
4516	/**
4517	 * Display an icon.
4518	 * @param string $icon
4519	 * @example $tp->toIcon("{e_IMAGES}icons/something.png");
4520	 */
4521	public function toIcon($icon='',$parm = array())
4522	{
4523
4524		if(empty($icon))
4525		{
4526			return null;
4527		}
4528
4529		if(strpos($icon,'e_MEDIA_IMAGE')!==false)
4530		{
4531		//	return "<div class='alert alert-danger'>Use \$tp->toImage() instead of toIcon() for ".$icon."</div>"; // debug info only.
4532		}
4533
4534		if(substr($icon,0,3) == '<i ') // if it's html (ie. css sprite) return the code.
4535		{
4536			return $icon;
4537		}
4538
4539		$ext = pathinfo($icon, PATHINFO_EXTENSION);
4540		$dimensions = null;
4541
4542		if(!$ext || $ext == 'glyph') // Bootstrap or Font-Awesome.
4543		{
4544			return $this->toGlyph($icon,$parm);
4545		}
4546
4547		if(strpos($icon,'e_MEDIA_IMAGE')!==false)
4548		{
4549			$path = $this->thumbUrl($icon);
4550			$dimensions = $this->thumbDimensions();
4551		}
4552		elseif($icon[0] === '{')
4553		{
4554			$path = $this->replaceConstants($icon,'abs');
4555		}
4556		elseif(!empty($parm['legacy']))
4557		{
4558			$legacyList = (!is_array($parm['legacy'])) ? array($parm['legacy']) : $parm['legacy'];
4559
4560			foreach($legacyList as $legPath)
4561			{
4562				$legacyPath = $legPath.$icon;
4563				$filePath = $this->replaceConstants($legacyPath);
4564
4565				if(is_readable($filePath))
4566				{
4567					$path = $this->replaceConstants($legacyPath,'full');
4568					break;
4569				}
4570
4571			}
4572
4573			if(empty($path))
4574			{
4575				$log = e107::getAdminLog();
4576				$log->addDebug('Broken Icon Path: '.$icon."\n".print_r(debug_backtrace(null,2), true), false)->save('IMALAN_00');
4577				e107::getDebug()->log('Broken Icon Path: '.$icon);
4578				return null;
4579			}
4580
4581		}
4582		else
4583		{
4584			$path = $icon;
4585		}
4586
4587
4588		$alt = (!empty($parm['alt'])) ? $this->toAttribute($parm['alt']) : basename($path);
4589		$class = (!empty($parm['class'])) ? $parm['class'] : 'icon';
4590
4591		return "<img class='".$class."' src='".$path."' alt='".$alt."' ".$dimensions." />";
4592	}
4593
4594
4595	/**
4596	 * Render an img tag.
4597	 * @param string $file
4598	 * @param array $parm  keys: legacy|w|h|alt|class|id|crop|loading
4599	 * @param array $parm['legacy'] Usually a legacy path like {e_FILE}
4600	 * @return string
4601	 * @example $tp->toImage('welcome.png', array('legacy'=>{e_IMAGE}newspost_images/','w'=>200));
4602	 */
4603	public function toImage($file, $parm=array())
4604	{
4605
4606		if(strpos($file,'e_AVATAR')!==false)
4607		{
4608			return "<div class='alert alert-danger'>Use \$tp->toAvatar() instead of toImage() for ".$file."</div>"; // debug info only.
4609
4610		}
4611
4612		if(empty($file) && empty($parm['placeholder']))
4613		{
4614			return null;
4615		}
4616
4617		if(!empty($file))
4618		{
4619			$srcset     = null;
4620			$path       = null;
4621			$file       = trim($file);
4622			$ext        = pathinfo($file, PATHINFO_EXTENSION);
4623			$accepted   = array('jpg','gif','png','jpeg', 'svg');
4624
4625
4626			if(!in_array($ext,$accepted))
4627			{
4628				return null;
4629			}
4630		}
4631
4632		/** @var e_parse $tp */
4633		$tp  = $this;
4634
4635	//		e107::getDebug()->log($file);
4636	//	e107::getDebug()->log($parm);
4637
4638		if(strpos($file,'http')===0)
4639		{
4640			$path = $file;
4641		}
4642		elseif(strpos($file,'e_MEDIA')!==false || strpos($file,'e_THEME')!==false || strpos($file,'e_PLUGIN')!==false || strpos($file,'{e_IMAGE}')!==false) //v2.x path.
4643		{
4644
4645			if(!isset($parm['w']) && !isset($parm['h']))
4646			{
4647				$parm['w']      = $tp->thumbWidth();
4648				$parm['h']      = $tp->thumbHeight();
4649				$parm['crop']   = $tp->thumbCrop();
4650				$parm['x']      = $tp->thumbEncode();
4651			}
4652
4653			unset($parm['src']);
4654			$path = $tp->thumbUrl($file,$parm);
4655
4656
4657			if(empty($parm['w']) && empty($parm['h']))
4658			{
4659				$parm['srcset'] = false;
4660			}
4661			elseif(!isset($parm['srcset']))
4662			{
4663				$srcSetParm = $parm;
4664				$srcSetParm['size'] = ($parm['w'] < 100) ? '4x' : '2x';
4665				$parm['srcset'] = $tp->thumbSrcSet($file, $srcSetParm);
4666			}
4667
4668		}
4669		elseif($file[0] === '{') // Legacy v1.x path. Example: {e_PLUGIN}myplugin/images/fixedimage.png
4670		{
4671			$path = $tp->replaceConstants($file,'abs');
4672		}
4673		elseif(!empty($parm['legacy'])) // Search legacy path for image in a specific folder. No path, only file name provided.
4674		{
4675
4676			$legacyPath = rtrim($parm['legacy'],'/').'/'.$file;
4677			$filePath = $tp->replaceConstants($legacyPath);
4678
4679			if(is_readable($filePath))
4680			{
4681				$path = $tp->replaceConstants($legacyPath,'abs');
4682			}
4683			else
4684			{
4685				$log = e107::getAdminLog();
4686				$log->addDebug('Broken Image Path: '.$legacyPath."\n".print_r(debug_backtrace(null,2), true), false)->save('IMALAN_00');
4687				e107::getDebug()->log("Broken Image Path: ".$legacyPath);
4688			}
4689
4690		}
4691		else // usually http://....
4692		{
4693			$path = $file;
4694		}
4695
4696		if(empty($path) && !empty($parm['placeholder']))
4697		{
4698			$path = $tp->thumbUrl($file,$parm);
4699		}
4700
4701		$id     = (!empty($parm['id']))     ? "id=\"".$parm['id']."\" " :  ""  ;
4702		$class  = (!empty($parm['class']))  ? $parm['class'] : "img-responsive img-fluid";
4703		$alt    = (!empty($parm['alt']))    ? $tp->toAttribute($parm['alt']) : basename($file);
4704		$style  = (!empty($parm['style']))  ? "style=\"".$parm['style']."\" " :  ""  ;
4705		$srcset = (!empty($parm['srcset'])) ? "srcset=\"".$parm['srcset']."\" " : "";
4706		$width  = (!empty($parm['w']))      ? "width=\"".intval($parm['w'])."\" " : "";
4707		$height = (!empty($parm['h']))      ? "height=\"".intval($parm['h'])."\" " : "";
4708		$loading = !empty($parm['loading']) ? "loading=\"".$parm['loading']."\" " : ""; // eg. lazy, eager, auto
4709
4710		return "<img {$id}class='{$class}' src='".$path."' alt=\"".$alt."\" ".$srcset.$width.$height.$style.$loading." />";
4711
4712	}
4713
4714
4715	/**
4716	 * Check if a string contains bbcode.
4717	 * @param $text
4718	 * @return bool
4719	 */
4720	function isBBcode($text)
4721	{
4722		if(preg_match('#(?<=<)\w+(?=[^<]*?>)#', $text))
4723		{
4724			return false;
4725		}
4726
4727		$bbsearch = array('[/img]','[/h]', '[/b]', '[/link]', '[/right]', '[/center]', '[/flash]', '[/code]', '[/table]');
4728
4729		foreach($bbsearch as $v)
4730		{
4731			if(strpos($text,$v)!==false)
4732			{
4733				return true;
4734			}
4735
4736		}
4737
4738		return false;
4739
4740
4741	}
4742
4743
4744	/**
4745	 * Check if a string is HTML
4746	 * @param $text
4747	 * @return bool
4748	 */
4749	function isHtml($text)
4750	{
4751
4752		if(strpos($text,'[html]') !==false)
4753		{
4754			return true;
4755		}
4756
4757		if($this->isBBcode($text))
4758		{
4759			return false;
4760		}
4761
4762		if(preg_match('#(?<=<)\w+(?=[^<]*?>)#', $text))
4763		{
4764			return true;
4765		}
4766
4767		return false;
4768
4769
4770	}
4771
4772
4773	/**
4774	 * Check if string is json and parse or return false.
4775	 * @param $text
4776	 * @return bool|mixed return false if not json, and json values if true.
4777	 */
4778	public function isJSON($text)
4779	{
4780		if(!is_string($text))
4781		{
4782			return false;
4783		}
4784
4785		 if(substr($text,0,1) === '{' || substr($text,0,1) === '[') // json
4786	    {
4787	        $dat = json_decode($text, true);
4788
4789	        if(json_last_error() !=  JSON_ERROR_NONE)
4790	        {
4791		        //   e107::getDebug()->log("Json data found");
4792	           return false;
4793	        }
4794
4795	        return $dat;
4796	    }
4797
4798		return false;
4799
4800	}
4801
4802
4803
4804	/**
4805	 * Checks if string is valid UTF-8.
4806	 *
4807	 * Try to detect UTF-8 using mb_detect_encoding(). If mb string extension is
4808	 * not installed, we try to use a simple UTF-8-ness checker using a regular
4809	 * expression originally created by the W3C. But W3C's function scans the
4810	 * entire strings and checks that it conforms to UTF-8.
4811	 *
4812	 * @see http://w3.org/International/questions/qa-forms-utf-8.html
4813	 *
4814	 * So this function is faster and less specific. It only looks for non-ascii
4815	 * multibyte sequences in the UTF-8 range and also to stop once it finds at
4816	 * least one multibytes string. This is quite a lot faster.
4817	 *
4818	 * @param $string string  string being checked.
4819	 * @return bool  Returns true if $string is valid UTF-8 and false otherwise.
4820	 */
4821	public function isUTF8($string)
4822	{
4823		if (function_exists('mb_check_encoding'))
4824		{
4825			return (mb_check_encoding($string, 'UTF-8'));
4826		}
4827
4828		return (bool) preg_match('%(?:
4829        [\xC2-\xDF][\x80-\xBF]        # non-overlong 2-byte
4830        |\xE0[\xA0-\xBF][\x80-\xBF]               # excluding overlongs
4831        |[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}      # straight 3-byte
4832        |\xED[\x80-\x9F][\x80-\xBF]               # excluding surrogates
4833        |\xF0[\x90-\xBF][\x80-\xBF]{2}    # planes 1-3
4834        |[\xF1-\xF3][\x80-\xBF]{3}                  # planes 4-15
4835        |\xF4[\x80-\x8F][\x80-\xBF]{2}    # plane 16
4836        )+%xs', $string);
4837
4838	}
4839
4840
4841
4842
4843
4844
4845	/**
4846	 * Check if a file is an video or not.
4847	 * @param $file string
4848	 * @return boolean
4849	 */
4850	function isVideo($file)
4851	{
4852		$ext = pathinfo($file,PATHINFO_EXTENSION);
4853
4854		return ($ext === 'youtube' || $ext === 'youtubepl') ? true : false;
4855
4856	}
4857
4858	/**
4859	 * Check if a file is an image or not.
4860	 * @param $file string
4861	 * @return boolean
4862	 */
4863	function isImage($file)
4864	{
4865		if(substr($file,0,3)=="{e_")
4866		{
4867			$file = e107::getParser()->replaceConstants($file);
4868		}
4869
4870
4871		$ext = pathinfo($file,PATHINFO_EXTENSION);
4872
4873		return ($ext === 'jpg' || $ext === 'png' || $ext === 'gif' || $ext === 'jpeg') ? true : false;
4874	}
4875
4876
4877	/**
4878	 * @param $file
4879	 * @param array $parm
4880	 * @return string
4881	 */
4882	public function toAudio($file, $parm=array())
4883	{
4884
4885		$file = $this->replaceConstants($file, 'abs');
4886
4887		$mime = varset($parm['mime'], 'audio/mpeg');
4888
4889		$autoplay = !empty($parm['autoplay']) ? "autoplay " : "";
4890		$controls = !empty($parm['controls']) ? "controls" : "";
4891
4892		$text = '<audio controls style="max-width:100%" '.$autoplay.$controls.'>
4893					<source src="'.$file.'" type="'.$mime .'">
4894					  Your browser does not support the audio tag.
4895				</audio>';
4896
4897		return $text;
4898
4899	}
4900
4901
4902
4903	/**
4904	 * Display a Video file.
4905	 * @param string $file - format: id.type eg. x123dkax.youtube
4906	 * @param boolean $thumbnail  - set to 'tag' to return an image thumbnail and 'src' to return the src url or 'video' for a small video thumbnail.
4907	 */
4908	function toVideo($file, $parm=array())
4909	{
4910		if(empty($file))
4911		{
4912			return false;
4913		}
4914
4915		$type = pathinfo($file, PATHINFO_EXTENSION);
4916
4917		$id = str_replace(".".$type, "", $file);
4918
4919		$thumb = vartrue($parm['thumb']);
4920		$mode = varset($parm['mode'],false); // tag, url
4921
4922
4923
4924		$pref = e107::getPref();
4925		$ytpref = array();
4926		foreach($pref as $k=>$v) // Find all Youtube Prefs.
4927		{
4928			if(substr($k,0,8) === 'youtube_')
4929			{
4930				$key = substr($k,8);
4931				$ytpref[$key] = $v;
4932			}
4933		}
4934
4935		unset($ytpref['bbcode_responsive']); // do not include in embed code.
4936
4937		if(!empty($ytpref['cc_load_policy']))
4938		{
4939			$ytpref['cc_lang_pref'] = e_LAN; // switch captions with chosen user language.
4940		}
4941
4942		$ytqry = http_build_query($ytpref, null, '&amp;');
4943
4944		$defClass = !empty($this->bootstrap) ? "embed-responsive embed-responsive-16by9" : "video-responsive"; // levacy backup.
4945
4946
4947		if($type === 'youtube')
4948		{
4949
4950		//	$thumbSrc = "https://i1.ytimg.com/vi/".$id."/0.jpg";
4951			$thumbSrc = "https://i1.ytimg.com/vi/".$id."/mqdefault.jpg";
4952			$video =  '<iframe class="embed-responsive-item" width="560" height="315" src="//www.youtube.com/embed/'.$id.'?'.$ytqry.'" style="background-size: 100%;background-image: url('.$thumbSrc.');border:0px" allowfullscreen></iframe>';
4953			$url 	= 'http://youtu.be/'.$id;
4954
4955
4956			if($mode === 'url')
4957			{
4958				return $url;
4959			}
4960
4961
4962			if($thumb === 'tag')
4963			{
4964				return "<img class='img-responsive img-fluid' src='".$thumbSrc."' alt='Youtube Video' style='width:".vartrue($parm['w'],'80')."px'/>";
4965			}
4966
4967			if($thumb === 'email')
4968			{
4969				$thumbSrc = "http://i1.ytimg.com/vi/".$id."/maxresdefault.jpg"; // 640 x 480
4970				$filename = 'temp/yt-thumb-'.md5($id).".jpg";
4971				$filepath = e_MEDIA.$filename;
4972
4973
4974				if(!file_exists($filepath))
4975				{
4976					e107::getFile()->getRemoteFile($thumbSrc, $filename,'media');
4977				}
4978
4979				return "<a href='".$url."'><img class='video-responsive video-thumbnail' src='{e_MEDIA}".$filename."' alt='".LAN_YOUTUBE_VIDEO."' title='".LAN_CLICK_TO_VIEW."' />
4980				<div class='video-thumbnail-caption'><small>".LAN_CLICK_TO_VIEW."</small></div></a>";
4981			}
4982
4983			if($thumb === 'src')
4984			{
4985				return $thumbSrc;
4986			}
4987
4988
4989
4990			if($thumb === 'video')
4991			{
4992				return '<div class="'.$defClass.' video-thumbnail thumbnail">'.$video.'</div>';
4993			}
4994
4995			return '<div class="'.$defClass.' '.vartrue($parm['class']).'">'.$video.'</div>';
4996		}
4997
4998
4999		if($type === 'youtubepl')
5000		{
5001
5002			if($thumb === 'tag')
5003			{
5004				$thumbSrc =  e107::getMedia()->getThumb($id);
5005
5006				if(empty($thumbSrc))
5007				{
5008					$thumbSrc = e_IMAGE_ABS."generic/playlist_120.png";
5009				}
5010				return "<img class='img-responsive img-fluid' src='".$thumbSrc."' alt='".LAN_YOUTUBE_PLAYLIST."' style='width:".vartrue($parm['w'],'80')."px'/>";
5011
5012			}
5013
5014			if($thumb === 'src')
5015			{
5016				$thumb = e107::getMedia()->getThumb($id);
5017				if(!empty($thumb))
5018				{
5019					return $thumb;
5020				}
5021				else
5022				{
5023					// return "https://cdn0.iconfinder.com/data/icons/internet-2-2/64/youtube_playlist_videos_vid_web_online_internet-256.png";
5024					return e_IMAGE_ABS."generic/playlist_120.png";
5025				}
5026			}
5027
5028			$video = '<iframe width="560" height="315" src="https://www.youtube.com/embed/videoseries?list='.$id.'" style="border:0" allowfullscreen></iframe>';
5029			return '<div class="'.$defClass.' '.vartrue($parm['class']).'">'.$video.'</div>';
5030		}
5031
5032		if($type === 'mp4')
5033		{
5034			$file = $this->replaceConstants($file, 'abs');
5035
5036			if($mode === 'url')
5037			{
5038				return $file;
5039			}
5040
5041
5042			$width = varset($parm['w'], 320);
5043			$height = varset($parm['h'], 240);
5044			$mime = varset($parm['mime'], 'video/mp4');
5045
5046			return '
5047			<div class="video-responsive">
5048			<video width="'.$width.'" height="'.$height.'" controls>
5049			  <source src="'.$file.'" type="'.$mime.'">
5050
5051			  Your browser does not support the video tag.
5052			</video>
5053			</div>';
5054		}
5055
5056
5057
5058		return false;
5059	}
5060
5061
5062
5063	/**
5064	 * Display a Date in the browser.
5065	 * Includes support for 'livestamp' (http://mattbradley.github.io/livestampjs/)
5066	 * @param integer $datestamp - unix timestamp
5067	 * @param string $format - short | long | relative
5068	 * @return string converted date (html)
5069	 */
5070	public function toDate($datestamp = null, $format='short')
5071	{
5072		if(!is_numeric($datestamp)){ return null; }
5073
5074		$value = e107::getDate()->convert_date($datestamp, $format);
5075
5076		$inc = ($format === 'relative') ? ' data-livestamp="'.$datestamp.'"' : '';
5077
5078		return '<span'.$inc.'>'.$value.'</span>';
5079	}
5080
5081
5082
5083
5084
5085
5086	/**
5087	 * Parse new <x-bbcode> tags into bbcode output.
5088	 * @param bool $retainTags : when you want to replace html and retain the <bbcode> tags wrapping it.
5089	 * @return string html
5090	 */
5091	function parseBBTags($text,$retainTags = false)
5092	{
5093		$stext = str_replace("&quot;", '"', $text);
5094
5095		$bbcodes = $this->getTags($stext, 'x-bbcode');
5096
5097		foreach($bbcodes as $v)
5098		{
5099			foreach($v as $val)
5100			{
5101				$tag = base64_decode($val['alt']);
5102				$repl = ($retainTags == true) ? '$1'.$tag.'$2' : $tag;
5103			//	$text = preg_replace('/(<x-bbcode[^>]*>).*(<\/x-bbcode>)/i',$repl, $text);
5104				$text = preg_replace('/(<x-bbcode alt=(?:&quot;|")'.$val['alt'].'(?:&quot;|")>).*(<\/x-bbcode>)/i',$repl, $text);
5105
5106			}
5107		}
5108
5109		return $text;
5110	}
5111
5112
5113
5114    /**
5115     * Perform and render XSS Test Comparison
5116     */
5117    public function test($text='',$advanced = false)
5118    {
5119      //  $tp = e107::getParser();
5120        $sql = e107::getDb();
5121        $tp = e107::getParser();
5122
5123	    if(empty($text))
5124	    {
5125		    $text = <<<TMPL
5126[html]<p><strong>bold print</strong></p>
5127<pre class="prettyprint linenums">&lt;a href='#'&gt;Something&lt;/a&gt;</pre>
5128<p>Some text's and things.</p>
5129<p>&nbsp;</p>
5130<p><a href="/test.php?w=9&amp;h=12">link</a></p>
5131<p>日本語 简体中文</p>
5132<p>&nbsp;</p>
5133[/html]
5134TMPL;
5135	    }
5136
5137	    //   $text .= '[code=inline]<b class="something">Something</b>[/code]日本語 ';
5138
5139        // -------------------- Encoding ----------------
5140
5141		$acc = $this->getScriptAccess();
5142		$accName = e107::getUserClass()->uc_get_classname($acc);
5143
5144		echo "<h2>e107 Parser Test <small>with script access by <span class='label label-warning'>".$accName."</span></small></h2>";
5145		echo"<h3>User-input <small>(eg. from \$_POST)</small></h3>";
5146
5147	    print_a($text);
5148
5149	    $dbText = $tp->toDB($text,true);
5150
5151		echo "<h3>User-input &gg; toDB() ";
5152
5153		if($this->isHtml == true)
5154		{
5155			echo "<small>detected as <span class='label label-warning'>HTML</span></small>";
5156		}
5157		else
5158		{
5159			echo "<small>detected as <span class='label label-info'>Plain text</span></small>";
5160		}
5161
5162		echo "</h3>";
5163
5164	    print_a($dbText);
5165
5166
5167	    if(!empty($advanced))
5168	    {
5169			echo "<div class='alert alert-warning'>";
5170		    $dbText2 = $tp->toDB($text, true, false, 'no_html');
5171		    echo "<h3>User-input &gg; toDb(\$text, true, false, 'no_html')</h3>";
5172		    print_a($dbText2);
5173
5174		    echo "<div class='alert alert-warning'>";
5175		    $dbText3 = $tp->toDB($text, false, false, 'pReFs');
5176		    echo "<h3>User-input &gg; toDb(\$text, false, false, 'pReFs')</h3>";
5177		    print_a($dbText3);
5178
5179		   // toClean
5180		    $filter3 = $tp->filter($text, 'wds');
5181		    echo "<h3>User-input &gg; filter(\$text, 'wds')</h3>";
5182		    print_a( $filter3);
5183
5184		    // Filter by String.
5185		    $filter1 = $tp->filter($text,'str');
5186		    echo "<h3>User-input &gg; filter(\$text, 'str')</h3>";
5187		    print_a($filter1);
5188
5189		    // Filter by Encoded.
5190		    $filter2 = $tp->filter($text,'enc');
5191		    echo "<h3>User-input &gg; filter(\$text, 'enc')</h3>";
5192		    print_a($filter2);
5193
5194
5195		    // toAttribute
5196		    $toAtt = $tp->toAttribute($text);
5197		    echo "<h3>User-input &gg; toAttribute(\$text)</h3>";
5198		    print_a($toAtt);
5199
5200		    // toEmail
5201		    $toEmail = $tp->toEmail($dbText);
5202		    echo "<h3>User-input &gg; toEmail(\$text) <small>from DB</small></h3>";
5203		    print_a($toEmail);
5204
5205		    // toEmail
5206		    $toRss = $tp->toRss($text);
5207		    echo "<h3>User-input &gg; toRss(\$text)</h3>";
5208		    print_a($toRss);
5209
5210		    echo "</div>";
5211
5212
5213
5214	    }
5215
5216	    echo "<h3>toDB() &gg; toHTML()</h3>";
5217		$html = $tp->toHTML($dbText,true);
5218	    print_a($html);
5219
5220	    echo "<h3>toDB &gg; toHTML() <small>(rendered)</small></h3>";
5221	    echo $html;
5222
5223	    echo "<h3>toDB &gg; toForm()</h3>";
5224		$toForm = $tp->toForm($dbText);
5225	    $toFormRender = e107::getForm()->open('test');
5226	    $toFormRender .= "<textarea cols='100' style='width:100%;height:300px' >".$toForm."</textarea>";
5227	    $toFormRender .= e107::getForm()->close();
5228
5229		echo  $toFormRender;
5230
5231
5232		 echo "<h3>toDB &gg; bbarea</h3>";
5233	    echo e107::getForm()->bbarea('name',$toForm);
5234
5235		if(!empty($advanced))
5236		{
5237
5238			echo "<h3>Allowed Tags</h3>";
5239			print_a($this->allowedTags);
5240
5241
5242		    echo "<h3>Converted Paths</h3>";
5243		    print_a($this->pathList);
5244
5245		    echo "<h3>Removed Tags and Attributes</h3>";
5246		    print_a($this->removedList);
5247
5248		    echo "<h3>Nodes to Convert</h3>";
5249			print_a($this->nodesToConvert);
5250
5251			  echo "<h3>Nodes to Disable SC</h3>";
5252			print_a($this->nodesToDisableSC);
5253		}
5254
5255	    similar_text($text, html_entity_decode( $toForm, ENT_COMPAT, 'UTF-8'),$perc);
5256	    $scoreStyle = ($perc > 98) ? 'label-success' : 'label-danger';
5257	    echo "<h3><span class='label ".$scoreStyle."'>Similarity:  ".number_format($perc)."%</span></h3>";
5258
5259		echo "<table class='table table-bordered'>
5260
5261
5262		<tr>
5263			<th style='width:50%'>User-input</th>
5264			<th style='width:50%'>toForm() output</th>
5265		</tr>
5266		<tr>
5267			<td>".print_a($text,true)."</td>
5268			<td>". $toFormRender."</td>
5269		</tr>
5270
5271		</table>";
5272	  /*  <tr>
5273			<td>".print_a(json_encode($text),true)."</td>
5274			<td>". print_a(json_encode(html_entity_decode( $toForm, ENT_COMPAT, 'UTF-8')),true)."</td>
5275		</tr>*/
5276
5277	//    print_a($text);
5278
5279return;
5280
5281//return;
5282        // ---------------------------------
5283
5284
5285		$html = $text;
5286
5287		$sql = e107::getDb();
5288		$tp = e107::getParser();
5289		$dbg = e107::getDebug();
5290
5291      //  $html = $this->getXss();
5292
5293        echo "<h2>Unprocessed XSS</h2>";
5294        // echo $html; // Remove Comment for a real mess!
5295        print_a($html);
5296
5297        echo "<h2>Standard v2 Parser</h2>";
5298        echo "<h3>\$tp->dataFilter()</h3>";
5299        // echo $tp->dataFilter($html); // Remove Comment for a real mess!
5300       $dbg->logTime('------ Start Parser Test -------');
5301        print_a($tp->dataFilter($html));
5302       $dbg->logTime('tp->dataFilter');
5303
5304        echo "<h3>\$tp->toHTML()</h3>";
5305        // echo $tp->dataFilter($html); // Remove Comment for a real mess!
5306        print_a($tp->toHTML($html));
5307       $dbg->logTime('tp->toHtml');
5308
5309        echo "<h3>\$tp->toDB()</h3>";
5310        // echo $tp->dataFilter($html); // Remove Comment for a real mess!
5311        $todb = $tp->toDB($html);
5312        print_a( $todb);
5313       $dbg->logTime('tp->toDB');
5314
5315	    echo "<h3>\$tp->toForm() with toDB input.</h3>";
5316       print_a( $tp->toForm($todb));
5317
5318        echo "<h2>New Parser</h2>";
5319        echo "<h3>Processed</h3>";
5320        $cleaned = $this->cleanHtml($html, true);  // false = don't check html pref.
5321        print_a($cleaned);
5322       $dbg->logTime('new Parser');
5323      // $dbg->logTime('------ End Parser Test -------');
5324        echo "<h3>Processed &amp; Rendered</h3>";
5325        echo $cleaned;
5326
5327        echo "<h2>New Parser - Data</h2>";
5328        echo "<h3>Converted Paths</h3>";
5329        print_a($this->pathList);
5330
5331        echo "<h3>Removed Tags and Attributes</h3>";
5332        print_a($this->removedList);
5333
5334         //   print_a($p);
5335    }
5336
5337
5338
5339	/**
5340	 * Filters/Validates using the PHP5 filter_var() method.
5341	 * @param $text
5342	 * @param $type string str|int|email|url|w|wds|file
5343	 * @return string | boolean | array
5344	 */
5345	function filter($text, $type='str',$validate=false)
5346	{
5347		if(empty($text))
5348		{
5349			return $text;
5350		}
5351
5352		if($type === 'w') // words only.
5353		{
5354			return preg_replace('/[^\w]/',"",$text);
5355		}
5356
5357		if($type === 'd') // digits only.
5358		{
5359			return preg_replace('/[^\d]/',"",$text);
5360		}
5361
5362		if($type === 'wd') // words and digits only.
5363		{
5364			return preg_replace('/[^\w\d]/',"",$text);
5365		}
5366
5367		if($type === 'wds') // words, digits and spaces only.
5368		{
5369			return preg_replace('/[^\w\d ]/',"",$text);
5370		}
5371
5372		if($type === 'file')
5373		{
5374			return preg_replace('/[^\w\d_\.-]/',"-",$text);
5375		}
5376
5377		if($type === 'version')
5378		{
5379			return preg_replace('/[^\d_\.]/',"",$text);
5380		}
5381
5382		if($validate == false)
5383		{
5384			$filterTypes = array(
5385				'int'   => FILTER_SANITIZE_NUMBER_INT,
5386				'str'   => FILTER_SANITIZE_STRING, // no html.
5387				'email' => FILTER_SANITIZE_EMAIL,
5388				'url'   => FILTER_SANITIZE_URL,
5389				'enc'   => FILTER_SANITIZE_ENCODED
5390			);
5391		}
5392		else
5393		{
5394			$filterTypes = array(
5395				'int'   => FILTER_VALIDATE_INT,
5396				'email' => FILTER_VALIDATE_EMAIL,
5397				'ip'    => FILTER_VALIDATE_IP,
5398				'url'   => FILTER_VALIDATE_URL,
5399
5400			);
5401		}
5402
5403		if(is_array($text))
5404		{
5405			return filter_var_array($text, $filterTypes[$type]);
5406		}
5407
5408
5409		return filter_var($text, $filterTypes[$type]);
5410
5411	}
5412
5413
5414    private function grantScriptAccess()
5415    {
5416         $this->allowedTags = array_merge($this->allowedTags, $this->scriptTags);
5417
5418        foreach($this->allowedAttributes as $tag => $att)
5419        {
5420            foreach($this->scriptAttributes as $new)
5421            {
5422                $this->allowedAttributes[$tag][] = $new;
5423            }
5424        }
5425
5426
5427        return null;
5428    }
5429
5430
5431
5432    /**
5433     * Process and clean HTML from user input.
5434     * TODO Html5 tag support.
5435     * @param string $html raw HTML
5436     * @param boolean $checkPref
5437     * @return string
5438     */
5439    public function cleanHtml($html='', $checkPref = true)
5440    {
5441        if(empty($html)){ return ''; }
5442
5443        if($this->isHtml($html) === false)
5444        {
5445            	$html = str_replace('<','&lt;',$html);
5446				$html = str_replace('>','&gt;',$html);
5447        }
5448
5449		$html = str_replace('&nbsp;', '__E_PARSER_CLEAN_HTML_NON_BREAKING_SPACE__', $html); // prevent replacement of &nbsp; with spaces.
5450		// Workaround for https://bugs.php.net/bug.php?id=76285
5451		//  Part 1 of 2
5452        $html = str_replace("\r", "", $html); // clean out windows line-breaks.
5453		$html = str_replace("\n", "__E_PARSER_CLEAN_HTML_LINE_BREAK__", $html);
5454        $html = str_replace("{", "__E_PARSER_CLEAN_HTML_CURLY_OPEN__", $html);
5455        $html = str_replace("}", "__E_PARSER_CLEAN_HTML_CURLY_CLOSED__", $html);
5456
5457
5458        if(strpos($html, "<body")===false) // HTML Fragment
5459		{
5460       		$html = '<body>'.$html.'</body>';
5461		}
5462		else  // Full HTML page.
5463		{
5464		//	$this->allowedTags[] = 'head';
5465		//	$this->allowedTags[] = 'body';
5466		//	$this->allowedTags[] = 'title';
5467			//$this->allowedTags[] = 'meta';
5468		}
5469
5470		if(!is_object($this->domObj))
5471		{
5472			$this->init();
5473		}
5474
5475
5476		if($this->scriptAccess === false)
5477		{
5478	        $this->scriptAccess = e107::getConfig()->get('post_script', e_UC_NOBODY); // Pref to Allow <script> tags11;
5479		}
5480
5481		if(check_class($this->scriptAccess))
5482        {
5483            $this->grantScriptAccess();
5484        }
5485
5486
5487        // Set it up for processing.
5488        $doc  = $this->domObj;
5489	    libxml_use_internal_errors(true);
5490	    if(function_exists('mb_convert_encoding'))
5491	    {
5492			$html = mb_convert_encoding($html, 'HTML-ENTITIES', "UTF-8");
5493	    }
5494
5495		@$doc->loadHTML($html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
5496
5497       	$this->nodesToConvert 	= array(); // required.
5498		$this->nodesToDelete 	= array(); // required.
5499		$this->removedList		= array();
5500
5501		$tmp = $doc->getElementsByTagName('*');
5502
5503        /** @var DOMElement $node */
5504        foreach($tmp as $node)
5505        {
5506            $path = $node->getNodePath();
5507
5508		//	echo "<br />Path = ".$path;
5509        //   $tag = strval(basename($path));
5510
5511
5512	        if(strpos($path,'/code') !== false || strpos($path,'/pre') !== false) //  treat as html.
5513            {
5514                $this->pathList[] = $path;
5515            //     $this->nodesToConvert[] =  $node->parentNode; // $node;
5516                $this->nodesToDisableSC[] = $node;
5517                continue;
5518            }
5519
5520
5521            $tag = preg_replace('/([a-z0-9\[\]\/]*)?\/([\w\-]*)(\[(\d)*\])?$/i', "$2", $path);
5522            if(!in_array($tag, $this->allowedTags))
5523            {
5524
5525                $this->removedList['tags'][] = $tag;
5526                $this->nodesToDelete[] = $node;
5527                continue;
5528            }
5529
5530            foreach ($node->attributes as $attr)
5531            {
5532                $name = $attr->nodeName;
5533                $value = $attr->nodeValue;
5534
5535                $allow = isset($this->allowedAttributes[$tag]) ? $this->allowedAttributes[$tag] : $this->allowedAttributes['default'];
5536
5537                $removeAttributes = array();
5538
5539                if(!in_array($name, $allow))
5540                {
5541
5542                    if(strpos($name,'data-') === 0 && $this->scriptAccess == true)
5543                    {
5544                        continue;
5545                    }
5546
5547                    $removeAttributes[] = $name;
5548                    //$node->removeAttribute($name);
5549                    $this->removedList['attributes'][] = $name. " from <".$tag.">";
5550                    continue;
5551                }
5552
5553                if($this->invalidAttributeValue($value)) // Check value against blacklisted values.
5554                {
5555					//$node->removeAttribute($name);
5556                    $node->setAttribute($name, '#---sanitized---#');
5557					$this->removedList['sanitized'][] = $tag.'['.$name.']';
5558                }
5559                else
5560                {
5561                    $_value = $this->secureAttributeValue($name, $value);
5562
5563                    $node->setAttribute($name, $_value);
5564                    if($_value !== $value)
5565                    {
5566                        $this->removedList['sanitized'][] = $tag.'['.$name.'] converted "'.$value.'" -> "'.$_value.'"';
5567                    }
5568                }
5569            }
5570
5571            // required - removing attributes in a loop breaks the loop
5572            if(!empty($removeAttributes))
5573            {
5574	            foreach ($removeAttributes as $name)
5575	            {
5576	                $node->removeAttribute($name);
5577	            }
5578            }
5579
5580
5581        }
5582
5583        // Remove some stuff.
5584        foreach($this->nodesToDelete as $node)
5585        {
5586            $node->parentNode->removeChild($node);
5587        }
5588
5589		// Disable Shortcodes in pre/code
5590
5591       foreach($this->nodesToDisableSC as $key => $node)
5592       {
5593		    $value = $node->C14N();
5594
5595		    if(empty($value))
5596		    {
5597		        continue;
5598		    }
5599
5600		    $value = str_replace("&#xD;", "\r", $value);
5601
5602		    if($node->nodeName === 'pre')
5603		    {
5604		        $value = preg_replace('/^<pre[^>]*>/', '', $value);
5605		        $value = str_replace("</pre>", "", $value);
5606		        $value = str_replace('<br></br>', "__E_PARSER_CLEAN_HTML_LINE_BREAK__", $value);
5607		    }
5608            elseif($node->nodeName === 'code')
5609		    {
5610		        $value = preg_replace('/^<code[^>]*>/', '', $value);
5611		        $value = str_replace("</code>", "", $value);
5612		        $value = str_replace("<br></br>", "__E_PARSER_CLEAN_HTML_LINE_BREAK__", $value);
5613		    }
5614
5615		   $value = str_replace('__E_PARSER_CLEAN_HTML_CURLY_OPEN__', '{{{', $value); // temporarily change {e_XXX} to {{{e_XXX}}}
5616		   $value = str_replace('__E_PARSER_CLEAN_HTML_CURLY_CLOSED__', '}}}', $value); // temporarily change {e_XXX} to {{{e_XXX}}}
5617
5618
5619		    $newNode = $doc->createElement($node->nodeName);
5620		    $newNode->nodeValue = $value;
5621
5622		    if($class = $node->getAttribute('class'))
5623		    {
5624		        $newNode->setAttribute('class',$class);
5625		    }
5626
5627	        if($style = $node->getAttribute('style'))
5628		    {
5629		        $newNode->setAttribute('style',$style);
5630		    }
5631
5632		    $node->parentNode->replaceChild($newNode, $node);
5633       }
5634
5635
5636
5637        // Convert <code> and <pre> Tags to Htmlentities.
5638        /* TODO XXX Still necessary? Perhaps using bbcodes only?
5639        foreach($this->nodesToConvert as $node)
5640        {
5641            $value = $node->C14N();
5642
5643            $value = str_replace("&#xD;","",$value);
5644
5645        //    print_a("WOWOWO");
5646
5647            if($node->nodeName == 'pre')
5648            {
5649                $value = substr($value,5);
5650                $end = strrpos($value,"</pre>");
5651                $value = substr($value,0,$end);
5652            }
5653
5654            if($node->nodeName == 'code')
5655            {
5656                $value = substr($value,6);
5657                $end = strrpos($value,"</code>");
5658                $value = substr($value,0,$end);
5659            }
5660
5661            $value = htmlentities(htmlentities($value)); // Needed
5662            $node->nodeValue = $value;
5663        }
5664		*/
5665
5666		$cleaned = $doc->saveHTML($doc->documentElement); // $doc->documentElement fixes utf-8/entities issue. @see http://stackoverflow.com/questions/8218230/php-domdocument-loadhtml-not-encoding-utf-8-correctly
5667		// Workaround for https://bugs.php.net/bug.php?id=76285
5668		//  Part 2 of 2
5669		$cleaned = str_replace("\n", "", $cleaned);
5670		$cleaned = str_replace("__E_PARSER_CLEAN_HTML_LINE_BREAK__", "\n", $cleaned);
5671
5672		$cleaned = str_replace('__E_PARSER_CLEAN_HTML_NON_BREAKING_SPACE__', '&nbsp;',  $cleaned); // prevent replacement of &nbsp; with spaces. - convert back.
5673
5674		$cleaned = str_replace('{{{','&#123;', $cleaned); // convert shortcode temporary triple-curly braces back to entities.
5675		$cleaned = str_replace('}}}','&#125;', $cleaned); // convert shortcode temporary triple-curly braces back to entities.
5676
5677        $cleaned = str_replace("__E_PARSER_CLEAN_HTML_CURLY_OPEN__","{", $cleaned);
5678        $cleaned = str_replace("__E_PARSER_CLEAN_HTML_CURLY_CLOSED__","}", $cleaned);
5679
5680		$cleaned = str_replace(array('<body>','</body>'),'', $cleaned); // filter out tags.
5681
5682        return trim($cleaned);
5683    }
5684
5685    public function secureAttributeValue($attribute, $value)
5686    {
5687        $search = isset($this->replaceAttrValues[$attribute]) ? $this->replaceAttrValues[$attribute] : $this->replaceAttrValues['default'];
5688        if(!empty($search))
5689        {
5690            $value = str_replace($search, '', $value);
5691        }
5692        return $value;
5693    }
5694
5695
5696    /**
5697     * Check for Invalid Attribute Values
5698     * @param $value string
5699     * @return true/false
5700     */
5701    function invalidAttributeValue($value)
5702    {
5703
5704
5705        foreach($this->badAttrValues as $v) // global list because a bad value is bad regardless of the attribute it's in. ;-)
5706        {
5707            if(preg_match('/'.$v.'/i',$value)==true)
5708            {
5709				$this->removedList['blacklist'][]	= "Match found for '{$v}' in '{$value}'";
5710
5711                return true;
5712            }
5713
5714        }
5715
5716        return false;
5717    }
5718
5719
5720
5721    /**
5722     * XSS HTML code to test against
5723     */
5724    public function getXss()
5725    {
5726
5727$html = <<<EOF
5728Internationalization Test:
5729ภาษาไทย <br />
5730日本語 <br />
5731简体中文 <br />
5732<a href='somewhere.html' src='invalidatrribute' >Test</a>
5733A GOOD LINK: <a href='http://mylink.php'>Some Link</a>
5734<a href='javascript: something' src='invalidatrribute' >Test regex</a>
5735<img href='invalidattribute' src='myimage.jpg' />
5736<frameset onload=alert(1) data-something=where>
5737<table background="javascript:alert(1)"><tr><td><a href="something.php" onclick="alert(1)">Hi there</a></td></tr></table>
5738<div>
5739<!--<img src="--><img src=x onerror=alert(1)//">
5740<comment><img src="</comment><img src=x onerror=alert(1)//">
5741<ul>
5742<li style=list-style:url() onerror=alert(1)></li> <div style=content:url(data:image/svg+xml,%3Csvg/%3E);visibility:hidden onload=alert(1)></div>
5743</ul>
5744</div>
5745</frameset>
5746<head><base href="javascript://"/></head><body><a href="/. /,alert(1)//#">XXX</a></body>
5747<SCRIPT FOR=document EVENT=onreadystatechange>alert(1)</SCRIPT>
5748<OBJECT CLASSID="clsid:333C7BC4-460F-11D0-BC04-0080C7055A83"><PARAM NAME="DataURL" VALUE="javascript:alert(1)"></OBJECT>
5749<b <script>alert(1)//</script>0</script></b>
5750<div id="div1"><input value="``onmouseover=alert(1)"></div> <div id="div2"></div><
5751script>document.getElementById("div2").innerHTML = document.getElementById("div1").innerHTML;</script>
5752Some example text<br />
5753<b>This is bold</b><br />
5754<i>This is italic</i><br />
5755<small>Some small text</small>
5756<pre>This is pre-formatted
5757        <script>alert('something')</script>
5758        <b>Bold Stuff</b>
5759        <pre>something</pre>
5760        <code>code</code>
5761        <b>BOLD</b>
5762        function myfunction()
5763        {
5764
5765        }
5766 </pre>
5767<code>
5768        function myfunction()
5769        {
5770
5771        }
5772
5773<script>alert('something')</script>
5774</code>
5775<svg><![CDATA[><image xlink:href="]]><img src=xx:x onerror=alert(2)//"></svg>
5776<style><img src="</style><img src=x onerror=alert(1)//">
5777<x '="foo"><x foo='><img src=x onerror=alert(1)//'> <!-- IE 6-9 --> <! '="foo"><x foo='><img src=x onerror=alert(2)//'> <? '="foo"><x foo='><img src=x onerror=alert(3)//'>
5778<embed src="javascript:alert(1)"></embed> // O10.10↓, OM10.0↓, GC6↓, FF <img src="javascript:alert(2)"> <image src="javascript:alert(2)"> // IE6, O10.10↓, OM10.0↓ <script src="javascript:alert(3)"></script> // IE6, O11.01↓, OM10.1↓
5779<div style=width:1px;filter:glow onfilterchange=alert(1)>x</div>
5780<object allowscriptaccess="always" data="test.swf"></object>
5781[A] <? foo="><script>alert(1)</script>"> <! foo="><script>alert(1)</script>"> </ foo="><script>alert(1)</script>"> [B] <? foo="><x foo='?><script>alert(1)</script>'>"> [C] <! foo="[[[x]]"><x foo="]foo><script>alert(1)</script>"> [D] <% foo><x foo="%><script>alert(1)</script>">
5782<iframe src=mhtml:http://html5sec.org/test.html!xss.html></iframe> <iframe src=mhtml:http://html5sec.org/test.gif!xss.html></iframe>
5783<html> <body> <b>some content without two new line \n\n</b> Content-Type: multipart/related; boundary="******"<b>some content without two new line</b> --****** Content-Location: xss.html Content-Transfer-Encoding: base64 PGlmcmFtZSBuYW1lPWxvIHN0eWxlPWRpc3BsYXk6bm9uZT48L2lmcmFtZT4NCjxzY3JpcHQ+DQp1 cmw9bG9jYXRpb24uaHJlZjtkb2N1bWVudC5nZXRFbGVtZW50c0J5TmFtZSgnbG8nKVswXS5zcmM9 dXJsLnN1YnN0cmluZyg2LHVybC5pbmRleE9mKCcvJywxNSkpO3NldFRpbWVvdXQoImFsZXJ0KGZy YW1lc1snbG8nXS5kb2N1bWVudC5jb29raWUpIiwyMDAwKTsNCjwvc2NyaXB0PiAgICAg --******-- </body> </html>
5784<!-- IE 5-9 --> <div id=d><x xmlns="><iframe onload=alert(1)"></div> <script>d.innerHTML+='';</script> <!-- IE 10 in IE5-9 Standards mode --> <div id=d><x xmlns='"><iframe onload=alert(2)//'></div> <script>d.innerHTML+='';</script>
5785<img[a][b]src=x[d]onerror[c]=[e]"alert(1)">
5786<a href="[a]java[b]script[c]:alert(1)">XXX</a>
5787<img src="x` `<script>alert(1)</script>"` `>
5788<img src onerror /" '"= alt=alert(1)//">
5789<title onpropertychange=alert(1)></title><title title=></title>
5790<!-- IE 5-8 standards mode --> <a href=http://foo.bar/#x=`y></a><img alt="`><img src=xx:x onerror=alert(1)></a>"> <!-- IE 5-9 standards mode --> <!a foo=x=`y><img alt="`><img src=xx:x onerror=alert(2)//"> <?a foo=x=`y><img alt="`><img src=xx:x onerror=alert(3)//">
5791<!--[if]><script>alert(1)</script --> <!--[if<img src=x onerror=alert(2)//]> -->
5792<script> Blabla </script>
5793<script src="/\example.com\foo.js"></script> // Safari 5.0, Chrome 9, 10 <script src="\\example.com\foo.js"></script> // Safari 5.0
5794<object id="x" classid="clsid:CB927D12-4FF7-4a9e-A169-56E4B8A75598"></object> <object classid="clsid:02BF25D5-8C17-4B23-BC80-D3488ABDDC6B" onqt_error="alert(1)" style="behavior:url(#x);"><param name=postdomevents /></object>
5795<!-- `<img/src=xx:xx onerror=alert(1)//--!>
5796<xmp> <% </xmp> <img alt='%></xmp><img src=xx:x onerror=alert(1)//'> <script> x='<%' </script> %>/ alert(2) </script> XXX <style> *['<!--']{} </style> -->{} *{color:red}</style>
5797<a style="-o-link:'javascript:alert(1)';-o-link-source:current">X</a>
5798<style>p[foo=bar{}*{-o-link:'javascript:alert(1)'}{}*{-o-link-source:current}*{background:red}]{background:green};</style>
5799<div style="font-family:'foo[a];color:red;';">XXX</div>
5800<form id="test"></form><button form="test" formaction="javascript:alert(1)">X</button>
5801<input onfocus=write(1) autofocus>
5802<video poster=javascript:alert(1)//></video>
5803<video>somemovei.mp4</video>
5804<body onscroll=alert(1)><br><br><br><br><br><br>...<br><br><br><br><input autofocus>
5805
5806<article id="something">Some text goes here</article>
5807
5808
5809EOF;
5810
5811return $html;
5812
5813    }
5814
5815
5816
5817
5818}
5819
5820
5821
5822class e_emotefilter
5823{
5824	private $search         = array();
5825	private $replace        = array();
5826	public $emotes;
5827	private $singleSearch   = array();
5828	private $singleReplace  = array();
5829
5830	function __construct()
5831	{
5832		$pref = e107::getPref();
5833
5834		if(empty($pref['emotepack']))
5835		{
5836			$pref['emotepack'] = "default";
5837			e107::getConfig('emote')->clearPrefCache('emote');
5838			e107::getConfig('core')->set('emotepack','default')->save(false,true,false);
5839		}
5840
5841		$this->emotes = e107::getConfig("emote")->getPref();
5842
5843		if(empty($this->emotes))
5844		{
5845			return;
5846		}
5847
5848		$base = defined('e_HTTP_STATIC') && is_string(e_HTTP_STATIC)  ? e_HTTP_STATIC : SITEURLBASE;
5849
5850		foreach($this->emotes as $key => $value)
5851		{
5852
5853		  $value = trim($value);
5854
5855		  if ($value)
5856		  {	// Only 'activate' emote if there's a substitution string set
5857
5858
5859			$key = preg_replace("#!(\w{3,}?)$#si", ".\\1", $key);
5860			// Next two probably to sort out legacy issues - may not be required any more
5861		//	$key = preg_replace("#_(\w{3})$#", ".\\1", $key);
5862
5863			  $key = str_replace("!", "_", $key);
5864
5865			  $filename = e_IMAGE."emotes/" . $pref['emotepack'] . "/" . $key;
5866
5867
5868
5869			  $fileloc = $base.e_IMAGE_ABS."emotes/" . $pref['emotepack'] . "/" . $key;
5870
5871			  $alt = str_replace(array('.png','.gif', '.jpg'),'', $key);
5872
5873			  if(file_exists($filename))
5874			  {
5875			        $tmp = explode(" ", $value);
5876					foreach($tmp as $code)
5877					{
5878						$img                = "<img class='e-emoticon' src='".$fileloc."' alt=\"".$alt."\"  />";
5879
5880				        $this->search[]     = "\n".$code;
5881				        $this->replace[]    = "\n".$img;
5882
5883						$this->search[]     = " ".$code;
5884				        $this->replace[]    = " ".$img;
5885
5886				        $this->search[]     = ">".$code; // Fix for emote within html.
5887				        $this->replace[]    = ">".$img;
5888
5889				        $this->singleSearch[] = $code;
5890				        $this->singleReplace[] = $img;
5891
5892					}
5893
5894
5895			  /*
5896				if(strstr($value, " "))
5897				{
5898					$tmp = explode(" ", $value);
5899					foreach($tmp as $code)
5900					{
5901						$this->search[] = " ".$code;
5902						$this->search[] = "\n".$code;
5903
5904						$this->replace[] = " <img class='e-emoticon' src='".$fileloc."' alt=\"".$alt."\"  /> ";
5905						$this->replace[] = "\n <img class='e-emoticon' src='".$fileloc."'alt=\"".$alt."\"   /> ";
5906					}
5907					unset($tmp);
5908				}
5909				else
5910				{
5911					if($value)
5912					{
5913						$this->search[] = " ".$value;
5914						$this->search[] = "\n".$value;
5915
5916						$this->replace[] = " <img class='e-emoticon' src='".$fileloc."' alt=\"".$alt."\"   /> ";
5917						$this->replace[] = "\n <img class='e-emoticon' src='".$fileloc."' alt=\"".$alt."\"   /> ";
5918					}
5919				}*/
5920			  }
5921		  }
5922		  else
5923		  {
5924			unset($this->emotes[$key]);
5925		  }
5926
5927
5928		}
5929
5930	//	print_a($this->regSearch);
5931	//	print_a($this->regReplace);
5932
5933	}
5934
5935
5936	function filterEmotes($text)
5937	{
5938
5939		if(empty($text))
5940		{
5941			return '';
5942		}
5943
5944		if(!empty($this->singleSearch) && (strlen($text) < 12) && in_array($text, $this->singleSearch)) // just one emoticon with no space, line-break or html tags around it.
5945		{
5946			return str_replace($this->singleSearch,$this->singleReplace,$text);
5947		}
5948
5949		return str_replace($this->search, $this->replace, $text);
5950
5951	}
5952
5953
5954	function filterEmotesRev($text)
5955	{
5956		return str_replace($this->replace, $this->search, $text);
5957	}
5958}
5959
5960
5961class e_profanityFilter
5962{
5963	var $profanityList;
5964
5965	function __construct()
5966	{
5967		global $pref;
5968
5969		$words = explode(",", $pref['profanity_words']);
5970        $word_array = array();
5971		foreach($words as $word)
5972		{
5973			$word = trim($word);
5974			if($word != "")
5975			{
5976				$word_array[] = $word;
5977				if (strpos($word, '&#036;') !== FALSE)
5978				{
5979					$word_array[] = str_replace('&#036;', '\$', $word);		// Special case - '$' may be 'in clear' or as entity
5980				}
5981			}
5982		}
5983		if(count($word_array))
5984		{
5985			$this->profanityList = str_replace('#','\#',implode("\b|\b", $word_array));		// We can get entities in the string - confuse the regex delimiters
5986		}
5987		unset($words);
5988		return TRUE;
5989	}
5990
5991	function filterProfanities($text)
5992	{
5993		global $pref;
5994		if (!$this->profanityList)
5995		{
5996			return $text;
5997		}
5998		if ($pref['profanity_replace'])
5999		{
6000			return preg_replace("#\b".$this->profanityList."\b#is", $pref['profanity_replace'], $text);
6001		}
6002		else
6003		{
6004			return preg_replace_callback("#\b".$this->profanityList."\b#is", array($this, 'replaceProfanities'), $text);
6005		}
6006	}
6007
6008	function replaceProfanities($matches)
6009	{
6010		/*!
6011		@function replaceProfanities callback
6012		@abstract replaces vowels in profanity words with stars
6013		@param text string - text string to be filtered
6014		@result filtered text
6015		*/
6016
6017		return preg_replace("#a|e|i|o|u#i", "*" , $matches[0]);
6018	}
6019}
6020
6021
6022/**
6023 * Backwards Compatibility Class textparse
6024 */
6025class textparse {
6026
6027	function editparse($text, $mode = "off")
6028	{
6029		if(E107_DBG_DEPRECATED)
6030		{
6031			e107::getDebug()->logDeprecated();
6032		}
6033
6034		return e107::getParser()->toForm($text);
6035	}
6036
6037	function tpa($text, $mode = '', $referrer = '', $highlight_search = false, $poster_id = '')
6038	{
6039		if(E107_DBG_DEPRECATED)
6040		{
6041			e107::getDebug()->logDeprecated();
6042		}
6043
6044		return e107::getParser()->toHTML($text, true, $mode, $poster_id);
6045	}
6046
6047	function tpj($text)
6048	{
6049
6050		if(E107_DBG_DEPRECATED)
6051		{
6052			e107::getDebug()->logDeprecated();
6053		}
6054
6055		return $text;
6056	}
6057
6058	function formtpa($text, $mode = '')
6059	{
6060
6061		if(E107_DBG_DEPRECATED)
6062		{
6063			e107::getDebug()->logDeprecated();
6064		}
6065
6066		return e107::getParser()->toDB($text);
6067	}
6068
6069	function formtparev($text)
6070	{
6071
6072		if(E107_DBG_DEPRECATED)
6073		{
6074			e107::getDebug()->logDeprecated();
6075		}
6076
6077		return e107::getParser()->toForm($text);
6078	}
6079
6080}