1<?php
2/**
3 * MyBB 1.8
4 * Copyright 2014 MyBB Group, All Rights Reserved
5 *
6 * Website: http://www.mybb.com
7 * License: http://www.mybb.com/about/license
8 *
9 */
10
11/**
12 * Selectively removes quote tags from a message, depending on its nested depth.  This is to be used with reply with quote functions.
13 * For malformed quote tag structures, will try to simulate how MyBB's parser handles the issue, but is slightly inaccurate.
14 * Examples, with a cutoff depth of 2:
15 *  #1. INPUT:  [quote]a[quote=me]b[quote]c[/quote][/quote][/quote]
16 *     OUTPUT:  [quote]a[quote=me]b[/quote][/quote]
17 *  #2. INPUT:  [quote=a][quote=b][quote=c][quote=d][/quote][quote=e][/quote][/quote][quote=f][/quote][/quote]
18 *     OUTPUT:  [quote=a][quote=b][/quote][quote=f][/quote][/quote]
19 *
20 * @param string $text the message from which quotes are to be removed
21 * @param integer $rmdepth nested depth at which quotes should be removed; if none supplied, will use MyBB's default; must be at least 0
22 * @return string the original message passed in $text, but with quote tags selectively removed
23 */
24function remove_message_quotes(&$text, $rmdepth=null)
25{
26	if(!$text)
27	{
28		return $text;
29	}
30	if(!isset($rmdepth))
31	{
32		global $mybb;
33		$rmdepth = $mybb->settings['maxquotedepth'];
34	}
35	$rmdepth = (int)$rmdepth;
36
37	// find all tokens
38	// note, at various places, we use the prefix "s" to denote "start" (ie [quote]) and "e" to denote "end" (ie [/quote])
39	preg_match_all("#\[quote(=(?:&quot;|\"|')?.*?(?:&quot;|\"|')?)?\]#si", $text, $smatches, PREG_OFFSET_CAPTURE | PREG_PATTERN_ORDER);
40	preg_match_all("#\[/quote\]#i", $text, $ematches, PREG_OFFSET_CAPTURE | PREG_PATTERN_ORDER);
41
42	if(empty($smatches) || empty($ematches))
43	{
44		return $text;
45	}
46
47	// make things easier by only keeping offsets
48	$soffsets = $eoffsets = array();
49	foreach($smatches[0] as $id => $match)
50	{
51		$soffsets[] = $match[1];
52	}
53	$first_token = 0;
54	if(isset($soffsets[0])) {
55		$first_token = $soffsets[0];
56	}
57	// whilst we loop, also remove unnecessary end tokens at the start of string
58	foreach($ematches[0] as $id => $match)
59	{
60		if($match[1] > $first_token)
61		{
62			$eoffsets[] = $match[1];
63		}
64	}
65	unset($smatches, $ematches);
66
67
68	// elmininate malformed quotes by parsing like the parser does (preg_replace in a while loop)
69	// NOTE: this is slightly inaccurate because the parser considers [quote] and [quote=...] to be different things
70	$good_offsets = array();
71	while(!empty($soffsets) && !empty($eoffsets)) // don't rely on this condition - an end offset before the start offset will cause this to loop indefinitely
72	{
73		$last_offset = 0;
74		foreach($soffsets as $sk => &$soffset)
75		{
76			if($soffset >= $last_offset)
77			{
78				// search for corresponding eoffset
79				foreach($eoffsets as $ek => &$eoffset) // use foreach instead of for to get around indexing issues with unset
80				{
81					if($eoffset > $soffset)
82					{
83						// we've found a pair
84						$good_offsets[$soffset] = 1;
85						$good_offsets[$eoffset] = -1;
86						$last_offset = $eoffset;
87
88						unset($soffsets[$sk], $eoffsets[$ek]);
89						break;
90					}
91				}
92			}
93		}
94
95		// remove any end offsets occurring before start offsets
96		$first_start = reset($soffsets);
97		foreach($eoffsets as $ek => &$eoffset)
98		{
99			if($eoffset < $first_start)
100			{
101				unset($eoffsets[$ek]);
102			}
103			else
104			{
105				break;
106			}
107		}
108		// we don't need to remove start offsets after the last end offset, because the loop will deplete something before that
109	}
110
111	if(empty($good_offsets))
112	{
113		return $text;
114	}
115	ksort($good_offsets);
116
117
118	// we now have a list of all the ordered tokens, ready to go through
119	$depth = 0;
120	$remove_regions = array();
121	$tmp_start = 0;
122	foreach($good_offsets as $offset => $dincr)
123	{
124		if($depth == $rmdepth && $dincr == 1)
125		{
126			$tmp_start = $offset;
127		}
128		$depth += $dincr;
129		if($depth == $rmdepth && $dincr == -1)
130		{
131			$remove_regions[] = array($tmp_start, $offset);
132		}
133	}
134
135	if(empty($remove_regions))
136	{
137		return $text;
138	}
139
140	// finally, remove the quotes from the string
141	$newtext = '';
142	$cpy_start = 0;
143	foreach($remove_regions as &$region)
144	{
145		$newtext .= substr($text, $cpy_start, $region[0]-$cpy_start);
146		$cpy_start = $region[1]+8; // 8 = strlen('[/quote]')
147		// clean up newlines
148		$next_char = $text[$region[1]+8];
149		if($next_char == "\r" || $next_char == "\n")
150		{
151			++$cpy_start;
152			if($next_char == "\r" && $text[$region[1]+9] == "\n")
153			{
154				++$cpy_start;
155			}
156		}
157	}
158	// append remaining end text
159	if(strlen($text) != $cpy_start)
160	{
161		$newtext .= substr($text, $cpy_start);
162	}
163
164	// we're done
165	return $newtext;
166}
167
168/**
169 * Performs cleanup of a quoted message, such as replacing /me commands, before presenting quoted post to the user.
170 *
171 * @param array $quoted_post quoted post info, taken from the DB (requires the 'message', 'username', 'pid' and 'dateline' entries to be set; will use 'userusername' if present. requires 'quote_is_pm' if quote message is from a private message)
172 * @param boolean $remove_message_quotes whether to call remove_message_quotes() on the quoted message
173 * @return string the cleaned up message, wrapped in a quote tag
174 */
175
176function parse_quoted_message(&$quoted_post, $remove_message_quotes=true)
177{
178	global $parser, $lang, $plugins;
179	if(!isset($parser))
180	{
181		require_once MYBB_ROOT."inc/class_parser.php";
182		$parser = new postParser;
183	}
184
185	// Swap username over if we have a registered user
186	if(isset($quoted_post['userusername']))
187	{
188		$quoted_post['username'] = $quoted_post['userusername'];
189	}
190	// Clean up the message
191	$quoted_post['message'] = preg_replace(array(
192		'#(^|\r|\n)/me ([^\r\n<]*)#i',
193		'#(^|\r|\n)/slap ([^\r\n<]*)#i',
194		'#\[attachment=([0-9]+?)\]#i'
195	), array(
196		"\\1* {$quoted_post['username']} \\2",
197		"\\1* {$quoted_post['username']} {$lang->slaps} \\2 {$lang->with_trout}",
198		"",
199	), $quoted_post['message']);
200	$quoted_post['message'] = $parser->parse_badwords($quoted_post['message']);
201
202	if($remove_message_quotes)
203	{
204		global $mybb;
205		$max_quote_depth = (int)$mybb->settings['maxquotedepth'];
206		if($max_quote_depth)
207		{
208			$quoted_post['message'] = remove_message_quotes($quoted_post['message'], $max_quote_depth-1); // we're wrapping the message in a [quote] tag, so take away one quote depth level
209		}
210	}
211
212	$quoted_post = $plugins->run_hooks("parse_quoted_message", $quoted_post);
213
214	$extra = '';
215	if(empty($quoted_post['quote_is_pm']))
216	{
217		$extra = " pid='{$quoted_post['pid']}' dateline='{$quoted_post['dateline']}'";
218	}
219
220	$quote_char = '"';
221	if(strpos($quoted_post['username'], '"') !== false)
222	{
223		$quote_char = "'";
224	}
225
226	return "[quote={$quote_char}{$quoted_post['username']}{$quote_char}{$extra}]\n{$quoted_post['message']}\n[/quote]\n\n";
227}
228
229