Completed
Pull Request — development (#2979)
by Stephen
08:55
created

BBCParser::handleEquals()   C

Complexity

Conditions 10
Paths 16

Size

Total Lines 53
Code Lines 24

Duplication

Lines 17
Ratio 32.08 %

Code Coverage

Tests 30
CRAP Score 10.0244

Importance

Changes 0
Metric Value
cc 10
eloc 24
nc 16
nop 1
dl 17
loc 53
ccs 30
cts 32
cp 0.9375
crap 10.0244
rs 6.5333
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 *
5
 * @name      ElkArte Forum
6
 * @copyright ElkArte Forum contributors
7
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause
8
 *
9
 * This file contains code covered by:
10
 * copyright:	2011 Simple Machines (http://www.simplemachines.org)
11
 * license:		BSD, See included LICENSE.TXT for terms and conditions.
12
 *
13
 * @version 1.1 Release Candidate 1
14
 *
15
 */
16
17
namespace BBC;
18
19
/**
20
 * Class BBCParser
21
 *
22
 * One of the primary functions, parsing BBC to HTML
23
 *
24
 * @package BBC
25
 */
26
class BBCParser
27
{
28
	/** The max number of iterations to perform while solving for out of order attributes */
29
	const MAX_PERMUTE_ITERATIONS = 5040;
30
31
	/** @var string  */
32
	protected $message;
33
	/** @var Codes  */
34
	protected $bbc;
35
	/** @var array  */
36
	protected $bbc_codes;
37
	/** @var array  */
38
	protected $item_codes;
39
	/** @var array  */
40
	protected $tags;
41
	/** @var int parser position in message */
42
	protected $pos;
43
	/** @var int  */
44
	protected $pos1;
45
	/** @var int  */
46
	protected $pos2;
47
	/** @var int  */
48
	protected $pos3;
49
	/** @var int  */
50
	protected $last_pos;
51
	/** @var bool  */
52
	protected $do_smileys = true;
53
	/** @var array  */
54
	protected $open_tags = array();
55
	/** @var string|null This is the actual tag that's open */
56
	protected $inside_tag;
57
	/** @var Autolink|null  */
58
	protected $autolinker;
59
	/** @var bool  */
60
	protected $possible_html;
61
	/** @var HtmlParser|null  */
62
	protected $html_parser;
63
	/** @var bool if we can cache the message or not (some tags disallow caching) */
64
	protected $can_cache = true;
65
	/** @var int footnote tracker */
66
	protected $num_footnotes = 0;
67
	/** @var string used to mark smiles in a message */
68
	protected $smiley_marker = "\r";
69
	/** @var int  */
70
	protected $lastAutoPos = 0;
71
	/** @var array content fo the footnotes */
72
	protected $fn_content = array();
73
	/** @var array  */
74
	protected $tag_possible = array();
75
76
	/**
77
	 * BBCParser constructor.
78
	 *
79
	 * @param \BBC\Codes $bbc
80
	 * @param \BBC\Autolink|null $autolinker
81
	 * @param \BBC\HtmlParser|null $html_parser
82
	 */
83 2
	public function __construct(Codes $bbc, Autolink $autolinker = null, HtmlParser $html_parser = null)
84
	{
85 2
		$this->bbc = $bbc;
86
87 2
		$this->bbc_codes = $this->bbc->getForParsing();
88 2
		$this->item_codes = $this->bbc->getItemCodes();
89
90 2
		$this->autolinker = $autolinker;
91 2
		$this->loadAutolink();
92
93 2
		$this->html_parser = $html_parser;
94 2
	}
95
96
	/**
97
	 * Reset the parser's properties for a new message
98
	 */
99 5
	public function resetParser()
100
	{
101 5
		$this->pos = -1;
102 5
		$this->pos1 = null;
103 5
		$this->pos2 = null;
104 5
		$this->last_pos = null;
105 5
		$this->open_tags = array();
106 5
		$this->inside_tag = null;
107 5
		$this->lastAutoPos = 0;
108 5
		$this->can_cache = true;
109 5
		$this->num_footnotes = 0;
110 5
	}
111
112
	/**
113
	 * Parse the BBC in a string/message
114
	 *
115
	 * @param string $message
116
	 *
117
	 * @return string
118
	 */
119 6
	public function parse($message)
120
	{
121 6
		call_integration_hook('integrate_pre_bbc_parser', array(&$message, $this->bbc));
122
123 6
		$this->message = (string) $message;
124
125
		// Don't waste cycles
126 6
		if ($this->message === '')
127 6
		{
128 1
			return '';
129
		}
130
131
		// @todo remove from here and make the caller figure it out
132 5
		if (!$this->parsingEnabled())
133 5
		{
134
			return $this->message;
135
		}
136
137 5
		$this->resetParser();
138
139
		// @todo change this to <br> (it will break tests and previews and ...)
140 5
		$this->message = str_replace("\n", '<br />', $this->message);
141
142
		// Check if the message might have a link or email to save a bunch of parsing in autolink()
143 5
		$this->autolinker->setPossibleAutolink($this->message);
144
145 5
		$this->possible_html = !empty($GLOBALS['modSettings']['enablePostHTML']) && strpos($message, '&lt;') !== false;
146
147
		// Don't load the HTML Parser unless we have to
148 5
		if ($this->possible_html && $this->html_parser === null)
149 5
		{
150
			$this->loadHtmlParser();
151
		}
152
153
		// This handles pretty much all of the parsing. It is a separate method so it is easier to override and profile.
154 5
		$this->parse_loop();
155
156
		// Close any remaining tags.
157 5
		while ($tag = $this->closeOpenedTag())
158
		{
159 1
			$this->message .= $this->noSmileys($tag[Codes::ATTR_AFTER]);
160 1
		}
161
162 5
		if (isset($this->message[0]) && $this->message[0] === ' ')
163 5
		{
164
			$this->message = substr_replace($this->message, '&nbsp;', 0, 1);
165
			//$this->message = '&nbsp;' . substr($this->message, 1);
166
		}
167
168
		// Cleanup whitespace.
169 5
		$this->message = str_replace(array('  ', '<br /> ', '&#13;'), array('&nbsp; ', '<br />&nbsp;', "\n"), $this->message);
170
171
		// Finish footnotes if we have any.
172 5
		if ($this->num_footnotes > 0)
173 5
		{
174 1
			$this->handleFootnotes();
175 1
		}
176
177
		// Allow addons access to what the parser created
178 5
		$message = $this->message;
179 5
		call_integration_hook('integrate_post_bbc_parser', array(&$message));
180 5
		$this->message = $message;
181
182 5
		return $this->message;
183
	}
184
185
	/**
186
	 * The BBC parsing loop-o-love
187
	 *
188
	 * Walks the string to parse, looking for BBC tags and passing items to the required translation functions
189
	 */
190 5
	protected function parse_loop()
191
	{
192 5
		while ($this->pos !== false)
193
		{
194 5
			$this->last_pos = isset($this->last_pos) ? max($this->pos, $this->last_pos) : $this->pos;
195 5
			$this->pos = strpos($this->message, '[', $this->pos + 1);
196
197
			// Failsafe.
198 5
			if ($this->pos === false || $this->last_pos > $this->pos)
199 5
			{
200 5
				$this->pos = strlen($this->message) + 1;
201 5
			}
202
203
			// Can't have a one letter smiley, URL, or email! (sorry.)
204 5
			if ($this->last_pos < $this->pos - 1)
205 5
			{
206 5
				$this->betweenTags();
207 5
			}
208
209
			// Are we there yet?  Are we there yet?
210 5
			if ($this->pos >= strlen($this->message) - 1)
211 5
			{
212 5
				return;
213
			}
214
215 4
			$next_char = strtolower($this->message[$this->pos + 1]);
216
217
			// Possibly a closer?
218 4
			if ($next_char === '/')
219 4
			{
220 4
				if ($this->hasOpenTags())
221 4
				{
222 3
					$this->handleOpenTags();
223 3
				}
224
225
				// We don't allow / to be used for anything but the closing character, so this can't be a tag
226 4
				continue;
227
			}
228
229
			// No tags for this character, so just keep going (fastest possible course.)
230 4
			if (!isset($this->bbc_codes[$next_char]))
231 4
			{
232 1
				continue;
233
			}
234
235 4
			$this->inside_tag = !$this->hasOpenTags() ? null : $this->getLastOpenedTag();
236
237 4
			if ($this->isItemCode($next_char) && isset($this->message[$this->pos + 2]) && $this->message[$this->pos + 2] === ']' && !$this->bbc->isDisabled('list') && !$this->bbc->isDisabled('li'))
238 4
			{
239
				// Itemcodes cannot be 0 and must be proceeded by a semi-colon, space, tab, new line, or greater than sign
240 1
				if (!($this->message[$this->pos + 1] === '0' && !in_array($this->message[$this->pos - 1], array(';', ' ', "\t", "\n", '>'))))
241 1
				{
242
					// Item codes are complicated buggers... they are implicit [li]s and can make [list]s!
243 1
					$this->handleItemCode();
244 1
				}
245
246
				// No matter what, we have to continue here.
247 1
				continue;
248
			}
249
			else
250
			{
251 4
				$tag = $this->findTag($this->bbc_codes[$next_char]);
252
			}
253
254
			// Implicitly close lists and tables if something other than what's required is in them. This is needed for itemcode.
255 4
			if ($tag === null && $this->inside_tag !== null && !empty($this->inside_tag[Codes::ATTR_REQUIRE_CHILDREN]))
256 4
			{
257
				$this->closeOpenedTag();
258
				$tmp = $this->noSmileys($this->inside_tag[Codes::ATTR_AFTER]);
259
				$this->message = substr_replace($this->message, $tmp, $this->pos, 0);
260
				$this->pos += strlen($tmp) - 1;
261
			}
262
263
			// No tag?  Keep looking, then.  Silly people using brackets without actual tags.
264 4
			if ($tag === null)
265 4
			{
266 1
				continue;
267
			}
268
269
			// Propagate the list to the child (so wrapping the disallowed tag won't work either.)
270 3
			if (isset($this->inside_tag[Codes::ATTR_DISALLOW_CHILDREN]))
271 3
			{
272
				$tag[Codes::ATTR_DISALLOW_CHILDREN] = isset($tag[Codes::ATTR_DISALLOW_CHILDREN]) ? $tag[Codes::ATTR_DISALLOW_CHILDREN] + $this->inside_tag[Codes::ATTR_DISALLOW_CHILDREN] : $this->inside_tag[Codes::ATTR_DISALLOW_CHILDREN];
273
			}
274
275
			// Is this tag disabled?
276 3
			if ($this->bbc->isDisabled($tag[Codes::ATTR_TAG]))
277 3
			{
278
				$this->handleDisabled($tag);
279
			}
280
281
			// The only special case is 'html', which doesn't need to close things.
282 3
			if ($tag[Codes::ATTR_BLOCK_LEVEL] && $tag[Codes::ATTR_TAG] !== 'html' && !$this->inside_tag[Codes::ATTR_BLOCK_LEVEL])
283 3
			{
284 2
				$this->closeNonBlockLevel();
285 2
			}
286
287
			// This is the part where we actually handle the tags. I know, crazy how long it took.
288 3
			if ($this->handleTag($tag))
289 3
			{
290
				continue;
291
			}
292
293
			// If this is block level, eat any breaks after it.
294 3
			if ($tag[Codes::ATTR_BLOCK_LEVEL] && isset($this->message[$this->pos + 1]) && substr_compare($this->message, '<br />', $this->pos + 1, 6) === 0)
295 3
			{
296
				$this->message = substr_replace($this->message, '', $this->pos + 1, 6);
297
			}
298
299
			// Are we trimming outside this tag?
300 3
			if (!empty($tag[Codes::ATTR_TRIM]) && $tag[Codes::ATTR_TRIM] !== Codes::TRIM_OUTSIDE)
301 3
			{
302 2
				$this->trimWhiteSpace($this->pos + 1);
303 2
			}
304 3
		}
305
	}
306
307
	/**
308
	 * Process a tag once the closing character / has been found
309
	 */
310 3
	protected function handleOpenTags()
311
	{
312
		// Next closing bracket after the first character
313 3
		$this->pos2 = strpos($this->message, ']', $this->pos + 1);
314
315
		// Playing games? string = [/]
316 3
		if ($this->pos2 === $this->pos + 2)
317 3
		{
318
			return;
319
		}
320
321
		// Get everything between [/ and ]
322 3
		$look_for = strtolower(substr($this->message, $this->pos + 2, $this->pos2 - $this->pos - 2));
323 3
		$to_close = array();
324 3
		$block_level = null;
325
326
		do
327
		{
328
			// Get the last opened tag
329 3
			$tag = $this->closeOpenedTag();
330
331
			// No open tags
332 3
			if (!$tag)
333 3
			{
334
				break;
335
			}
336
337 3
			if ($tag[Codes::ATTR_BLOCK_LEVEL])
338 3
			{
339
				// Only find out if we need to.
340 2
				if ($block_level === false)
341 2
				{
342
					$this->addOpenTag($tag);
343
					break;
344
				}
345
346
				// The idea is, if we are LOOKING for a block level tag, we can close them on the way.
347 2 View Code Duplication
				if (isset($look_for[1]) && isset($this->bbc_codes[$look_for[0]]))
348 2
				{
349 2
					foreach ($this->bbc_codes[$look_for[0]] as $temp)
350
					{
351 2
						if ($temp[Codes::ATTR_TAG] === $look_for)
352 2
						{
353 2
							$block_level = $temp[Codes::ATTR_BLOCK_LEVEL];
354 2
							break;
355
						}
356 2
					}
357 2
				}
358
359 2
				if ($block_level !== true)
360 2
				{
361
					$block_level = false;
362
					$this->addOpenTag($tag);
363
					break;
364
				}
365 2
			}
366
367 3
			$to_close[] = $tag;
368 3
		} while ($tag[Codes::ATTR_TAG] !== $look_for);
369
370
		// Did we just eat through everything and not find it?
371 3
		if (!$this->hasOpenTags() && (empty($tag) || $tag[Codes::ATTR_TAG] !== $look_for))
372 3
		{
373
			$this->open_tags = $to_close;
374
			return;
375
		}
376 3
		elseif (!empty($to_close) && $tag[Codes::ATTR_TAG] !== $look_for)
377
		{
378 View Code Duplication
			if ($block_level === null && isset($look_for[0], $this->bbc_codes[$look_for[0]]))
379
			{
380
				foreach ($this->bbc_codes[$look_for[0]] as $temp)
381
				{
382
					if ($temp[Codes::ATTR_TAG] === $look_for)
383
					{
384
						$block_level = !empty($temp[Codes::ATTR_BLOCK_LEVEL]);
385
						break;
386
					}
387
				}
388
			}
389
390
			// We're not looking for a block level tag (or maybe even a tag that exists...)
391
			if (!$block_level)
392
			{
393
				foreach ($to_close as $tag)
394
				{
395
					$this->addOpenTag($tag);
396
				}
397
398
				return;
399
			}
400
		}
401
402 3
		foreach ($to_close as $tag)
403
		{
404 3
			$tmp = $this->noSmileys($tag[Codes::ATTR_AFTER]);
405 3
			$this->message = substr_replace($this->message, $tmp, $this->pos, $this->pos2 + 1 - $this->pos);
406 3
			$this->pos += strlen($tmp);
407 3
			$this->pos2 = $this->pos - 1;
408
409
			// See the comment at the end of the big loop - just eating whitespace ;).
410 3 View Code Duplication
			if ($tag[Codes::ATTR_BLOCK_LEVEL] && isset($this->message[$this->pos]) && substr_compare($this->message, '<br />', $this->pos, 6) === 0)
411 3
			{
412
				$this->message = substr_replace($this->message, '', $this->pos, 6);
413
			}
414
415
			// Trim inside whitespace
416 3
			if (!empty($tag[Codes::ATTR_TRIM]) && $tag[Codes::ATTR_TRIM] !== Codes::TRIM_INSIDE)
417 3
			{
418 2
				$this->trimWhiteSpace($this->pos);
419 2
			}
420 3
		}
421
422 3
		if (!empty($to_close))
423 3
		{
424 3
			$this->pos--;
425 3
		}
426 3
	}
427
428
	/**
429
	 * Turn smiley parsing on/off
430
	 *
431
	 * @param bool $toggle
432
	 * @return BBCParser
433
	 */
434
	public function doSmileys($toggle)
435
	{
436
		$this->do_smileys = (bool) $toggle;
437
438
		return $this;
439
	}
440
441
	/**
442
	 * Check if parsing is enabled
443
	 *
444
	 * @return bool
445
	 */
446 5
	public function parsingEnabled()
447
	{
448 5
		return !empty($GLOBALS['modSettings']['enableBBC']);
449
	}
450
451
	/**
452
	 * Load the HTML parsing engine
453
	 */
454
	public function loadHtmlParser()
455
	{
456
		$parser = new HtmlParser;
457
		call_integration_hook('integrate_bbc_load_html_parser', array(&$parser));
458
		$this->html_parser = $parser;
459
	}
460
461
	/**
462
	 * Parse the HTML in a string
463
	 *
464
	 * @param string $data
465
	 */
466
	protected function parseHTML($data)
467
	{
468
		return $this->html_parser->parse($data);
469
	}
470
471
	/**
472
	 * Parse URIs and email addresses in a string to url and email BBC tags to be parsed by the BBC parser
473
	 *
474
	 * @param string $data
475
	 */
476 5
	protected function autoLink($data)
477
	{
478 5
		if ($data === '' || $data === $this->smiley_marker || !$this->autolinker->hasPossible())
479 5
		{
480 4
			return $data;
481
		}
482
483
		// Are we inside tags that should be auto linked?
484 3
		if ($this->hasOpenTags())
485 3
		{
486 3
			foreach ($this->getOpenedTags() as $open_tag)
487
			{
488 3
				if (!$open_tag[Codes::ATTR_AUTOLINK])
489 3
				{
490 3
					return $data;
491
				}
492 1
			}
493 1
		}
494
495 3
		return $this->autolinker->parse($data);
496
	}
497
498
	/**
499
	 * Load the autolink regular expression to be used in autoLink()
500
	 */
501 2
	protected function loadAutolink()
502
	{
503 2
		if ($this->autolinker === null)
504 2
		{
505 1
			$this->autolinker = new Autolink($this->bbc);
506 1
		}
507 2
	}
508
509
	/**
510
	 * Find if the current character is the start of a tag and get it
511
	 *
512
	 * @param array $possible_codes
513
	 *
514
	 * @return null|array the tag that was found or null if no tag found
515
	 */
516 4
	protected function findTag(array $possible_codes)
517
	{
518 4
		$tag = null;
519 4
		$last_check = null;
520
521 4
		foreach ($possible_codes as $possible)
522
		{
523
			// Skip tags that didn't match the next X characters
524 4
			if ($possible[Codes::ATTR_TAG] === $last_check)
525 4
			{
526 2
				continue;
527
			}
528
529
			// The character after the possible tag or nothing
530 4
			$next_c = isset($this->message[$this->pos + 1 + $possible[Codes::ATTR_LENGTH]]) ? $this->message[$this->pos + 1 + $possible[Codes::ATTR_LENGTH]] : '';
531
532
			// This only happens if the tag is the last character of the string
533 4
			if ($next_c === '')
534 4
			{
535
				break;
536
			}
537
538
			// The next character must be one of these or it's not a tag
539 4
			if ($next_c !== ' ' && $next_c !== ']' && $next_c !== '=' && $next_c !== '/')
540 4
			{
541 4
				$last_check = $possible[Codes::ATTR_TAG];
542 4
				continue;
543
			}
544
545
			// Not a match?
546 4
			if (substr_compare($this->message, $possible[Codes::ATTR_TAG], $this->pos + 1, $possible[Codes::ATTR_LENGTH], true) !== 0)
547 4
			{
548 2
				$last_check = $possible[Codes::ATTR_TAG];
549 2
				continue;
550
			}
551
552 4
			$tag = $this->checkCodeAttributes($next_c, $possible);
553 4
			if ($tag === null)
554 4
			{
555 4
				continue;
556
			}
557
558
			// Quotes can have alternate styling, we do this php-side due to all the permutations of quotes.
559 3
			if ($tag[Codes::ATTR_TAG] === 'quote')
560 3
			{
561 1
				$this->alternateQuoteStyle($tag);
562 1
			}
563
564 3
			break;
565 4
		}
566
567
		// If there is a code that says you can't cache, the message can't be cached
568 4
		if ($tag !== null && $this->can_cache !== false)
569 4
		{
570 3
			$this->can_cache = empty($tag[Codes::ATTR_NO_CACHE]);
571 3
		}
572
573
		// If its a footnote, keep track of the number
574 4
		if ($tag[Codes::ATTR_TAG] === 'footnote')
575 4
		{
576 1
			$this->num_footnotes++;
577 1
		}
578
579 4
		return $tag;
580
	}
581
582
	/**
583
	 * Just alternates the applied class for quotes for themes that want to distinguish them
584
	 *
585
	 * @param array $tag
586
	 */
587 1
	protected function alternateQuoteStyle(array &$tag)
588
	{
589
		// Start with standard
590 1
		$quote_alt = false;
591 1
		foreach ($this->open_tags as $open_quote)
592
		{
593
			// Every parent quote this quote has flips the styling
594 1
			if ($open_quote[Codes::ATTR_TAG] === 'quote')
595 1
			{
596 1
				$quote_alt = !$quote_alt;
597 1
			}
598 1
		}
599
		// Add a class to the quote and quoteheader to style alternating blockquotes
600
		//  - Example: class="quoteheader" and class="quoteheader bbc_alt_quoteheader" on the header
601
		//             class="bbc_quote" and class="bbc_quote bbc_alternate_quote" on the blockquote
602
		// This allows simpler CSS for themes (like default) which do not use the alternate styling,
603
		// but still allow it for themes that want it.
604 1
		$tag[Codes::ATTR_BEFORE] = str_replace('<div class="quoteheader">', '<div class="quoteheader' . ($quote_alt ? ' bbc_alt_quoteheader' : '') . '">', $tag[Codes::ATTR_BEFORE]);
605 1
		$tag[Codes::ATTR_BEFORE] = str_replace('<blockquote>', '<blockquote class="bbc_quote' . ($quote_alt ? ' bbc_alternate_quote' : '') . '">', $tag[Codes::ATTR_BEFORE]);
606 1
	}
607
608
	/**
609
	 * Parses BBC codes attributes for codes that may have them
610
	 *
611
	 * @param string $next_c
612
	 * @param array $possible
613
	 * @return array|null
614
	 */
615 4
	protected function checkCodeAttributes($next_c, array $possible)
616
	{
617
		// Do we want parameters?
618 4
		if (!empty($possible[Codes::ATTR_PARAM]))
619 4
		{
620 2
			if ($next_c !== ' ')
621 2
			{
622 1
				return null;
623
			}
624 2
		}
625
		// parsed_content demands an immediate ] without parameters!
626 4
		elseif ($possible[Codes::ATTR_TYPE] === Codes::TYPE_PARSED_CONTENT)
627
		{
628 3
			if ($next_c !== ']')
629 3
			{
630 2
				return null;
631
			}
632 2
		}
633
		else
634
		{
635
			// Do we need an equal sign?
636 4
			if ($next_c !== '=' && in_array($possible[Codes::ATTR_TYPE], array(Codes::TYPE_UNPARSED_EQUALS, Codes::TYPE_UNPARSED_COMMAS, Codes::TYPE_UNPARSED_COMMAS_CONTENT, Codes::TYPE_UNPARSED_EQUALS_CONTENT, Codes::TYPE_PARSED_EQUALS)))
637 4
			{
638 2
				return null;
639
			}
640
641 4
			if ($next_c !== ']')
642 4
			{
643
				// An immediate ]?
644 4
				if ($possible[Codes::ATTR_TYPE] === Codes::TYPE_UNPARSED_CONTENT)
645 4
				{
646 3
					return null;
647
				}
648
				// Maybe we just want a /...
649 4
				elseif ($possible[Codes::ATTR_TYPE] === Codes::TYPE_CLOSED && substr_compare($this->message, '/]', $this->pos + 1 + $possible[Codes::ATTR_LENGTH], 2) !== 0 && substr_compare($this->message, ' /]', $this->pos + 1 + $possible[Codes::ATTR_LENGTH], 3) !== 0)
650
				{
651
					return null;
652
				}
653 4
			}
654
		}
655
656
		// Check allowed tree?
657 4 View Code Duplication
		if (isset($possible[Codes::ATTR_REQUIRE_PARENTS]) && ($this->inside_tag === null || !isset($possible[Codes::ATTR_REQUIRE_PARENTS][$this->inside_tag[Codes::ATTR_TAG]])))
658 4
		{
659
			return null;
660
		}
661
662 4
		if ($this->inside_tag !== null)
663 4
		{
664 2 View Code Duplication
			if (isset($this->inside_tag[Codes::ATTR_REQUIRE_CHILDREN]) && !isset($this->inside_tag[Codes::ATTR_REQUIRE_CHILDREN][$possible[Codes::ATTR_TAG]]))
665 2
			{
666
				return null;
667
			}
668
669
			// If this is in the list of disallowed child tags, don't parse it.
670 2 View Code Duplication
			if (isset($this->inside_tag[Codes::ATTR_DISALLOW_CHILDREN]) && isset($this->inside_tag[Codes::ATTR_DISALLOW_CHILDREN][$possible[Codes::ATTR_TAG]]))
671 2
			{
672
				return null;
673
			}
674
675
			// Not allowed in this parent, replace the tags or show it like regular text
676 2
			if (isset($possible[Codes::ATTR_DISALLOW_PARENTS]) && isset($possible[Codes::ATTR_DISALLOW_PARENTS][$this->inside_tag[Codes::ATTR_TAG]]))
677 2
			{
678
				if (!isset($possible[Codes::ATTR_DISALLOW_BEFORE], $possible[Codes::ATTR_DISALLOW_AFTER]))
679
				{
680
					return null;
681
				}
682
683
				$possible[Codes::ATTR_BEFORE] = isset($possible[Codes::ATTR_DISALLOW_BEFORE]) ? $possible[Codes::ATTR_DISALLOW_BEFORE] : $possible[Codes::ATTR_BEFORE];
684
				$possible[Codes::ATTR_AFTER] = isset($possible[Codes::ATTR_DISALLOW_AFTER]) ? $possible[Codes::ATTR_DISALLOW_AFTER] : $possible[Codes::ATTR_AFTER];
685
			}
686 2
		}
687
688 4
		if (isset($possible[Codes::ATTR_TEST]) && $this->handleTest($possible))
689 4
		{
690 2
			return null;
691
		}
692
693
		// +1 for [, then the length of the tag, then a space
694 4
		$this->pos1 = $this->pos + 1 + $possible[Codes::ATTR_LENGTH] + 1;
695
696
		// This is long, but it makes things much easier and cleaner.
697 4
		if (!empty($possible[Codes::ATTR_PARAM]))
698 4
		{
699 2
			$match = $this->matchParameters($possible, $matches);
700
701
			// Didn't match our parameter list, try the next possible.
702 2
			if (!$match)
703 2
			{
704 2
				return null;
705
			}
706
707 1
			return $this->setupTagParameters($possible, $matches);
708
		}
709
710 3
		return $possible;
711
	}
712
713
	/**
714
	 * Called when a code has defined a test parameter
715
	 *
716
	 * @param array $possible
717
	 *
718
	 * @return bool
719
	 */
720 3
	protected function handleTest(array $possible)
721
	{
722 3
		return preg_match('~^' . $possible[Codes::ATTR_TEST] . '\]$~', substr($this->message, $this->pos + 2 + $possible[Codes::ATTR_LENGTH], strpos($this->message, ']', $this->pos) - ($this->pos + 1 + $possible[Codes::ATTR_LENGTH]))) === 0;
723
	}
724
725
	/**
726
	 * Handles item codes by converting them to lists
727
	 */
728 1
	protected function handleItemCode()
729
	{
730 1
		if (!isset($this->item_codes[$this->message[$this->pos + 1]]))
731 1
			return;
732
733 1
		$tag = $this->item_codes[$this->message[$this->pos + 1]];
734
735
		// First let's set up the tree: it needs to be in a list, or after an li.
736 1
		if ($this->inside_tag === null || ($this->inside_tag[Codes::ATTR_TAG] !== 'list' && $this->inside_tag[Codes::ATTR_TAG] !== 'li'))
737 1
		{
738 1
			$this->addOpenTag(array(
739 1
				Codes::ATTR_TAG => 'list',
740 1
				Codes::ATTR_TYPE => Codes::TYPE_PARSED_CONTENT,
741 1
				Codes::ATTR_AFTER => '</ul>',
742 1
				Codes::ATTR_BLOCK_LEVEL => true,
743 1
				Codes::ATTR_REQUIRE_CHILDREN => array('li' => 'li'),
744 1
				Codes::ATTR_DISALLOW_CHILDREN => isset($this->inside_tag[Codes::ATTR_DISALLOW_CHILDREN]) ? $this->inside_tag[Codes::ATTR_DISALLOW_CHILDREN] : null,
745 1
				Codes::ATTR_LENGTH => 4,
746 1
				Codes::ATTR_AUTOLINK => true,
747 1
			));
748 1
			$code = '<ul' . ($tag === '' ? '' : ' style="list-style-type: ' . $tag . '"') . ' class="bbc_list">';
749 1
		}
750
		// We're in a list item already: another itemcode?  Close it first.
751 1
		elseif ($this->inside_tag[Codes::ATTR_TAG] === 'li')
752
		{
753 1
			$this->closeOpenedTag();
754 1
			$code = '</li>';
755 1
		}
756
		else
757
		{
758
			$code = '';
759
		}
760
761
		// Now we open a new tag.
762 1
		$this->addOpenTag(array(
763 1
			Codes::ATTR_TAG => 'li',
764 1
			Codes::ATTR_TYPE => Codes::TYPE_PARSED_CONTENT,
765 1
			Codes::ATTR_AFTER => '</li>',
766 1
			Codes::ATTR_TRIM => Codes::TRIM_OUTSIDE,
767 1
			Codes::ATTR_BLOCK_LEVEL => true,
768 1
			Codes::ATTR_DISALLOW_CHILDREN => isset($this->inside_tag[Codes::ATTR_DISALLOW_CHILDREN]) ? $this->inside_tag[Codes::ATTR_DISALLOW_CHILDREN] : null,
769 1
			Codes::ATTR_AUTOLINK => true,
770 1
			Codes::ATTR_LENGTH => 2,
771 1
		));
772
773
		// First, open the tag...
774 1
		$code .= '<li>';
775
776 1
		$tmp = $this->noSmileys($code);
777 1
		$this->message = substr_replace($this->message, $tmp, $this->pos, 3);
778 1
		$this->pos += strlen($tmp) - 1;
779
780
		// Next, find the next break (if any.)  If there's more itemcode after it, keep it going - otherwise close!
781 1
		$this->pos2 = strpos($this->message, '<br />', $this->pos);
782 1
		$this->pos3 = strpos($this->message, '[/', $this->pos);
783
784 1
		$num_open_tags = count($this->open_tags);
785 1
		if ($this->pos2 !== false && ($this->pos3 === false || $this->pos2 <= $this->pos3))
786 1
		{
787
			// Can't use offset because of the ^
788
			preg_match('~^(<br />|&nbsp;|\s|\[)+~', substr($this->message, $this->pos2 + 6), $matches);
789
			//preg_match('~(<br />|&nbsp;|\s|\[)+~', $this->message, $matches, 0, $this->pos2 + 6);
790
791
			// Keep the list open if the next character after the break is a [. Otherwise, close it.
792
			$replacement = !empty($matches[0]) && substr_compare($matches[0], '[', -1, 1) === 0 ? '[/li]' : '[/li][/list]';
793
794
			$this->message = substr_replace($this->message, $replacement, $this->pos2, 0);
795
			$this->open_tags[$num_open_tags - 2][Codes::ATTR_AFTER] = '</ul>';
796
		}
797
		// Tell the [list] that it needs to close specially.
798
		else
799
		{
800
			// Move the li over, because we're not sure what we'll hit.
801 1
			$this->open_tags[$num_open_tags - 1][Codes::ATTR_AFTER] = '';
802 1
			$this->open_tags[$num_open_tags - 2][Codes::ATTR_AFTER] = '</li></ul>';
803
		}
804 1
	}
805
806
	/**
807
	 * Handle codes that are of the parsed context type
808
	 *
809
	 * @param array $tag
810
	 *
811
	 * @return bool
812
	 */
813 2
	protected function handleTypeParsedContext(array $tag)
814
	{
815
		// @todo Check for end tag first, so people can say "I like that [i] tag"?
816 2
		$this->addOpenTag($tag);
817 2
		$tmp = $this->noSmileys($tag[Codes::ATTR_BEFORE]);
818 2
		$this->message = substr_replace($this->message, $tmp, $this->pos, $this->pos1 - $this->pos);
819 2
		$this->pos += strlen($tmp) - 1;
820
821 2
		return false;
822
	}
823
824
	/**
825
	 * Handle codes that are of the unparsed context type
826
	 *
827
	 * @param array $tag
828
	 *
829
	 * @return bool
830
	 */
831 2
	protected function handleTypeUnparsedContext(array $tag)
832
	{
833
		// Find the next closer
834 2
		$this->pos2 = stripos($this->message, '[/' . $tag[Codes::ATTR_TAG] . ']', $this->pos1);
835
836
		// No closer
837 2
		if ($this->pos2 === false)
838 2
		{
839
			return true;
840
		}
841
842 2
		$data = substr($this->message, $this->pos1, $this->pos2 - $this->pos1);
843
844 2
		if (!empty($tag[Codes::ATTR_BLOCK_LEVEL]) && isset($data[0]) && substr_compare($data, '<br />', 0, 6) === 0)
845 2
		{
846
			$data = substr($data, 6);
847
		}
848
849 2
		if (isset($tag[Codes::ATTR_VALIDATE]))
850 2
		{
851
			//$tag[Codes::ATTR_VALIDATE]($tag, $data, $this->bbc->getDisabled());
852 2
			$this->filterData($tag, $data);
853 2
		}
854
855 2
		$code = strtr($tag[Codes::ATTR_CONTENT], array('$1' => $data));
856 2
		$tmp = $this->noSmileys($code);
857 2
		$this->message = substr_replace($this->message, $tmp, $this->pos, $this->pos2 + 3 + $tag[Codes::ATTR_LENGTH] - $this->pos);
858 2
		$this->pos += strlen($tmp) - 1;
859 2
		$this->last_pos = $this->pos + 1;
860
861 2
		return false;
862
	}
863
864
	/**
865
	 * Handle codes that are of the unparsed equals context type
866
	 *
867
	 * @param array $tag
868
	 *
869
	 * @return bool
870
	 */
871 1
	protected function handleUnparsedEqualsContext(array $tag)
872
	{
873
		// The value may be quoted for some tags - check.
874 1 View Code Duplication
		if (isset($tag[Codes::ATTR_QUOTED]))
875 1
		{
876
			$quoted = substr_compare($this->message, '&quot;', $this->pos1, 6) === 0;
877
			if ($tag[Codes::ATTR_QUOTED] !== Codes::OPTIONAL && !$quoted)
878
			{
879
				return true;
880
			}
881
882
			if ($quoted)
883
			{
884
				$this->pos1 += 6;
885
			}
886
		}
887
		else
888
		{
889 1
			$quoted = false;
890
		}
891
892 1
		$this->pos2 = strpos($this->message, $quoted === false ? ']' : '&quot;]', $this->pos1);
893 1
		if ($this->pos2 === false)
894 1
		{
895
			return true;
896
		}
897
898 1
		$this->pos3 = stripos($this->message, '[/' . $tag[Codes::ATTR_TAG] . ']', $this->pos2);
899 1
		if ($this->pos3 === false)
900 1
		{
901
			return true;
902
		}
903
904
		$data = array(
905 1
			substr($this->message, $this->pos2 + ($quoted === false ? 1 : 7), $this->pos3 - ($this->pos2 + ($quoted === false ? 1 : 7))),
906 1
			substr($this->message, $this->pos1, $this->pos2 - $this->pos1)
907 1
		);
908
909 1
		if (!empty($tag[Codes::ATTR_BLOCK_LEVEL]) && substr_compare($data[0], '<br />', 0, 6) === 0)
910 1
		{
911
			$data[0] = substr($data[0], 6);
912
		}
913
914
		// Validation for my parking, please!
915 1
		if (isset($tag[Codes::ATTR_VALIDATE]))
916 1
		{
917
			//$tag[Codes::ATTR_VALIDATE]($tag, $data, $this->bbc->getDisabled());
918 1
			$this->filterData($tag, $data);
919 1
		}
920
921 1
		$code = strtr($tag[Codes::ATTR_CONTENT], array('$1' => $data[0], '$2' => $data[1]));
922 1
		$tmp = $this->noSmileys($code);
923 1
		$this->message = substr_replace($this->message, $tmp, $this->pos, $this->pos3 + 3 + $tag[Codes::ATTR_LENGTH] - $this->pos);
924 1
		$this->pos += strlen($tmp) - 1;
925
926 1
		return false;
927
	}
928
929
	/**
930
	 * Handle codes that are of the closed type
931
	 *
932
	 * @param array $tag
933
	 *
934
	 * @return bool
935
	 */
936 1
	protected function handleTypeClosed(array $tag)
937
	{
938 1
		$this->pos2 = strpos($this->message, ']', $this->pos);
939 1
		$tmp = $this->noSmileys($tag[Codes::ATTR_CONTENT]);
940 1
		$this->message = substr_replace($this->message, $tmp, $this->pos, $this->pos2 + 1 - $this->pos);
941 1
		$this->pos += strlen($tmp) - 1;
942
943 1
		return false;
944
	}
945
946
	/**
947
	 * Handle codes that are of the unparsed commas context type
948
	 *
949
	 * @param array $tag
950
	 *
951
	 * @return bool
952
	 */
953
	protected function handleUnparsedCommasContext(array $tag)
954
	{
955
		$this->pos2 = strpos($this->message, ']', $this->pos1);
956
		if ($this->pos2 === false)
957
		{
958
			return true;
959
		}
960
961
		$this->pos3 = stripos($this->message, '[/' . $tag[Codes::ATTR_TAG] . ']', $this->pos2);
962
		if ($this->pos3 === false)
963
		{
964
			return true;
965
		}
966
967
		// We want $1 to be the content, and the rest to be csv.
968
		$data = explode(',', ',' . substr($this->message, $this->pos1, $this->pos2 - $this->pos1));
969
		$data[0] = substr($this->message, $this->pos2 + 1, $this->pos3 - $this->pos2 - 1);
970
971
		if (isset($tag[Codes::ATTR_VALIDATE]))
972
		{
973
			//$tag[Codes::ATTR_VALIDATE]($tag, $data, $this->bbc->getDisabled());
974
			$this->filterData($tag, $data);
975
		}
976
977
		$code = $tag[Codes::ATTR_CONTENT];
978 View Code Duplication
		foreach ($data as $k => $d)
979
		{
980
			$code = strtr($code, array('$' . ($k + 1) => trim($d)));
981
		}
982
983
		$tmp = $this->noSmileys($code);
984
		$this->message = substr_replace($this->message, $tmp, $this->pos, $this->pos3 + 3 + $tag[Codes::ATTR_LENGTH] - $this->pos);
985
		$this->pos += strlen($tmp) - 1;
986
987
		return false;
988
	}
989
990
	/**
991
	 * Handle codes that are of the unparsed commas type
992
	 *
993
	 * @param array $tag
994
	 *
995
	 * @return bool
996
	 */
997
	protected function handleUnparsedCommas(array $tag)
998
	{
999
		$this->pos2 = strpos($this->message, ']', $this->pos1);
1000
		if ($this->pos2 === false)
1001
		{
1002
			return true;
1003
		}
1004
1005
		$data = explode(',', substr($this->message, $this->pos1, $this->pos2 - $this->pos1));
1006
1007
		if (isset($tag[Codes::ATTR_VALIDATE]))
1008
		{
1009
			//$tag[Codes::ATTR_VALIDATE]($tag, $data, $this->bbc->getDisabled());
1010
			$this->filterData($tag, $data);
1011
		}
1012
1013
		// Fix after, for disabled code mainly.
1014
		foreach ($data as $k => $d)
1015
		{
1016
			$tag[Codes::ATTR_AFTER] = strtr($tag[Codes::ATTR_AFTER], array('$' . ($k + 1) => trim($d)));
1017
		}
1018
1019
		$this->addOpenTag($tag);
1020
1021
		// Replace them out, $1, $2, $3, $4, etc.
1022
		$code = $tag[Codes::ATTR_BEFORE];
1023 View Code Duplication
		foreach ($data as $k => $d)
1024
		{
1025
			$code = strtr($code, array('$' . ($k + 1) => trim($d)));
1026
		}
1027
1028
		$tmp = $this->noSmileys($code);
1029
		$this->message = substr_replace($this->message, $tmp, $this->pos, $this->pos2 + 1 - $this->pos);
1030
		$this->pos += strlen($tmp) - 1;
1031
1032
		return false;
1033
	}
1034
1035
	/**
1036
	 * Handle codes that are of the equals type
1037
	 *
1038
	 * @param array $tag
1039
	 *
1040
	 * @return bool
1041
	 */
1042 3
	protected function handleEquals(array $tag)
1043
	{
1044
		// The value may be quoted for some tags - check.
1045 3 View Code Duplication
		if (isset($tag[Codes::ATTR_QUOTED]))
1046 3
		{
1047 2
			$quoted = substr_compare($this->message, '&quot;', $this->pos1, 6) === 0;
1048 2
			if ($tag[Codes::ATTR_QUOTED] !== Codes::OPTIONAL && !$quoted)
1049 2
			{
1050
				return true;
1051
			}
1052
1053
			if ($quoted)
1054 2
			{
1055 1
				$this->pos1 += 6;
1056 1
			}
1057 2
		}
1058
		else
1059
		{
1060 3
			$quoted = false;
1061
		}
1062
1063 3
		$this->pos2 = strpos($this->message, $quoted === false ? ']' : '&quot;]', $this->pos1);
1064 3
		if ($this->pos2 === false)
1065 3
		{
1066
			return true;
1067
		}
1068
1069 3
		$data = substr($this->message, $this->pos1, $this->pos2 - $this->pos1);
1070
1071
		// Validation for my parking, please!
1072 3
		if (isset($tag[Codes::ATTR_VALIDATE]))
1073 3
		{
1074
			//$tag[Codes::ATTR_VALIDATE]($tag, $data, $this->bbc->getDisabled());
1075 3
			$this->filterData($tag, $data);
1076 3
		}
1077
1078
		// For parsed content, we must recurse to avoid security problems.
1079 3
		if ($tag[Codes::ATTR_TYPE] === Codes::TYPE_PARSED_EQUALS)
1080 3
		{
1081 1
			$data = $this->recursiveParser($data, $tag);
1082 1
		}
1083
1084 3
		$tag[Codes::ATTR_AFTER] = strtr($tag[Codes::ATTR_AFTER], array('$1' => $data));
1085
1086 3
		$this->addOpenTag($tag);
1087
1088 3
		$code = strtr($tag[Codes::ATTR_BEFORE], array('$1' => $data));
1089 3
		$tmp = $this->noSmileys($code);
1090 3
		$this->message = substr_replace($this->message, $tmp, $this->pos, $this->pos2 + ($quoted === false ? 1 : 7) - $this->pos);
1091 3
		$this->pos += strlen($tmp) - 1;
1092
1093 3
		return false;
1094
	}
1095
1096
	/**
1097
	 * Handles a tag by its type. Offloads the actual handling to handle*() method
1098
	 *
1099
	 * @param array $tag
1100
	 *
1101
	 * @return bool true if there was something wrong and the parser should advance
1102
	 */
1103 3
	protected function handleTag(array $tag)
1104
	{
1105 3
		switch ($tag[Codes::ATTR_TYPE])
1106
		{
1107 3
			case Codes::TYPE_PARSED_CONTENT:
1108 2
				return $this->handleTypeParsedContext($tag);
1109
1110
			// Don't parse the content, just skip it.
1111 3
			case Codes::TYPE_UNPARSED_CONTENT:
1112 2
				return $this->handleTypeUnparsedContext($tag);
1113
1114
			// Don't parse the content, just skip it.
1115 3
			case Codes::TYPE_UNPARSED_EQUALS_CONTENT:
1116 1
				return $this->handleUnparsedEqualsContext($tag);
1117
1118
			// A closed tag, with no content or value.
1119 3
			case Codes::TYPE_CLOSED:
1120 1
				return $this->handleTypeClosed($tag);
1121
1122
			// This one is sorta ugly... :/
1123 3
			case Codes::TYPE_UNPARSED_COMMAS_CONTENT:
1124
				return $this->handleUnparsedCommasContext($tag);
1125
1126
			// This has parsed content, and a csv value which is unparsed.
1127 3
			case Codes::TYPE_UNPARSED_COMMAS:
1128
				return $this->handleUnparsedCommas($tag);
1129
1130
			// A tag set to a value, parsed or not.
1131 3
			case Codes::TYPE_PARSED_EQUALS:
1132 3
			case Codes::TYPE_UNPARSED_EQUALS:
1133 3
				return $this->handleEquals($tag);
1134
		}
1135
1136
		return false;
1137
	}
1138
1139
	/**
1140
	 * Text between tags
1141
	 *
1142
	 * @todo I don't know what else to call this. It's the area that isn't a tag.
1143
	 */
1144 5
	protected function betweenTags()
1145
	{
1146
		// Make sure the $this->last_pos is not negative.
1147 5
		$this->last_pos = max($this->last_pos, 0);
1148
1149
		// Pick a block of data to do some raw fixing on.
1150 5
		$data = substr($this->message, $this->last_pos, $this->pos - $this->last_pos);
1151
1152
		// This happens when the pos is > last_pos and there is a trailing \n from one of the tags having "AFTER"
1153
		// In micro-optimization tests, using substr() here doesn't prove to be slower. This is much easier to read so leave it.
1154 5
		if ($data === $this->smiley_marker)
1155 5
		{
1156 2
			return;
1157
		}
1158
1159
		// Take care of some HTML!
1160 5
		if ($this->possible_html && strpos($data, '&lt;') !== false)
1161 5
		{
1162
			// @todo new \Parser\BBC\HTML;
1163
			$data = $this->parseHTML($data);
1164
		}
1165
1166 5
		if (!empty($GLOBALS['modSettings']['autoLinkUrls']))
1167 5
		{
1168 5
			$data = $this->autoLink($data);
1169 5
		}
1170
1171
		// This cannot be moved earlier. It breaks tests
1172 5
		$data = str_replace("\t", '&nbsp;&nbsp;&nbsp;', $data);
1173
1174
		// If it wasn't changed, no copying or other boring stuff has to happen!
1175 5
		if (substr_compare($this->message, $data, $this->last_pos, $this->pos - $this->last_pos))
1176 5
		{
1177 1
			$this->message = substr_replace($this->message, $data, $this->last_pos, $this->pos - $this->last_pos);
1178
1179
			// Since we changed it, look again in case we added or removed a tag.  But we don't want to skip any.
1180 1
			$old_pos = strlen($data) + $this->last_pos;
1181 1
			$this->pos = strpos($this->message, '[', $this->last_pos);
1182 1
			$this->pos = $this->pos === false ? $old_pos : min($this->pos, $old_pos);
1183 1
		}
1184 5
	}
1185
1186
	/**
1187
	 * Handles special [footnote] tag processing as the tag is not rendered "inline"
1188
	 */
1189 1
	protected function handleFootnotes()
1190
	{
1191 1
		global $fn_num, $fn_count;
1192 1
		static $fn_total;
1193
1194
		// @todo temporary until we have nesting
1195 1
		$this->message = str_replace(array('[footnote]', '[/footnote]'), '', $this->message);
1196
1197 1
		$fn_num = 0;
1198 1
		$this->fn_content = array();
1199 1
		$fn_count = isset($fn_total) ? $fn_total : 0;
1200
1201
		// Replace our footnote text with a [1] link, save the text for use at the end of the message
1202 1
		$this->message = preg_replace_callback('~(%fn%(.*?)%fn%)~is', array($this, 'footnoteCallback'), $this->message);
1203 1
		$fn_total += $fn_num;
1204
1205
		// If we have footnotes, add them in at the end of the message
1206 1
		if (!empty($fn_num))
1207 1
		{
1208 1
			$this->message .=  $this->smiley_marker . '<div class="bbc_footnotes">' . implode('', $this->fn_content) . '</div>' . $this->smiley_marker;
1209 1
		}
1210 1
	}
1211
1212
	/**
1213
	 * Final footnote conversions, builds the proper link code to footnote at base of post
1214
	 *
1215
	 * @param array $matches
1216
	 *
1217
	 * @return string
1218
	 */
1219 1
	protected function footnoteCallback(array $matches)
1220
	{
1221 1
		global $fn_num, $fn_count;
1222
1223 1
		$fn_num++;
1224 1
		$this->fn_content[] = '<div class="target" id="fn' . $fn_num . '_' . $fn_count . '"><sup>' . $fn_num . '&nbsp;</sup>' . $matches[2] . '<a class="footnote_return" href="#ref' . $fn_num . '_' . $fn_count . '">&crarr;</a></div>';
1225
1226 1
		return '<a class="target" href="#fn' . $fn_num . '_' . $fn_count . '" id="ref' . $fn_num . '_' . $fn_count . '">[' . $fn_num . ']</a>';
1227
	}
1228
1229
	/**
1230
	 * Parse a tag that is disabled
1231
	 *
1232
	 * @param array $tag
1233
	 */
1234
	protected function handleDisabled(array &$tag)
1235
	{
1236
		if (!isset($tag[Codes::ATTR_DISABLED_BEFORE]) && !isset($tag[Codes::ATTR_DISABLED_AFTER]) && !isset($tag[Codes::ATTR_DISABLED_CONTENT]))
1237
		{
1238
			$tag[Codes::ATTR_BEFORE] = !empty($tag[Codes::ATTR_BLOCK_LEVEL]) ? '<div>' : '';
1239
			$tag[Codes::ATTR_AFTER] = !empty($tag[Codes::ATTR_BLOCK_LEVEL]) ? '</div>' : '';
1240
			$tag[Codes::ATTR_CONTENT] = $tag[Codes::ATTR_TYPE] === Codes::TYPE_CLOSED ? '' : (!empty($tag[Codes::ATTR_BLOCK_LEVEL]) ? '<div>$1</div>' : '$1');
1241
		}
1242
		elseif (isset($tag[Codes::ATTR_DISABLED_BEFORE]) || isset($tag[Codes::ATTR_DISABLED_AFTER]))
1243
		{
1244
			$tag[Codes::ATTR_BEFORE] = isset($tag[Codes::ATTR_DISABLED_BEFORE]) ? $tag[Codes::ATTR_DISABLED_BEFORE] : (!empty($tag[Codes::ATTR_BLOCK_LEVEL]) ? '<div>' : '');
1245
			$tag[Codes::ATTR_AFTER] = isset($tag[Codes::ATTR_DISABLED_AFTER]) ? $tag[Codes::ATTR_DISABLED_AFTER] : (!empty($tag[Codes::ATTR_BLOCK_LEVEL]) ? '</div>' : '');
1246
		}
1247
		else
1248
		{
1249
			$tag[Codes::ATTR_CONTENT] = $tag[Codes::ATTR_DISABLED_CONTENT];
1250
		}
1251
	}
1252
1253
	/**
1254
	 * Map required / optional tag parameters to the found tag
1255
	 *
1256
	 * @param array &$possible
1257
	 * @param array &$matches
1258
	 * @return bool
1259
	 */
1260 2
	protected function matchParameters(array &$possible, &$matches)
1261
	{
1262 2
		if (!isset($possible['regex_cache']))
1263 2
		{
1264 2
			$possible['regex_cache'] = array();
1265 2
			$possible['param_check'] = array();
1266 2
			$possible['optionals'] = array();
1267
1268 2
			foreach ($possible[Codes::ATTR_PARAM] as $param => $info)
1269
			{
1270 2
				$quote = empty($info[Codes::PARAM_ATTR_QUOTED]) ? '' : '&quot;';
1271 2
				$possible['optionals'][] = !empty($info[Codes::PARAM_ATTR_OPTIONAL]);
1272 2
				$possible['param_check'][] = ' ' . $param . '=' . $quote;
1273 2
				$possible['regex_cache'][] = '(\s+' . $param . '=' . $quote . (isset($info[Codes::PARAM_ATTR_MATCH]) ? $info[Codes::PARAM_ATTR_MATCH] : '(.+?)') . $quote . ')';
1274 2
			}
1275
1276 2
			$possible['regex_size'] = count($possible[Codes::ATTR_PARAM]) - 1;
1277 2
		}
1278
1279
		// Tag setup for this loop
1280 2
		$this->tag_possible = $possible;
1281
1282
		// Okay, this may look ugly and it is, but it's not going to happen much and it is the best way
1283
		// of allowing any order of parameters but still parsing them right.
1284 2
		$message_stub = $this->messageStub();
1285
1286
		// Set regex optional flags only if the param *is* optional and it *was not used* in this tag
1287 2
		$this->optionalParam($message_stub);
1288
1289
		// If an addon adds many parameters we can exceed max_execution time, lets prevent that
1290
		// 5040 = 7, 40,320 = 8, (N!) etc
1291 2
		$max_iterations = self::MAX_PERMUTE_ITERATIONS;
1292
1293
		// Use the same range to start each time. Most BBC is in the order that it should be in when it starts.
1294 2
		$keys = $this->setKeys();
1295
1296
		// Step, one by one, through all possible permutations of the parameters until we have a match
1297
		do
1298
		{
1299 2
			$match_preg = '~^';
1300 2
			foreach ($keys as $key)
1301
			{
1302 2
				$match_preg .= $this->tag_possible['regex_cache'][$key];
1303 2
			}
1304 2
			$match_preg .= '\]~i';
1305
1306
			// Check if this combination of parameters matches the user input
1307 2
			$match = preg_match($match_preg, $message_stub, $matches) !== 0;
1308 2
		} while (!$match && --$max_iterations && ($keys = pc_next_permutation($keys, $possible['regex_size'])));
1309
1310 2
		return $match;
1311
	}
1312
1313
	/**
1314
	 * Sorts the params so they are in a required to optional order.
1315
	 *
1316
	 * Supports the assumption that the params as defined in CODES is the preferred / common
1317
	 * order they are found in, and inserted by, the editor toolbar.
1318
	 *
1319
	 * @return array
1320
	 */
1321 2
	private function setKeys()
1322
	{
1323 2
		$control_order = array();
1324 2
		$this->tag_possible['regex_keys'] = range(0, $this->tag_possible['regex_size']);
1325
1326
		// Push optional params to the end of the stack but maintain current order of required ones
1327 2
		foreach ($this->tag_possible['regex_keys'] as $index => $info)
1328
		{
1329 2
			$control_order[$index] = $index;
1330
1331 2
			if ($this->tag_possible['optionals'][$index])
1332 2
				$control_order[$index] = $index + $this->tag_possible['regex_size'];
1333 2
		}
1334
1335 2
		array_multisort($control_order, SORT_ASC, $this->tag_possible['regex_cache']);
1336
1337 2
		return $this->tag_possible['regex_keys'];
1338
	}
1339
1340
	/**
1341
	 * Sets the optional parameter search flag only where needed.
1342
	 *
1343
	 * What it does:
1344
	 *
1345
	 * - Sets the optional ()? flag only for optional params that were not actually used
1346
	 * - This makes the permutation function match all required *and* passed parameters
1347
	 * - Returns false if an non optional tag was not found
1348
	 *
1349
	 * @param string $message_stub
1350
	 */
1351 2
	protected function optionalParam($message_stub)
1352
	{
1353
		// Set optional flag only if the param is optional and it was not used in this tag
1354 2
		foreach ($this->tag_possible['optionals'] as $index => $optional)
1355
		{
1356
			// @todo more robust, and slower, check would be a preg_match on $possible['regex_cache'][$index]
1357 2
			$param_exists = stripos($message_stub, $this->tag_possible['param_check'][$index]) !== false;
1358
1359
			// Only make unused optional tags as optional
1360
			if ($optional)
1361 2
			{
1362
				if ($param_exists)
1363 1
					$this->tag_possible['optionals'][$index] = false;
1364
				else
1365 1
					$this->tag_possible['regex_cache'][$index] .= '?';
1366 1
			}
1367 2
		}
1368 2
	}
1369
1370
	/**
1371
	 * Given the position in a message, extracts the tag for analysis
1372
	 *
1373
	 * What it does:
1374
	 *
1375
	 * - Given ' width=100 height=100 alt=image]....[/img]more text and [tags]...'
1376
	 * - Returns ' width=100 height=100 alt=image]....[/img]'
1377
	 *
1378
	 * @return string
1379
	 */
1380 2
	protected function messageStub()
1381
	{
1382
		// For parameter searching, swap in \n's to reduce any regex greediness
1383 2
		$message_stub = str_replace('<br />', "\n", substr($this->message, $this->pos1 - 1)) . "\n";
1384
1385
		// Attempt to pull out just this tag
1386 2
		if (preg_match('~^(?:.+?)\](?>.|(?R))*?\[\/' . $this->tag_possible[Codes::ATTR_TAG] . '\](?:.|\s)~i', $message_stub, $matches) === 1)
1387 2
		{
1388 2
			$message_stub = $matches[0];
1389 2
		}
1390
1391 2
		return $message_stub;
1392
	}
1393
1394
	/**
1395
	 * Recursively call the parser with a new Codes object
1396
	 * This allows to parse BBC in parameters like [quote author="[url]www.quotes.com[/url]"]Something famous.[/quote]
1397
	 *
1398
	 * @param string $data
1399
	 * @param array $tag
1400
	 */
1401 1
	protected function recursiveParser($data, array $tag)
1402
	{
1403
		// @todo if parsed tags allowed is empty, return?
1404 1
		$bbc = clone $this->bbc;
1405
1406 1
		if (!empty($tag[Codes::ATTR_PARSED_TAGS_ALLOWED]))
1407 1
		{
1408 1
			$bbc->setParsedTags($tag[Codes::ATTR_PARSED_TAGS_ALLOWED]);
1409 1
		}
1410
1411
		// Do not use $this->autolinker. For some reason it causes a recursive loop
1412 1
		$autolinker = null;
1413 1
		$html = null;
1414 1
		call_integration_hook('integrate_recursive_bbc_parser', array(&$autolinker, &$html));
1415
1416 1
		$parser = new \BBC\BBCParser($bbc, $autolinker, $html);
1417
1418 1
		return $parser->enableSmileys(empty($tag[Codes::ATTR_PARSED_TAGS_ALLOWED]))->parse($data);
1419
	}
1420
1421
	/**
1422
	 * Return the BBC codes in the system
1423
	 *
1424
	 * @return array
1425
	 */
1426
	public function getBBC()
1427
	{
1428
		return $this->bbc_codes;
1429
	}
1430
1431
	/**
1432
	 * Enable the parsing of smileys
1433
	 *
1434
	 * @param boolean $enable
1435
	 *
1436
	 * @return $this
1437
	 */
1438 1
	public function enableSmileys($enable = true)
1439
	{
1440 1
		$this->do_smileys = (bool) $enable;
1441
1442 1
		return $this;
1443
	}
1444
1445
	/**
1446
	 * Open a tag
1447
	 *
1448
	 * @param array $tag
1449
	 */
1450 3
	protected function addOpenTag(array $tag)
1451
	{
1452 3
		$this->open_tags[] = $tag;
1453 3
	}
1454
1455
	/**
1456
	 * @param string|bool $tag = false False closes the last open tag. Anything else finds that tag LIFO
1457
	 *
1458
	 * @return mixed
1459
	 */
1460 5
	protected function closeOpenedTag($tag = false)
1461
	{
1462 5
		if ($tag === false)
1463 5
		{
1464 5
			return array_pop($this->open_tags);
1465
		}
1466
		elseif (isset($this->open_tags[$tag]))
1467
		{
1468
			$return = $this->open_tags[$tag];
1469
			unset($this->open_tags[$tag]);
1470
1471
			return $return;
1472
		}
1473
	}
1474
1475
	/**
1476
	 * Check if there are any tags that are open
1477
	 *
1478
	 * @return bool
1479
	 */
1480 4
	protected function hasOpenTags()
1481
	{
1482 4
		return !empty($this->open_tags);
1483
	}
1484
1485
	/**
1486
	 * Get the last opened tag
1487
	 *
1488
	 * @return string
1489
	 */
1490 2
	protected function getLastOpenedTag()
1491
	{
1492 2
		return end($this->open_tags);
1493
	}
1494
1495
	/**
1496
	 * Get the currently opened tags
1497
	 *
1498
	 * @param bool|false $tags_only True if you want just the tag or false for the whole code
1499
	 *
1500
	 * @return array
1501
	 */
1502 3
	protected function getOpenedTags($tags_only = false)
1503
	{
1504 3
		if (!$tags_only)
1505 3
		{
1506 3
			return $this->open_tags;
1507
		}
1508
1509
		$tags = array();
1510
		foreach ($this->open_tags as $tag)
1511
		{
1512
			$tags[] = $tag[Codes::ATTR_TAG];
1513
		}
1514
1515
		return $tags;
1516
	}
1517
1518
	/**
1519
	 * Does what it says, removes whitespace
1520
	 *
1521
	 * @param null|int $offset = null
1522
	 */
1523 2
	protected function trimWhiteSpace($offset = null)
1524
	{
1525 2
		if (preg_match('~(<br />|&nbsp;|\s)*~', $this->message, $matches, null, $offset) !== 0 && isset($matches[0]) && $matches[0] !== '')
1526 2
		{
1527
			$this->message = substr_replace($this->message, '', $this->pos, strlen($matches[0]));
1528
		}
1529 2
	}
1530
1531
	/**
1532
	 * @param array $possible
1533
	 * @param array $matches
1534
	 *
1535
	 * @return array
1536
	 */
1537 1
	protected function setupTagParameters(array $possible, array $matches)
1538
	{
1539 1
		$params = array();
1540 1
		for ($i = 1, $n = count($matches); $i < $n; $i += 2)
1541
		{
1542 1
			$key = strtok(ltrim($matches[$i]), '=');
1543
1544 1
			if (isset($possible[Codes::ATTR_PARAM][$key][Codes::PARAM_ATTR_VALUE]))
1545 1
			{
1546 1
				$params['{' . $key . '}'] = strtr($possible[Codes::ATTR_PARAM][$key][Codes::PARAM_ATTR_VALUE], array('$1' => $matches[$i + 1]));
1547 1
			}
1548 1
			elseif (isset($possible[Codes::ATTR_PARAM][$key][Codes::PARAM_ATTR_VALIDATE]))
1549
			{
1550 1
				$params['{' . $key . '}'] = $possible[Codes::ATTR_PARAM][$key][Codes::PARAM_ATTR_VALIDATE]($matches[$i + 1]);
1551 1
			}
1552
			else
1553
			{
1554 1
				$params['{' . $key . '}'] = $matches[$i + 1];
1555
			}
1556
1557
			// Just to make sure: replace any $ or { so they can't interpolate wrongly.
1558 1
			$params['{' . $key . '}'] = str_replace(array('$', '{'), array('&#036;', '&#123;'), $params['{' . $key . '}']);
1559 1
		}
1560
1561 1
		foreach ($possible[Codes::ATTR_PARAM] as $p => $info)
1562
		{
1563 1
			if (!isset($params['{' . $p . '}']))
1564 1
			{
1565 1
				$params['{' . $p . '}'] = '';
1566 1
			}
1567 1
		}
1568
1569
		// We found our tag
1570 1
		$tag = $possible;
1571
1572
		// Put the parameters into the string.
1573 1 View Code Duplication
		if (isset($tag[Codes::ATTR_BEFORE]))
1574 1
		{
1575 1
			$tag[Codes::ATTR_BEFORE] = strtr($tag[Codes::ATTR_BEFORE], $params);
1576 1
		}
1577
1578 1 View Code Duplication
		if (isset($tag[Codes::ATTR_AFTER]))
1579 1
		{
1580 1
			$tag[Codes::ATTR_AFTER] = strtr($tag[Codes::ATTR_AFTER], $params);
1581 1
		}
1582
1583 1 View Code Duplication
		if (isset($tag[Codes::ATTR_CONTENT]))
1584 1
		{
1585 1
			$tag[Codes::ATTR_CONTENT] = strtr($tag[Codes::ATTR_CONTENT], $params);
1586 1
		}
1587
1588 1
		$this->pos1 += strlen($matches[0]) - 1;
1589
1590 1
		return $tag;
1591
	}
1592
1593
	/**
1594
	 * Check if a tag (not a code) is open
1595
	 *
1596
	 * @param string $tag
1597
	 *
1598
	 * @return bool
1599
	 */
1600
	protected function isOpen($tag)
1601
	{
1602
		foreach ($this->open_tags as $open)
1603
		{
1604
			if ($open[Codes::ATTR_TAG] === $tag)
1605
			{
1606
				return true;
1607
			}
1608
		}
1609
1610
		return false;
1611
	}
1612
1613
	/**
1614
	 * Check if a character is an item code
1615
	 *
1616
	 * @param string $char
1617
	 *
1618
	 * @return bool
1619
	 */
1620 4
	protected function isItemCode($char)
1621
	{
1622 4
		return isset($this->item_codes[$char]);
1623
	}
1624
1625
	/**
1626
	 * Close any open codes that aren't block level.
1627
	 * Used before opening a code that *is* block level
1628
	 */
1629 2
	protected function closeNonBlockLevel()
1630
	{
1631 2
		$n = count($this->open_tags) - 1;
1632 2
		while (empty($this->open_tags[$n][Codes::ATTR_BLOCK_LEVEL]) && $n >= 0)
1633
		{
1634
			$n--;
1635
		}
1636
1637
		// Close all the non block level tags so this tag isn't surrounded by them.
1638 2
		for ($i = count($this->open_tags) - 1; $i > $n; $i--)
1639
		{
1640
			$tmp = $this->noSmileys($this->open_tags[$i][Codes::ATTR_AFTER]);
1641
			$this->message = substr_replace($this->message, $tmp, $this->pos, 0);
1642
			$ot_strlen = strlen($tmp);
1643
			$this->pos += $ot_strlen;
1644
			$this->pos1 += $ot_strlen;
1645
1646
			// Trim or eat trailing stuff... see comment at the end of the big loop.
1647 View Code Duplication
			if (!empty($this->open_tags[$i][Codes::ATTR_BLOCK_LEVEL]) && substr_compare($this->message, '<br />', $this->pos, 6) === 0)
1648
			{
1649
				$this->message = substr_replace($this->message, '', $this->pos, 6);
1650
			}
1651
1652
			if (isset($tag[Codes::ATTR_TRIM]) && $tag[Codes::ATTR_TRIM] !== Codes::TRIM_INSIDE)
1653
			{
1654
				$this->trimWhiteSpace($this->pos);
1655
			}
1656
1657
			$this->closeOpenedTag();
1658
		}
1659 2
	}
1660
1661
	/**
1662
	 * Add markers around a string to denote that smileys should not be parsed
1663
	 *
1664
	 * @param string $string
1665
	 *
1666
	 * @return string
1667
	 */
1668 3
	protected function noSmileys($string)
1669
	{
1670 3
		return $this->smiley_marker . $string . $this->smiley_marker;
1671
	}
1672
1673
	/**
1674
	 * Checks if we can cache, some codes prevent this and require parsing each time
1675
	 *
1676
	 * @return bool
1677
	 */
1678
	public function canCache()
1679
	{
1680
		return $this->can_cache;
1681
	}
1682
1683
	/**
1684
	 * This is just so I can profile it.
1685
	 *
1686
	 * @param array $tag
1687
	 * @param $data
1688
	 */
1689 3
	protected function filterData(array $tag, &$data)
1690
	{
1691 3
		$tag[Codes::ATTR_VALIDATE]($tag, $data, $this->bbc->getDisabled());
1692 3
	}
1693
}
1694