1<?php
2/**
3 * Loads various functions used to parse posts.
4 *
5 * @copyright (C) 2008-2012 PunBB, partially based on code (C) 2008-2009 FluxBB.org
6 * @license http://www.gnu.org/licenses/gpl.html GPL version 2 or higher
7 * @package PunBB
8 */
9
10
11// Make sure no one attempts to run this script "directly"
12if (!defined('FORUM'))
13	exit;
14
15// Load the IDNA class for international url handling
16if (defined('FORUM_SUPPORT_PCRE_UNICODE') && defined('FORUM_ENABLE_IDNA'))
17{
18	require FORUM_ROOT.'include/idna/idna_convert.class.php';
19}
20
21
22// Here you can add additional smilies if you like (please note that you must escape singlequote and backslash)
23$smilies = array(':)' => 'smile.png', '=)' => 'smile.png', ':|' => 'neutral.png', '=|' => 'neutral.png', ':(' => 'sad.png', '=(' => 'sad.png', ':D' => 'big_smile.png', '=D' => 'big_smile.png', ':o' => 'yikes.png', ':O' => 'yikes.png', ';)' => 'wink.png', ':/' => 'hmm.png', ':P' => 'tongue.png', ':p' => 'tongue.png', ':lol:' => 'lol.png', ':mad:' => 'mad.png', ':rolleyes:' => 'roll.png', ':cool:' => 'cool.png');
24
25($hook = get_hook('ps_start')) ? eval($hook) : null;
26
27
28//
29// Make sure all BBCodes are lower case and do a little cleanup
30//
31function preparse_bbcode($text, &$errors, $is_signature = false)
32{
33	global $forum_config;
34
35	$return = ($hook = get_hook('ps_preparse_bbcode_start')) ? eval($hook) : null;
36	if ($return !== null)
37		return $return;
38
39	if ($is_signature)
40	{
41		global $lang_profile;
42
43		if (preg_match('#\[quote(=(&quot;|"|\'|)(.*)\\1)?\]|\[/quote\]|\[code\]|\[/code\]|\[list(=([1a\*]))?\]|\[/list\]#i', $text))
44			$errors[] = $lang_profile['Signature quote/code/list'];
45	}
46
47	if ($forum_config['p_sig_bbcode'] == '1' && $is_signature || $forum_config['p_message_bbcode'] == '1' && !$is_signature)
48	{
49		// If the message contains a code tag we have to split it up (text within [code][/code] shouldn't be touched)
50		if (strpos($text, '[code]') !== false && strpos($text, '[/code]') !== false)
51		{
52			list($inside, $outside) = split_text($text, '[code]', '[/code]', $errors);
53			$text = implode("\x1", $outside);
54		}
55
56		// Tidy up lists
57		$pattern_callback = '%\[list(?:=([1a*]))?+\]((?:(?>.*?(?=\[list(?:=[1a*])?+\]|\[/list\]))|(?R))*)\[/list\]%is';
58		$text = preg_replace_callback($pattern_callback, function($matches, $errors) {
59		    return preparse_list_tag($matches[2], $matches[1], $errors);
60        }, $text);
61		$text = str_replace('*'."\0".']', '*]', $text);
62
63		if ($forum_config['o_make_links'] == '1')
64		{
65			$text = do_clickable($text, defined('FORUM_SUPPORT_PCRE_UNICODE'));
66		}
67
68		// If we split up the message before we have to concatenate it together again (code tags)
69		if (isset($inside))
70		{
71			$outside = explode("\x1", $text);
72			$text = '';
73
74			$num_tokens = count($outside);
75
76			for ($i = 0; $i < $num_tokens; ++$i)
77			{
78				$text .= $outside[$i];
79				if (isset($inside[$i]))
80					$text .= '[code]'.$inside[$i].'[/code]';
81			}
82		}
83
84		$temp_text = false;
85		if (empty($errors))
86			$temp_text = preparse_tags($text, $errors, $is_signature);
87
88		if ($temp_text !== false)
89			$text = $temp_text;
90
91		// Remove empty tags
92		while ($new_text = preg_replace('/\[(b|u|i|h|colou?r|quote|code|img|url|email|list)(?:\=[^\]]*)?\]\[\/\1\]/', '', $text))
93		{
94			if ($new_text != $text)
95				$text = $new_text;
96			else
97				break;
98		}
99
100	}
101
102	$return = ($hook = get_hook('ps_preparse_bbcode_end')) ? eval($hook) : null;
103	if ($return !== null)
104		return $return;
105
106	return forum_trim($text);
107}
108
109
110//
111// Check the structure of bbcode tags and fix simple mistakes where possible
112//
113function preparse_tags($text, &$errors, $is_signature = false)
114{
115	global $lang_common, $forum_config;
116
117	// Start off by making some arrays of bbcode tags and what we need to do with each one
118
119	// List of all the tags
120	$tags = array('quote', 'code', 'b', 'i', 'u', 'color', 'colour', 'url', 'email', 'img', 'list', '*', 'h');
121	// List of tags that we need to check are open (You could not put b,i,u in here then illegal nesting like [b][i][/b][/i] would be allowed)
122	$tags_opened = $tags;
123	// and tags we need to check are closed (the same as above, added it just in case)
124	$tags_closed = $tags;
125	// Tags we can nest and the depth they can be nested to (only quotes )
126	$tags_nested = array('quote' => $forum_config['o_quote_depth'], 'list' => 5, '*' => 5);
127	// Tags to ignore the contents of completely (just code)
128	$tags_ignore = array('code');
129	// Block tags, block tags can only go within another block tag, they cannot be in a normal tag
130	$tags_block = array('quote', 'code', 'list', 'h', '*');
131	// Inline tags, we do not allow new lines in these
132	$tags_inline = array('b', 'i', 'u', 'color', 'colour', 'h');
133	// Tags we trim interior space
134	$tags_trim = array('img');
135	// Tags we remove quotes from the argument
136	$tags_quotes = array('url', 'email', 'img');
137	// Tags we limit bbcode in
138	$tags_limit_bbcode = array(
139		'*'		=> array('b', 'i', 'u', 'color', 'colour', 'url', 'email', 'list', 'img'),
140		'list'	=> array('*'),
141		'url'	=> array('b', 'i', 'u', 'color', 'colour', 'img'),
142		'email' => array(),
143		'img'	=> array()
144	);
145	// Tags we can automatically fix bad nesting
146	$tags_fix = array('quote', 'b', 'i', 'u', 'color', 'colour', 'url', 'email', 'h');
147
148	$return = ($hook = get_hook('ps_preparse_tags_start')) ? eval($hook) : null;
149	if ($return !== null)
150		return $return;
151
152	$split_text = preg_split("/(\[[\*a-zA-Z0-9-\/]*?(?:=.*?)?\])/", $text, -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);
153
154	$open_tags = array('post');
155	$open_args = array('');
156	$opened_tag = 0;
157	$new_text = '';
158	$current_ignore = '';
159	$current_nest = '';
160	$current_depth = array();
161	$limit_bbcode = $tags;
162
163	foreach ($split_text as $current)
164	{
165		if ($current == '')
166			continue;
167
168		// Are we dealing with a tag?
169		if (substr($current, 0, 1) != '[' || substr($current, -1, 1) != ']')
170		{
171			// Its not a bbcode tag so we put it on the end and continue
172
173			// If we are nested too deeply don't add to the end
174			if ($current_nest)
175				continue;
176
177			$current = str_replace("\r\n", "\n", $current);
178			$current = str_replace("\r", "\n", $current);
179			if (in_array($open_tags[$opened_tag], $tags_inline) && strpos($current, "\n") !== false)
180			{
181				// Deal with new lines
182				$split_current = preg_split("/(\n\n+)/", $current, -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);
183				$current = '';
184
185				if (!forum_trim($split_current[0], "\n")) // the first part is a linebreak so we need to handle any open tags first
186					array_unshift($split_current, '');
187
188				for ($i = 1; $i < count($split_current); $i += 2)
189				{
190					$temp_opened = array();
191					$temp_opened_arg = array();
192					$temp = $split_current[$i - 1];
193
194					while (!empty($open_tags))
195					{
196						$temp_tag = array_pop($open_tags);
197						$temp_arg = array_pop($open_args);
198
199						if (in_array($temp_tag , $tags_inline))
200						{
201							array_push($temp_opened, $temp_tag);
202							array_push($temp_opened_arg, $temp_arg);
203							$temp .= '[/'.$temp_tag.']';
204						}
205						else
206						{
207							array_push($open_tags, $temp_tag);
208							array_push($open_args, $temp_arg);
209							break;
210						}
211					}
212					$current .= $temp.$split_current[$i];
213					$temp = '';
214					while (!empty($temp_opened))
215					{
216						$temp_tag = array_pop($temp_opened);
217						$temp_arg = array_pop($temp_opened_arg);
218						if (empty($temp_arg))
219							$temp .= '['.$temp_tag.']';
220						else
221							$temp .= '['.$temp_tag.'='.$temp_arg.']';
222						array_push($open_tags, $temp_tag);
223						array_push($open_args, $temp_arg);
224					}
225					$current .= $temp;
226				}
227
228				if (array_key_exists($i - 1, $split_current))
229					$current .= $split_current[$i - 1];
230			}
231
232			if (in_array($open_tags[$opened_tag], $tags_trim))
233				$new_text .= forum_trim($current);
234			else
235				$new_text .= $current;
236
237			continue;
238		}
239
240		// Get the name of the tag
241		$current_arg = '';
242		if (strpos($current, '/') === 1)
243		{
244			$current_tag = substr($current, 2, -1);
245		}
246		else if (strpos($current, '=') === false)
247		{
248			$current_tag = substr($current, 1, -1);
249		}
250		else
251		{
252			$current_tag = substr($current, 1, strpos($current, '=')-1);
253			$current_arg = substr($current, strpos($current, '=')+1, -1);
254		}
255		$current_tag = strtolower($current_tag);
256
257		// Is the tag defined?
258		if (!in_array($current_tag, $tags))
259		{
260			// Its not a bbcode tag so we put it on the end and continue
261			if (!$current_nest)
262				$new_text .= $current;
263
264			continue;
265		}
266
267		// We definitely have a bbcode tag.
268
269		// Make the tag string lower case
270		if ($equalpos = strpos($current,'='))
271		{
272			// We have an argument for the tag which we don't want to make lowercase
273			if (strlen(substr($current, $equalpos)) == 2)
274			{
275				// Empty tag argument
276				$errors[] = sprintf($lang_common['BBCode error 6'], $current_tag);
277				return false;
278			}
279			$current = strtolower(substr($current, 0, $equalpos)).substr($current, $equalpos);
280		}
281		else
282			$current = strtolower($current);
283
284		//This is if we are currently in a tag which escapes other bbcode such as code
285		if ($current_ignore)
286		{
287			if ('[/'.$current_ignore.']' == $current)
288			{
289				// We've finished the ignored section
290				$current = '[/'.$current_tag.']';
291				$current_ignore = '';
292			}
293
294			$new_text .= $current;
295
296			continue;
297		}
298
299		if ($current_nest)
300		{
301			// We are currently too deeply nested so lets see if we are closing the tag or not.
302			if ($current_tag != $current_nest)
303				continue;
304
305			if (substr($current, 1, 1) == '/')
306				$current_depth[$current_nest]--;
307			else
308				$current_depth[$current_nest]++;
309
310			if ($current_depth[$current_nest] <= $tags_nested[$current_nest])
311				$current_nest = '';
312
313			continue;
314		}
315
316		// Check the current tag is allowed here
317		if (!in_array($current_tag, $limit_bbcode) && $current_tag != $open_tags[$opened_tag])
318		{
319			$errors[] = sprintf($lang_common['BBCode error 3'], $current_tag, $open_tags[$opened_tag]);
320			return false;
321		}
322
323		if (substr($current, 1, 1) == '/')
324		{
325			//This is if we are closing a tag
326
327			if ($opened_tag == 0 || !in_array($current_tag, $open_tags))
328			{
329				//We tried to close a tag which is not open
330				if (in_array($current_tag, $tags_opened))
331				{
332					$errors[] = sprintf($lang_common['BBCode error 1'], $current_tag);
333					return false;
334				}
335			}
336			else
337			{
338				// Check nesting
339				while (true)
340				{
341					// Nesting is ok
342					if ($open_tags[$opened_tag] == $current_tag)
343					{
344						array_pop($open_tags);
345						array_pop($open_args);
346						$opened_tag--;
347						break;
348					}
349
350					// Nesting isn't ok, try to fix it
351					if (in_array($open_tags[$opened_tag], $tags_closed) && in_array($current_tag, $tags_closed))
352					{
353						if (in_array($current_tag, $open_tags))
354						{
355							$temp_opened = array();
356							$temp_opened_arg = array();
357							$temp = '';
358							while (!empty($open_tags))
359							{
360								$temp_tag = array_pop($open_tags);
361								$temp_arg = array_pop($open_args);
362
363								if (!in_array($temp_tag, $tags_fix))
364								{
365									// We couldn't fix nesting
366									$errors[] = sprintf($lang_common['BBCode error 5'], $temp_tag);
367									return false;
368								}
369								array_push($temp_opened, $temp_tag);
370								array_push($temp_opened_arg, $temp_arg);
371
372								if ($temp_tag == $current_tag)
373									break;
374								else
375									$temp .= '[/'.$temp_tag.']';
376							}
377							$current = $temp.$current;
378							$temp = '';
379							array_pop($temp_opened);
380							array_pop($temp_opened_arg);
381
382							while (!empty($temp_opened))
383							{
384								$temp_tag = array_pop($temp_opened);
385								$temp_arg = array_pop($temp_opened_arg);
386								if (empty($temp_arg))
387									$temp .= '['.$temp_tag.']';
388								else
389									$temp .= '['.$temp_tag.'='.$temp_arg.']';
390								array_push($open_tags, $temp_tag);
391								array_push($open_args, $temp_arg);
392							}
393							$current .= $temp;
394							$opened_tag--;
395							break;
396						}
397						else
398						{
399							// We couldn't fix nesting
400							$errors[] = sprintf($lang_common['BBCode error 1'], $current_tag);
401							return false;
402						}
403					}
404					else if (in_array($open_tags[$opened_tag], $tags_closed))
405						break;
406					else
407					{
408						array_pop($open_tags);
409						array_pop($open_args);
410						$opened_tag--;
411					}
412				}
413			}
414
415			if (in_array($current_tag, array_keys($tags_nested)))
416			{
417				if (isset($current_depth[$current_tag]))
418					$current_depth[$current_tag]--;
419			}
420
421			if (in_array($open_tags[$opened_tag], array_keys($tags_limit_bbcode)))
422				$limit_bbcode = $tags_limit_bbcode[$open_tags[$opened_tag]];
423			else
424				$limit_bbcode = $tags;
425			$new_text .= $current;
426
427			continue;
428		}
429		else
430		{
431			// We are opening a tag
432			if (in_array($current_tag, array_keys($tags_limit_bbcode)))
433				$limit_bbcode = $tags_limit_bbcode[$current_tag];
434			else
435				$limit_bbcode = $tags;
436
437			if (in_array($current_tag, $tags_block) && !in_array($open_tags[$opened_tag], $tags_block) && $opened_tag != 0)
438			{
439				// We tried to open a block tag within a non-block tag
440				$errors[] = sprintf($lang_common['BBCode error 3'], $current_tag, $open_tags[$opened_tag]);
441				return false;
442			}
443
444			if (in_array($current_tag, $tags_ignore))
445			{
446				// Its an ignore tag so we don't need to worry about whats inside it,
447				$current_ignore = $current_tag;
448				$new_text .= $current;
449				continue;
450			}
451
452			// Deal with nested tags
453			if (in_array($current_tag, $open_tags) && !in_array($current_tag, array_keys($tags_nested)))
454			{
455				// We nested a tag we shouldn't
456				$errors[] = sprintf($lang_common['BBCode error 4'], $current_tag);
457				return false;
458			}
459			else if (in_array($current_tag, array_keys($tags_nested)))
460			{
461				// We are allowed to nest this tag
462
463				if (isset($current_depth[$current_tag]))
464					$current_depth[$current_tag]++;
465				else
466					$current_depth[$current_tag] = 1;
467
468				// See if we are nested too deep
469				if ($current_depth[$current_tag] > $tags_nested[$current_tag])
470				{
471					$current_nest = $current_tag;
472					continue;
473				}
474			}
475
476			// Remove quotes from arguments for certain tags
477			if (strpos($current, '=') !== false && in_array($current_tag, $tags_quotes))
478			{
479				$current = preg_replace('#\['.$current_tag.'=("|\'|)(.*?)\\1\]\s*#i', '['.$current_tag.'=$2]', $current);
480			}
481
482			if (in_array($current_tag, array_keys($tags_limit_bbcode)))
483				$limit_bbcode = $tags_limit_bbcode[$current_tag];
484
485			$open_tags[] = $current_tag;
486			$open_args[] = $current_arg;
487			$opened_tag++;
488			$new_text .= $current;
489			continue;
490		}
491	}
492
493	// Check we closed all the tags we needed to
494	foreach ($tags_closed as $check)
495	{
496		if (in_array($check, $open_tags))
497		{
498			// We left an important tag open
499			$errors[] = sprintf($lang_common['BBCode error 5'], $check);
500			return false;
501		}
502	}
503
504	if ($current_ignore)
505	{
506		// We left an ignore tag open
507		$errors[] = sprintf($lang_common['BBCode error 5'], $current_ignore);
508		return false;
509	}
510
511	$return = ($hook = get_hook('ps_preparse_tags_end')) ? eval($hook) : null;
512	if ($return !== null)
513		return $return;
514
515	return $new_text;
516}
517
518
519//
520// Preparse the contents of [list] bbcode
521//
522function preparse_list_tag($content, $type = '*', &$errors)
523{
524	global $lang_common;
525
526	if (strlen($type) != 1)
527		$type = '*';
528
529	if (strpos($content,'[list') !== false)
530	{
531		$pattern_callback = '%\[list(?:=([1a*]))?+\]((?:(?>.*?(?=\[list(?:=[1a*])?+\]|\[/list\]))|(?R))*)\[/list\]%is';
532		$content = preg_replace_callback($pattern_callback, $callback = function($matches, $errors) {
533		    return preparse_list_tag($matches[2], $matches[1], $errors);
534        }, $content);
535	}
536
537	$items = explode('[*]', str_replace('\"', '"', $content));
538
539	$content = '';
540	foreach ($items as $item)
541	{
542		if (forum_trim($item) != '')
543			$content .= '[*'."\0".']'.str_replace('[/*]', '', forum_trim($item)).'[/*'."\0".']'."\n";
544	}
545
546	return '[list='.$type.']'."\n".$content.'[/list]';
547}
548
549
550//
551// Split text into chunks ($inside contains all text inside $start and $end, and $outside contains all text outside)
552//
553function split_text($text, $start, $end, &$errors, $retab = true)
554{
555	global $forum_config, $lang_common;
556
557	$tokens = explode($start, $text);
558
559	$outside[] = $tokens[0];
560
561	$num_tokens = count($tokens);
562	for ($i = 1; $i < $num_tokens; ++$i)
563	{
564		$temp = explode($end, $tokens[$i]);
565
566		if (count($temp) != 2)
567		{
568			$errors[] = $lang_common['BBCode code problem'];
569			return array(null, array($text));
570		}
571		$inside[] = $temp[0];
572		$outside[] = $temp[1];
573	}
574
575	if ($forum_config['o_indent_num_spaces'] != 8 && $retab)
576	{
577		$spaces = str_repeat(' ', $forum_config['o_indent_num_spaces']);
578		$inside = str_replace("\t", $spaces, $inside);
579	}
580
581	return array($inside, $outside);
582}
583
584
585//
586// Truncate URL if longer than 55 characters (add http:// or ftp:// if missing)
587//
588function handle_url_tag($url, $link = '', $bbcode = false)
589{
590	$return = ($hook = get_hook('ps_handle_url_tag_start')) ? eval($hook) : null;
591	if ($return !== null)
592		return $return;
593
594	$full_url = str_replace(array(' ', '\'', '`', '"'), array('%20', '', '', ''), $url);
595	if (strpos($url, 'www.') === 0)			// If it starts with www, we add http://
596		$full_url = 'http://'.$full_url;
597	else if (strpos($url, 'ftp.') === 0)	// Else if it starts with ftp, we add ftp://
598		$full_url = 'ftp://'.$full_url;
599	else if (!preg_match('#^([a-z0-9]{3,6})://#', $url))	// Else if it doesn't start with abcdef://, we add http://
600		$full_url = 'http://'.$full_url;
601
602	if (defined('FORUM_SUPPORT_PCRE_UNICODE') && defined('FORUM_ENABLE_IDNA'))
603	{
604		static $idn;
605		static $cached_encoded_urls = null;
606
607		if (is_null($cached_encoded_urls))
608			$cached_encoded_urls = array();
609
610		// Check in cache
611		$cache_key = md5($full_url);
612		if (isset($cached_encoded_urls[$cache_key]))
613			$full_url = $cached_encoded_urls[$cache_key];
614		else
615		{
616			if (!isset($idn))
617			{
618				$idn = new idna_convert();
619				$idn->set_parameter('encoding', 'utf8');
620				$idn->set_parameter('strict', false);
621			}
622
623			$full_url = $idn->encode($full_url);
624			$cached_encoded_urls[$cache_key] = $full_url;
625		}
626	}
627
628	// Ok, not very pretty :-)
629	if (!$bbcode)
630	{
631		if (defined('FORUM_SUPPORT_PCRE_UNICODE') && defined('FORUM_ENABLE_IDNA'))
632		{
633			$link_name = ($link == '' || $link == $url) ? $url : $link;
634			if (preg_match('!^(https?|ftp|news){1}'.preg_quote('://xn--', '!').'!', $link_name))
635			{
636				$link = $idn->decode($link_name);
637			}
638		}
639
640		$link = ($link == '' || $link == $url) ? ((utf8_strlen($url) > 55) ? utf8_substr($url, 0 , 39).' … '.utf8_substr($url, -10) : $url) : stripslashes($link);
641	}
642
643	$return = ($hook = get_hook('ps_handle_url_tag_end')) ? eval($hook) : null;
644	if ($return !== null)
645		return $return;
646
647	if ($bbcode)
648	{
649		if (defined('FORUM_SUPPORT_PCRE_UNICODE') && defined('FORUM_ENABLE_IDNA'))
650		{
651			if (preg_match('!^(https?|ftp|news){1}'.preg_quote('://xn--', '!').'!', $link))
652			{
653				$link = $idn->decode($link);
654			}
655		}
656
657		if ($full_url == $link)
658			return '[url]'.$link.'[/url]';
659		else
660			return '[url='.$full_url.']'.$link.'[/url]';
661	}
662	else
663		return '<a href="'.$full_url.'">'.$link.'</a>';
664}
665
666
667//
668// Callback for handle_url_tag
669//
670function callback_handle_url_nobb($reg)
671{
672	return handle_url_tag($reg[1], (isset($reg[2]) ? $reg[2] : ''), false);
673}
674
675//
676// Callback for handle_url_tag
677//
678function callback_handle_url_bb($reg)
679{
680	return handle_url_tag($reg[1], (isset($reg[2]) ? $reg[2] : ''), true);
681}
682
683
684//
685// Turns an URL from the [img] tag into an <img> tag or a <a href...> tag
686//
687function handle_img_tag($url, $is_signature = false, $alt = null)
688{
689	global $lang_common, $forum_user;
690
691	$return = ($hook = get_hook('ps_handle_img_tag_start')) ? eval($hook) : null;
692	if ($return !== null)
693		return $return;
694
695	if ($alt == null)
696		$alt = $url;
697
698	$img_tag = '<a href="'.$url.'">&lt;'.$lang_common['Image link'].'&gt;</a>';
699
700	if ($is_signature && $forum_user['show_img_sig'] != '0')
701		$img_tag = '<img class="sigimage" src="'.$url.'" alt="'.forum_htmlencode($alt).'" />';
702	else if (!$is_signature && $forum_user['show_img'] != '0')
703		$img_tag = '<span class="postimg"><img src="'.$url.'" alt="'.forum_htmlencode($alt).'" /></span>';
704
705	$return = ($hook = get_hook('ps_handle_img_tag_end')) ? eval($hook) : null;
706	if ($return !== null)
707		return $return;
708
709	return $img_tag;
710}
711
712
713//
714// Parse the contents of [list] bbcode
715//
716function handle_list_tag($content, $type = '*')
717{
718	if (strlen($type) != 1)
719		$type = '*';
720
721	if (strpos($content,'[list') !== false)
722	{
723		$pattern_callback = '%\[list(?:=([1a*]))?+\]((?:(?>.*?(?=\[list(?:=[1a*])?+\]|\[/list\]))|(?R))*)\[/list\]%is';
724		$content = preg_replace_callback($pattern_callback, function($matches) {
725		    return handle_list_tag($matches[2], $matches[1]);
726        }, $content);
727	}
728
729	$content = preg_replace('#\s*\[\*\](.*?)\[/\*\]\s*#s', '<li><p>$1</p></li>', forum_trim($content));
730
731	if ($type == '*')
732		$content = '<ul>'.$content.'</ul>';
733	else
734		if ($type == 'a')
735			$content = '<ol class="alpha">'.$content.'</ol>';
736		else
737			$content = '<ol class="decimal">'.$content.'</ol>';
738
739	return '</p>'.$content.'<p>';
740}
741
742
743//
744// Convert BBCodes to their HTML equivalent
745//
746function do_bbcode($text, $is_signature = false)
747{
748	global $lang_common, $forum_user, $forum_config;
749
750	$return = ($hook = get_hook('ps_do_bbcode_start')) ? eval($hook) : null;
751	if ($return !== null)
752		return $return;
753
754	if (strpos($text, '[quote') !== false)
755	{
756		$text = preg_replace_callback(
757			'#\[quote=(&\#039;|&quot;|"|\'|)(.*?)\\1\]#', function($matches) {
758global $lang_common;
759return '</p><div class="quotebox"><cite>'.str_replace(array('[', '"'), array('&#91;', '"'), $matches[2])." ".$lang_common['wrote'].":</cite><blockquote><p>";
760},
761$text);
762		$text = preg_replace('#\[quote\]\s*#', '</p><div class="quotebox"><blockquote><p>', $text);
763		$text = preg_replace('#\s*\[\/quote\]#S', '</p></blockquote></div><p>', $text);
764	}
765
766	if (!$is_signature)
767	{
768		$pattern_callback[] = '%\[list(?:=([1a*]))?+\]((?:(?>.*?(?=\[list(?:=[1a*])?+\]|\[/list\]))|(?R))*)\[/list\]%is';
769		$replace_callback[] = 'handle_list_tag($matches[2], $matches[1])';
770	}
771
772    $pattern[] = '#\[email\]([^\[]*?)\[/email\]#';
773    $pattern[] = '#\[email=([^\[]+?)\](.*?)\[/email\]#';
774
775    $replace[] = '<a href=\"mailto:$matches[1]\">$matches[1]</a>';
776    $replace[] = '<a href=\"mailto:$matches[1]\">$matches[2]</a>';
777
778	$pattern[] = '#\[b\](.*?)\[/b\]#ms';
779	$pattern[] = '#\[i\](.*?)\[/i\]#ms';
780	$pattern[] = '#\[u\](.*?)\[/u\]#ms';
781	$pattern[] = '#\[colou?r=([a-zA-Z]{3,20}|\#[0-9a-fA-F]{6}|\#[0-9a-fA-F]{3})](.*?)\[/colou?r\]#ms';
782	$pattern[] = '#\[h\](.*?)\[/h\]#ms';
783
784	$replace[] = '<strong>$matches[1]</strong>';
785	$replace[] = '<em>$matches[1]</em>';
786	$replace[] = '<span class=\"bbu\">$matches[1]</span>';
787	$replace[] = '<span style=\"color: $matches[1]\">$matches[2]</span>';
788	$replace[] = '</p><h5>$matches[1]</h5><p>';
789
790	if (($is_signature && $forum_config['p_sig_img_tag'] == '1') || (!$is_signature && $forum_config['p_message_img_tag'] == '1'))
791	{
792		$pattern[] = '#\[img\]((ht|f)tps?://)([^\s<"]*?)\[/img\]#';
793		$pattern[] = '#\[img=([^\[]*?)\]((ht|f)tps?://)([^\s<"]*?)\[/img\]#';
794		if ($is_signature)
795		{
796			$replace[] = '".handle_img_tag($matches[1].$matches[3], true)."';
797			$replace[] = '".handle_img_tag($matches[2].$matches[4], true, $matches[1])."';
798		}
799		else
800		{
801			$replace[] = '".handle_img_tag($matches[1].$matches[3], false)."';
802			$replace[] = '".handle_img_tag($matches[2].$matches[4], false, $matches[1])."';
803		}
804	}
805
806	$text = preg_replace_callback('#\[url\]([^\[]*?)\[/url\]#', 'callback_handle_url_nobb', $text);
807	$text = preg_replace_callback('#\[url=([^\[]+?)\](.*?)\[/url\]#', 'callback_handle_url_nobb', $text);
808
809	$return = ($hook = get_hook('ps_do_bbcode_replace')) ? eval($hook) : null;
810	if ($return !== null)
811		return $return;
812
813	$count = count($pattern);
814	for ($i = 0; $i < $count; $i++) {
815		$text = preg_replace_callback($pattern[$i], function($matches) use ($replace, $i) {
816		        return eval('return "'.$replace[$i].'";');
817            }, $text);
818	}
819
820	$count = count($pattern_callback);
821	for ($i = 0; $i < $count; $i++) {
822		$text = preg_replace_callback($pattern_callback[$i], function($matches) use ($replace_callback, $i) {
823            return eval('return '.$replace_callback[$i].';');
824        }, $text);
825	}
826	$return = ($hook = get_hook('ps_do_bbcode_end')) ? eval($hook) : null;
827	if ($return !== null)
828		return $return;
829
830	return $text;
831}
832
833
834//
835// Make hyperlinks clickable
836//
837function do_clickable($text, $unicode = FALSE)
838{
839	$text = ' '.$text;
840
841	if ($unicode)
842	{
843	    // Round 1
844		$text = preg_replace_callback(
845		    '#(?<=[\s\]\)])(<)?(\[)?(\()?([\'"]?)(https?|ftp|news){1}://([\p{Nd}\p{L}\-]+\.([\p{Nd}\p{L}\-]+\.)*[\p{Nd}\p{L}\-]+(:[0-9]+)?(/[^\s\[]*[^\s.,?!\[;:-]?)?)\4(?(3)(\)))(?(2)(\]))(?(1)(>))(?![^\s]*\[/(?:url|img)\])#iu',
846            function($matches) {
847		        for($i = 1; $i <= 12; $i++) {
848		            $matches[$i] = isset($matches[$i]) ? $matches[$i]:'';
849		        }
850		        return stripslashes($matches[1].$matches[2].$matches[3].$matches[4]).
851                       handle_url_tag($matches[5].'://'.$matches[6], $matches[5].'://'.$matches[6], true).
852                       stripslashes($matches[4].$matches[10].$matches[11].$matches[12]);
853            }, $text);
854
855        // Round 2
856		$text = preg_replace_callback(
857		    '#(?<=[\s\]\)])(<)?(\[)?(\()?([\'"]?)(www|ftp)\.(([\p{Nd}\p{L}\-]+\.)*[\p{Nd}\p{L}\-]+(:[0-9]+)?(/[^\s\[]*[^\s.,?!\[;:-])?)\4(?(3)(\)))(?(2)(\]))(?(1)(>))(?![^\s]*\[/(?:url|img)\])#iu',
858            function($matches) {
859		        for($i = 1; $i <= 12; $i++) {
860		            $matches[$i] = isset($matches[$i]) ? $matches[$i] : '';
861		        }
862		        return stripslashes($matches[1].$matches[2].$matches[3].$matches[4]).
863                       handle_url_tag($matches[5].'.'.$matches[6], $matches[5].'.'.$matches[6], true).
864                       stripslashes($matches[4].$matches[10].$matches[11].$matches[12]);
865            }, $text);
866	}
867	else
868	{
869	    // Round 1
870		$text = preg_replace_callback(
871		    '#(?<=[\s\]\)])(<)?(\[)?(\()?([\'"]?)(https?|ftp|news){1}://([\w\-]+\.([\w\-]+\.)*[\w]+(:[0-9]+)?(/[^\s\[]*[^\s.,?!\[;:-]?)?)\4(?(3)(\)))(?(2)(\]))(?(1)(>))(?![^\s]*\[/(?:url|img)\])#i',
872            function($matches) {
873		        for($i = 1; $i <= 12; $i++) {
874		            $matches[$i] = isset($matches[$i]) ? $matches[$i] : '';
875		        }
876		        return stripslashes($matches[1].$matches[2].$matches[3].$matches[4]).
877                       handle_url_tag($matches[5].'://'.$matches[6], $matches[5].'://'.$matches[6], true).
878                       stripslashes($matches[4].$matches[10].$matches[11].$matches[12]);
879            }, $text);
880
881		// Round 2
882		$text = preg_replace_callback(
883		    '#(?<=[\s\]\)])(<)?(\[)?(\()?([\'"]?)(www|ftp)\.(([\w\-]+\.)*[\w]+(:[0-9]+)?(/[^\s\[]*[^\s.,?!\[;:-])?)\4(?(3)(\)))(?(2)(\]))(?(1)(>))(?![^\s]*\[/(?:url|img)\])#i',
884            function($matches) {
885		        for($i = 1; $i <= 12; $i++) {
886		            $matches[$i] = isset($matches[$i]) ? $matches[$i]:'';
887		        }
888		        return stripslashes($matches[1].$matches[2].$matches[3].$matches[4]).
889                    handle_url_tag($matches[5].'.'.$matches[6], $matches[5].'.'.$matches[6], true).
890                    stripslashes($matches[4].$matches[10].$matches[11].$matches[12]);
891            }, $text);
892	}
893
894	return substr($text, 1);
895}
896
897
898//
899// Convert a series of smilies to images
900//
901function do_smilies($text)
902{
903	global $forum_config, $base_url, $smilies;
904
905	$return = ($hook = get_hook('ps_do_smilies_start')) ? eval($hook) : null;
906	if ($return !== null)
907		return $return;
908
909	$text = ' '.$text.' ';
910
911	foreach ($smilies as $smiley_text => $smiley_img)
912	{
913		if (strpos($text, $smiley_text) !== false)
914			$text = preg_replace("#(?<=[>\s])".preg_quote($smiley_text, '#')."(?=\W)#m", '<img src="'.$base_url.'/img/smilies/'.$smiley_img.'" width="15" height="15" alt="'.substr($smiley_img, 0, strrpos($smiley_img, '.')).'" />', $text);
915	}
916
917	$return = ($hook = get_hook('ps_do_smilies_end')) ? eval($hook) : null;
918
919	return substr($text, 1, -1);
920}
921
922
923//
924// Parse message text
925//
926function parse_message($text, $hide_smilies)
927{
928	global $forum_config, $lang_common, $forum_user;
929
930	$return = ($hook = get_hook('ps_parse_message_start')) ? eval($hook) : null;
931	if ($return !== null)
932		return $return;
933
934	if ($forum_config['o_censoring'] == '1')
935		$text = censor_words($text);
936
937	$return = ($hook = get_hook('ps_parse_message_post_censor')) ? eval($hook) : null;
938	if ($return !== null)
939		return $return;
940
941	// Convert applicable characters to HTML entities
942	$text = forum_htmlencode($text);
943
944	$return = ($hook = get_hook('ps_parse_message_pre_split')) ? eval($hook) : null;
945	if ($return !== null)
946		return $return;
947
948	// If the message contains a code tag we have to split it up (text within [code][/code] shouldn't be touched)
949	if (strpos($text, '[code]') !== false && strpos($text, '[/code]') !== false)
950	{
951		list($inside, $outside) = split_text($text, '[code]', '[/code]', $errors);
952		$text = implode("\x1", $outside);
953	}
954
955	$return = ($hook = get_hook('ps_parse_message_post_split')) ? eval($hook) : null;
956	if ($return !== null)
957		return $return;
958
959	if ($forum_config['p_message_bbcode'] == '1' && strpos($text, '[') !== false && strpos($text, ']') !== false)
960		$text = do_bbcode($text);
961
962	if ($forum_config['o_smilies'] == '1' && $forum_user['show_smilies'] == '1' && $hide_smilies == '0')
963		$text = do_smilies($text);
964
965	$return = ($hook = get_hook('ps_parse_message_bbcode')) ? eval($hook) : null;
966	if ($return !== null)
967		return $return;
968
969	// Deal with newlines, tabs and multiple spaces
970	$pattern = array("\n", "\t", '  ', '  ');
971	$replace = array('<br />', '&nbsp; &nbsp; ', '&nbsp; ', ' &nbsp;');
972	$text = str_replace($pattern, $replace, $text);
973
974	$return = ($hook = get_hook('ps_parse_message_pre_merge')) ? eval($hook) : null;
975	if ($return !== null)
976		return $return;
977
978	// If we split up the message before we have to concatenate it together again (code tags)
979	if (isset($inside))
980	{
981		$outside = explode("\x1", $text);
982		$text = '';
983
984		$num_tokens = count($outside);
985
986		for ($i = 0; $i < $num_tokens; ++$i)
987		{
988			$text .= $outside[$i];
989			if (isset($inside[$i]))
990				$text .= '</p><div class="codebox"><pre><code>'.forum_trim($inside[$i], "\n\r").'</code></pre></div><p>';
991		}
992	}
993
994	$return = ($hook = get_hook('ps_parse_message_post_merge')) ? eval($hook) : null;
995	if ($return !== null)
996		return $return;
997
998	// Add paragraph tag around post, but make sure there are no empty paragraphs
999	$text = preg_replace('#<br />\s*?<br />((\s*<br />)*)#i', "</p>$1<p>", $text);
1000	$text = str_replace('<p><br />', '<p>', $text);
1001	$text = str_replace('<p></p>', '', '<p>'.$text.'</p>');
1002
1003	$return = ($hook = get_hook('ps_parse_message_end')) ? eval($hook) : null;
1004	if ($return !== null)
1005		return $return;
1006
1007	return $text;
1008}
1009
1010
1011//
1012// Parse signature text
1013//
1014function parse_signature($text)
1015{
1016	global $forum_config, $lang_common, $forum_user;
1017
1018	$return = ($hook = get_hook('ps_parse_signature_start')) ? eval($hook) : null;
1019	if ($return !== null)
1020		return $return;
1021
1022	if ($forum_config['o_censoring'] == '1')
1023		$text = censor_words($text);
1024
1025	$return = ($hook = get_hook('ps_parse_signature_post_censor')) ? eval($hook) : null;
1026	if ($return !== null)
1027		return $return;
1028
1029	// Convert applicable characters to HTML entities
1030	$text = forum_htmlencode($text);
1031
1032	$return = ($hook = get_hook('ps_parse_signature_pre_bbcode')) ? eval($hook) : null;
1033	if ($return !== null)
1034		return $return;
1035
1036	if ($forum_config['p_sig_bbcode'] == '1' && strpos($text, '[') !== false && strpos($text, ']') !== false)
1037		$text = do_bbcode($text, true);
1038
1039	if ($forum_config['o_smilies_sig'] == '1' && $forum_user['show_smilies'] == '1')
1040		$text = do_smilies($text);
1041
1042	$return = ($hook = get_hook('ps_parse_signature_post_bbcode')) ? eval($hook) : null;
1043	if ($return !== null)
1044		return $return;
1045
1046	// Deal with newlines, tabs and multiple spaces
1047	$pattern = array("\n", "\t", '  ', '  ');
1048	$replace = array('<br />', '&nbsp; &nbsp; ', '&nbsp; ', ' &nbsp;');
1049	$text = str_replace($pattern, $replace, $text);
1050
1051	$return = ($hook = get_hook('ps_parse_signature_end')) ? eval($hook) : null;
1052	if ($return !== null)
1053		return $return;
1054
1055	return $text;
1056}
1057
1058define('FORUM_PARSER_LOADED', 1);
1059