Completed
Pull Request — development (#2329)
by Joshua
10:12
created

BBCParser::handleDisabled()   B

Complexity

Conditions 14
Paths 33

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 210

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 18
ccs 0
cts 13
cp 0
rs 7.37
c 1
b 0
f 0
cc 14
eloc 10
nc 33
nop 1
crap 210

How to fix   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 software is a derived product, based on:
10
 *
11
 * Simple Machines Forum (SMF)
12
 * copyright:	2011 Simple Machines (http://www.simplemachines.org)
13
 * license:		BSD, See included LICENSE.TXT for terms and conditions.
14
 *
15
 *
16
 */
17
18
namespace BBC;
19
20
class BBCParser
21
{
22
	const MAX_PERMUTE_ITERATIONS = 5040;
23
24
	protected $message;
25
	protected $bbc;
26
	protected $bbc_codes;
27
	protected $item_codes;
28
	protected $tags;
29
	protected $pos;
30
	protected $pos1;
31
	protected $pos2;
32
	protected $pos3;
33
	protected $last_pos;
34
	protected $do_smileys = true;
35
	protected $open_tags = array();
36
	// This is the actual tag that's open
37
	protected $inside_tag;
38
39
	protected $autolinker;
40
	protected $possible_html;
41
	protected $html_parser;
42
43
	protected $can_cache = true;
44
	protected $num_footnotes = 0;
45
	protected $smiley_marker = "\r";
46
47
	/**
48
	 * BBCParser constructor.
49
	 *
50
	 * @param \BBC\Codes $bbc
51
	 * @param \BBC\Autolink|null $autolinker
52
	 * @param \BBC\HtmlParser|null $html_parser
53
	 */
54 1
	public function __construct(Codes $bbc, Autolink $autolinker = null, HtmlParser $html_parser = null)
55
	{
56 1
		$this->bbc = $bbc;
57
58 1
		$this->bbc_codes = $this->bbc->getForParsing();
59 1
		$this->item_codes = $this->bbc->getItemCodes();
60
61 1
		$this->autolinker = $autolinker;
62 1
		$this->loadAutolink();
63
64 1
		$this->html_parser = $html_parser;
65 1
	}
66
67
	/**
68
	 * Reset the parser's properties for a new message
69
	 */
70
	public function resetParser()
71
	{
72
		$this->pos = -1;
73
		$this->pos1 = null;
74
		$this->pos2 = null;
75
		$this->last_pos = null;
76
		$this->open_tags = array();
77
		$this->inside_tag = null;
78
		$this->lastAutoPos = 0;
0 ignored issues
show
Bug introduced by
The property lastAutoPos does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
79
		$this->can_cache = true;
80
		$this->num_footnotes = 0;
81
	}
82
83
	/**
84
	 * Parse the BBC in a string/message
85
	 *
86
	 * @param string $message
87
	 *
88
	 * @return string
89
	 */
90
	public function parse($message)
0 ignored issues
show
Coding Style introduced by
parse uses the super-global variable $GLOBALS which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
91
	{
92
		call_integration_hook('integrate_pre_bbc_parser', array(&$message, $this->bbc));
93
94
		$this->message = $message;
95
96
		// Don't waste cycles
97
		if ($this->message === '')
98
		{
99
			return '';
100
		}
101
102
		// Clean up any cut/paste issues we may have
103
		$this->message = sanitizeMSCutPaste($this->message);
104
105
		// @todo remove from here and make the caller figure it out
106
		if (!$this->parsingEnabled())
107
		{
108
			return $this->message;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression $this->message; of type string|false adds false to the return on line 108 which is incompatible with the return type documented by BBC\BBCParser::parse of type string. It seems like you forgot to handle an error condition.
Loading history...
109
		}
110
111
		$this->resetParser();
112
113
		// @todo change this to <br> (it will break tests)
114
		$this->message = str_replace("\n", '<br />', $this->message);
115
116
		// Check if the message might have a link or email to save a bunch of parsing in autolink()
117
		$this->autolinker->setPossibleAutolink($this->message);
118
119
		$this->possible_html = !empty($GLOBALS['modSettings']['enablePostHTML']) && strpos($message, '&lt;') !== false;
120
121
		// Don't load the HTML Parser unless we have to
122
		if ($this->possible_html && $this->html_parser === null)
123
		{
124
			$this->loadHtmlParser();
125
		}
126
127
		// This handles pretty much all of the parsing. It is a separate method so it is easier to override and profile.
128
		$this->parse_loop();
129
130
		// Close any remaining tags.
131
		while ($tag = $this->closeOpenedTag())
132
		{
133
			$this->message .= $this->noSmileys($tag[Codes::ATTR_AFTER]);
134
		}
135
136
		if (isset($this->message[0]) && $this->message[0] === ' ')
137
		{
138
			$this->message = substr_replace($this->message, '&nbsp;', 0, 1);
139
			//$this->message = '&nbsp;' . substr($this->message, 1);
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
140
		}
141
142
		// Cleanup whitespace.
143
		$this->message = str_replace(array('  ', '<br /> ', '&#13;'), array('&nbsp; ', '<br />&nbsp;', "\n"), $this->message);
144
145
		// Finish footnotes if we have any.
146
		if ($this->num_footnotes > 0)
147
		{
148
			$this->handleFootnotes();
149
		}
150
151
		// Allow addons access to what the parser created
152
		$message = $this->message;
153
		call_integration_hook('integrate_post_bbc_parser', array(&$message));
154
		$this->message = $message;
155
156
		return $this->message;
157
	}
158
159
	protected function parse_loop()
160
	{
161
		while ($this->pos !== false)
162
		{
163
			$this->last_pos = isset($this->last_pos) ? max($this->pos, $this->last_pos) : $this->pos;
164
			$this->pos = strpos($this->message, '[', $this->pos + 1);
165
166
			// Failsafe.
167
			if ($this->pos === false || $this->last_pos > $this->pos)
168
			{
169
				$this->pos = strlen($this->message) + 1;
170
			}
171
172
			// Can't have a one letter smiley, URL, or email! (sorry.)
173
			if ($this->last_pos < $this->pos - 1)
174
			{
175
				$this->betweenTags();
176
			}
177
178
			// Are we there yet?  Are we there yet?
179
			if ($this->pos >= strlen($this->message) - 1)
180
			{
181
				return;
182
			}
183
184
			$next_char = strtolower($this->message[$this->pos + 1]);
185
186
			// Possibly a closer?
187
			if ($next_char === '/')
188
			{
189
				if ($this->hasOpenTags())
190
				{
191
					$this->handleOpenTags();
192
				}
193
194
				// We don't allow / to be used for anything but the closing character, so this can't be a tag
195
				continue;
196
			}
197
198
			// No tags for this character, so just keep going (fastest possible course.)
199
			if (!isset($this->bbc_codes[$next_char]))
200
			{
201
				continue;
202
			}
203
204
			$this->inside_tag = !$this->hasOpenTags() ? null : $this->getLastOpenedTag();
205
206
			if ($this->isItemCode($next_char) && isset($this->message[$this->pos + 2]) && $this->message[$this->pos + 2] === ']' && !$this->bbc->isDisabled('list') && !$this->bbc->isDisabled('li'))
207
			{
208
				// Itemcodes cannot be 0 and must be preceeded by a semi-colon, space, tab, new line, or greater than sign
209
				if (!($this->message[$this->pos + 1] === '0' && !in_array($this->message[$this->pos - 1], array(';', ' ', "\t", "\n", '>'))))
210
				{
211
					// Item codes are complicated buggers... they are implicit [li]s and can make [list]s!
212
					$this->handleItemCode();
213
				}
214
215
				// No matter what, we have to continue here.
216
				continue;
217
			}
218
			else
219
			{
220
				$tag = $this->findTag($this->bbc_codes[$next_char]);
221
			}
222
223
			// Implicitly close lists and tables if something other than what's required is in them. This is needed for itemcode.
224
			if ($tag === null && $this->inside_tag !== null && !empty($this->inside_tag[Codes::ATTR_REQUIRE_CHILDREN]))
225
			{
226
				$this->closeOpenedTag();
227
				$tmp = $this->noSmileys($this->inside_tag[Codes::ATTR_AFTER]);
228
				$this->message = substr_replace($this->message, $tmp, $this->pos, 0);
229
				$this->pos += strlen($tmp) - 1;
230
			}
231
232
			// No tag?  Keep looking, then.  Silly people using brackets without actual tags.
233
			if ($tag === null)
234
			{
235
				continue;
236
			}
237
238
			// Propagate the list to the child (so wrapping the disallowed tag won't work either.)
239
			if (isset($this->inside_tag[Codes::ATTR_DISALLOW_CHILDREN]))
240
			{
241
				$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];
242
			}
243
244
			// Is this tag disabled?
245
			if ($this->bbc->isDisabled($tag[Codes::ATTR_TAG]))
246
			{
247
				$this->handleDisabled($tag);
248
			}
249
250
			// The only special case is 'html', which doesn't need to close things.
251
			if ($tag[Codes::ATTR_BLOCK_LEVEL] && $tag[Codes::ATTR_TAG] !== 'html' && !$this->inside_tag[Codes::ATTR_BLOCK_LEVEL])
252
			{
253
				$this->closeNonBlockLevel();
254
			}
255
256
			// This is the part where we actually handle the tags. I know, crazy how long it took.
257
			if ($this->handleTag($tag))
258
			{
259
				continue;
260
			}
261
262
			// If this is block level, eat any breaks after it.
263
			if ($tag[Codes::ATTR_BLOCK_LEVEL] && isset($this->message[$this->pos + 1]) && substr_compare($this->message, '<br />', $this->pos + 1, 6) === 0)
264
			{
265
				$this->message = substr_replace($this->message, '', $this->pos + 1, 6);
266
			}
267
268
			// Are we trimming outside this tag?
269
			if (!empty($tag[Codes::ATTR_TRIM]) && $tag[Codes::ATTR_TRIM] !== Codes::TRIM_OUTSIDE)
270
			{
271
				$this->trimWhiteSpace($this->message, $this->pos + 1);
272
			}
273
		}
274
	}
275
276
	protected function handleOpenTags()
277
	{
278
		// Next closing bracket after the first character
279
		$this->pos2 = strpos($this->message, ']', $this->pos + 1);
280
281
		// Playing games? string = [/]
282
		if ($this->pos2 === $this->pos + 2)
283
		{
284
			return;
285
		}
286
287
		// Get everything between [/ and ]
288
		$look_for = strtolower(substr($this->message, $this->pos + 2, $this->pos2 - $this->pos - 2));
289
		$to_close = array();
290
		$block_level = null;
291
292
		do
293
		{
294
			// Get the last opened tag
295
			$tag = $this->closeOpenedTag();
296
297
			// No open tags
298
			if (!$tag)
299
			{
300
				break;
301
			}
302
303
			if ($tag[Codes::ATTR_BLOCK_LEVEL])
304
			{
305
				// Only find out if we need to.
306
				if ($block_level === false)
307
				{
308
					$this->addOpenTag($tag);
309
					break;
310
				}
311
312
				// The idea is, if we are LOOKING for a block level tag, we can close them on the way.
313
				if (isset($look_for[1]) && isset($this->bbc_codes[$look_for[0]]))
314
				{
315
					foreach ($this->bbc_codes[$look_for[0]] as $temp)
316
					{
317
						if ($temp[Codes::ATTR_TAG] === $look_for)
318
						{
319
							$block_level = $temp[Codes::ATTR_BLOCK_LEVEL];
320
							break;
321
						}
322
					}
323
				}
324
325
				if ($block_level !== true)
326
				{
327
					$block_level = false;
328
					$this->addOpenTag($tag);
329
					break;
330
				}
331
			}
332
333
			$to_close[] = $tag;
334
		} while ($tag[Codes::ATTR_TAG] !== $look_for);
335
336
		// Did we just eat through everything and not find it?
337
		if (!$this->hasOpenTags() && (empty($tag) || $tag[Codes::ATTR_TAG] !== $look_for))
338
		{
339
			$this->open_tags = $to_close;
340
			return;
341
		}
342
		elseif (!empty($to_close) && $tag[Codes::ATTR_TAG] !== $look_for)
343
		{
344
			if ($block_level === null && isset($look_for[0], $this->bbc_codes[$look_for[0]]))
345
			{
346
				foreach ($this->bbc_codes[$look_for[0]] as $temp)
347
				{
348
					if ($temp[Codes::ATTR_TAG] === $look_for)
349
					{
350
						$block_level = !empty($temp[Codes::ATTR_BLOCK_LEVEL]);
351
						break;
352
					}
353
				}
354
			}
355
356
			// We're not looking for a block level tag (or maybe even a tag that exists...)
357
			if (!$block_level)
358
			{
359
				foreach ($to_close as $tag)
360
				{
361
					$this->addOpenTag($tag);
362
				}
363
364
				return;
365
			}
366
		}
367
368
		foreach ($to_close as $tag)
369
		{
370
			$tmp = $this->noSmileys($tag[Codes::ATTR_AFTER]);
371
			$this->message = substr_replace($this->message, $tmp, $this->pos, $this->pos2 + 1 - $this->pos);
372
			$this->pos += strlen($tmp);
373
			$this->pos2 = $this->pos - 1;
374
375
			// See the comment at the end of the big loop - just eating whitespace ;).
376
			if ($tag[Codes::ATTR_BLOCK_LEVEL] && isset($this->message[$this->pos]) && substr_compare($this->message, '<br />', $this->pos, 6) === 0)
377
			{
378
				$this->message = substr_replace($this->message, '', $this->pos, 6);
379
			}
380
381
			// Trim inside whitespace
382
			if (!empty($tag[Codes::ATTR_TRIM]) && $tag[Codes::ATTR_TRIM] !== Codes::TRIM_INSIDE)
383
			{
384
				$this->trimWhiteSpace($this->message, $this->pos + 1);
385
			}
386
		}
387
388
		if (!empty($to_close))
389
		{
390
			$this->pos--;
391
		}
392
	}
393
394
	/**
395
	 * Turn smiley parsing on/off
396
	 * @param bool $toggle
397
	 * @return \BBC\Parser
398
	 */
399
	public function doSmileys($toggle)
400
	{
401
		$this->do_smileys = (bool) $toggle;
402
		return $this;
403
	}
404
405
	/**
406
	 * Check if parsing is enabled
407
	 *
408
	 * @return bool
409
	 */
410
	public function parsingEnabled()
0 ignored issues
show
Coding Style introduced by
parsingEnabled uses the super-global variable $GLOBALS which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
411
	{
412
		return !empty($GLOBALS['modSettings']['enableBBC']);
413
	}
414
415
	public function loadHtmlParser()
416
	{
417
		$parser = new HtmlParser;
418
		call_integration_hook('integrate_bbc_load_html_parser', array(&$parser));
419
		$this->html_parser = $parser;
420
	}
421
422
	/**
423
	 * Parse the HTML in a string
424
	 *
425
	 * @param string &$data
426
	 */
427
	protected function parseHTML(&$data)
428
	{
429
		$this->html_parser->parse($data);
430
	}
431
432
	/**
433
	 * Parse URIs and email addresses in a string to url and email BBC tags to be parsed by the BBC parser
434
	 *
435
	 * @param string &$data
436
	 */
437
	protected function autoLink(&$data)
438
	{
439
		if ($data === '' || $data === $this->smiley_marker || !$this->autolinker->hasPossible())
440
		{
441
			return;
442
		}
443
444
		// Are we inside tags that should be auto linked?
445
		if ($this->hasOpenTags())
446
		{
447
			foreach ($this->getOpenedTags() as $open_tag)
448
			{
449
				if (!$open_tag[Codes::ATTR_AUTOLINK])
450
				{
451
					return;
452
				}
453
			}
454
		}
455
456
		$this->autolinker->parse($data);
457
	}
458
459
	/**
460
	 * Load the autolink regular expression to be used in autoLink()
461
	 */
462 1
	protected function loadAutolink()
463
	{
464 1
		if ($this->autolinker === null)
465 1
		{
466
			$this->autolinker = new Autolink($this->bbc);
467
		}
468 1
	}
469
470
	/**
471
	 * Find if the current character is the start of a tag and get it
472
	 *
473
	 * @param array $possible_codes
474
	 *
475
	 * @return null|array the tag that was found or null if no tag found
476
	 */
477
	protected function findTag(array $possible_codes)
478
	{
479
		$tag = null;
480
		$last_check = null;
481
482
		foreach ($possible_codes as $possible)
483
		{
484
			// Skip tags that didn't match the next X characters
485
			if ($possible[Codes::ATTR_TAG] === $last_check)
486
			{
487
				continue;
488
			}
489
490
			// The character after the possible tag or nothing
491
			$next_c = isset($this->message[$this->pos + 1 + $possible[Codes::ATTR_LENGTH]]) ? $this->message[$this->pos + 1 + $possible[Codes::ATTR_LENGTH]] : '';
492
493
			// This only happens if the tag is the last character of the string
494
			if ($next_c === '')
495
			{
496
				break;
497
			}
498
499
			// The next character must be one of these or it's not a tag
500
			if ($next_c !== ' ' && $next_c !== ']' && $next_c !== '=' && $next_c !== '/')
501
			{
502
				$last_check = $possible[Codes::ATTR_TAG];
503
				continue;
504
			}
505
506
			// Not a match?
507
			if (substr_compare($this->message, $possible[Codes::ATTR_TAG], $this->pos + 1, $possible[Codes::ATTR_LENGTH], true) !== 0)
508
			{
509
				$last_check = $possible[Codes::ATTR_TAG];
510
				continue;
511
			}
512
513
			$tag = $this->checkCodeAttributes($next_c, $possible, $tag);
0 ignored issues
show
Unused Code introduced by
The call to BBCParser::checkCodeAttributes() has too many arguments starting with $tag.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
514
			if ($tag === null)
515
			{
516
				continue;
517
			}
518
519
			// Quotes can have alternate styling, we do this php-side due to all the permutations of quotes.
520
			if ($tag[Codes::ATTR_TAG] === 'quote')
521
			{
522
				$this->alternateQuoteStyle($tag);
523
			}
524
525
			break;
526
		}
527
528
		// If there is a code that says you can't cache, the message can't be cached
529
		if ($tag !== null && $this->can_cache !== false)
530
		{
531
			$this->can_cache = empty($tag[Codes::ATTR_NO_CACHE]);
532
		}
533
534
		if ($tag[Codes::ATTR_TAG] === 'footnote')
535
		{
536
			$this->num_footnotes++;
537
		}
538
539
		return $tag;
540
	}
541
542
	/**
543
	 * @param array $tag
544
	 */
545
	protected function alternateQuoteStyle(array &$tag)
546
	{
547
		// Start with standard
548
		$quote_alt = false;
549
		foreach ($this->open_tags as $open_quote)
550
		{
551
			// Every parent quote this quote has flips the styling
552
			if ($open_quote[Codes::ATTR_TAG] === 'quote')
553
			{
554
				$quote_alt = !$quote_alt;
555
			}
556
		}
557
		// Add a class to the quote to style alternating blockquotes
558
		// @todo - Frankly it makes little sense to allow alternate blockquote
559
		// styling without also catering for alternate quoteheader styling.
560
		// I do remember coding that some time back, but it seems to have gotten
561
		// lost somewhere in the Elk processes.
562
		// Come to think of it, it may be better to append a second class rather
563
		// than alter the standard one.
564
		//  - Example: class="bbc_quote" and class="bbc_quote alt_quote".
0 ignored issues
show
Unused Code Comprehensibility introduced by
40% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
565
		// This would mean simpler CSS for themes (like default) which do not use the alternate styling,
566
		// but would still allow it for themes that want it.
567
		$tag[Codes::ATTR_BEFORE] = str_replace('<blockquote>', '<blockquote class="bbc_' . ($quote_alt ? 'alternate' : 'standard') . '_quote">', $tag[Codes::ATTR_BEFORE]);
568
	}
569
570
	/**
571
	 * @param $next_c
572
	 * @param array $possible
573
	 * @return array|void
574
	 */
575
	protected function checkCodeAttributes($next_c, array $possible)
576
	{
577
		// Do we want parameters?
578
		if (!empty($possible[Codes::ATTR_PARAM]))
579
		{
580
			if ($next_c !== ' ')
581
			{
582
				return;
583
			}
584
		}
585
		// parsed_content demands an immediate ] without parameters!
586
		elseif ($possible[Codes::ATTR_TYPE] === Codes::TYPE_PARSED_CONTENT)
587
		{
588
			if ($next_c !== ']')
589
			{
590
				return;
591
			}
592
		}
593
		else
594
		{
595
			// Do we need an equal sign?
596
			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)))
597
			{
598
				return;
599
			}
600
601
			if ($next_c !== ']')
602
			{
603
				// An immediate ]?
604
				if ($possible[Codes::ATTR_TYPE] === Codes::TYPE_UNPARSED_CONTENT)
605
				{
606
					return;
607
				}
608
				// Maybe we just want a /...
609
				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)
610
				{
611
					return;
612
				}
613
			}
614
		}
615
616
617
		// Check allowed tree?
618
		if (isset($possible[Codes::ATTR_REQUIRE_PARENTS]) && ($this->inside_tag === null || !isset($possible[Codes::ATTR_REQUIRE_PARENTS][$this->inside_tag[Codes::ATTR_TAG]])))
619
		{
620
			return;
621
		}
622
623
		if ($this->inside_tag !== null)
624
		{
625
			if (isset($this->inside_tag[Codes::ATTR_REQUIRE_CHILDREN]) && !isset($this->inside_tag[Codes::ATTR_REQUIRE_CHILDREN][$possible[Codes::ATTR_TAG]]))
626
			{
627
				return;
628
			}
629
630
			// If this is in the list of disallowed child tags, don't parse it.
631
			if (isset($this->inside_tag[Codes::ATTR_DISALLOW_CHILDREN]) && isset($this->inside_tag[Codes::ATTR_DISALLOW_CHILDREN][$possible[Codes::ATTR_TAG]]))
632
			{
633
				return;
634
			}
635
636
			// Not allowed in this parent, replace the tags or show it like regular text
637
			if (isset($possible[Codes::ATTR_DISALLOW_PARENTS]) && isset($possible[Codes::ATTR_DISALLOW_PARENTS][$this->inside_tag[Codes::ATTR_TAG]]))
638
			{
639
				if (!isset($possible[Codes::ATTR_DISALLOW_BEFORE], $possible[Codes::ATTR_DISALLOW_AFTER]))
640
				{
641
					return;
642
				}
643
644
				$possible[Codes::ATTR_BEFORE] = isset($possible[Codes::ATTR_DISALLOW_BEFORE]) ? $possible[Codes::ATTR_DISALLOW_BEFORE] : $possible[Codes::ATTR_BEFORE];
645
				$possible[Codes::ATTR_AFTER] = isset($possible[Codes::ATTR_DISALLOW_AFTER]) ? $possible[Codes::ATTR_DISALLOW_AFTER] : $possible[Codes::ATTR_AFTER];
646
			}
647
		}
648
649
		if (isset($possible[Codes::ATTR_TEST]) && $this->handleTest($possible))
650
		{
651
			return;
652
		}
653
654
		// +1 for [, then the length of the tag, then a space
655
		$this->pos1 = $this->pos + 1 + $possible[Codes::ATTR_LENGTH] + 1;
656
657
		// This is long, but it makes things much easier and cleaner.
658
		if (!empty($possible[Codes::ATTR_PARAM]))
659
		{
660
			$match = $this->matchParameters($possible, $matches);
661
662
			// Didn't match our parameter list, try the next possible.
663
			if (!$match)
664
			{
665
				return;
666
			}
667
668
			return $this->setupTagParameters($possible, $matches);
669
		}
670
671
		return $possible;
672
	}
673
674
	protected function handleTest(array $possible)
675
	{
676
		return preg_match('~^' . $possible[Codes::ATTR_TEST] . '~', substr($this->message, $this->pos + 2 + $possible[Codes::ATTR_LENGTH], strpos($this->message, ']', $this->pos) - ($this->pos + 2 + $possible[Codes::ATTR_LENGTH]))) === 0;
677
	}
678
679
	protected function handleItemCode()
680
	{
681
		$tag = $this->item_codes[$this->message[$this->pos + 1]];
682
683
		// First let's set up the tree: it needs to be in a list, or after an li.
684
		if ($this->inside_tag === null || ($this->inside_tag[Codes::ATTR_TAG] !== 'list' && $this->inside_tag[Codes::ATTR_TAG] !== 'li'))
685
		{
686
			$this->addOpenTag(array(
687
				Codes::ATTR_TAG => 'list',
688
				Codes::ATTR_TYPE => Codes::TYPE_PARSED_CONTENT,
689
				Codes::ATTR_AFTER => '</ul>',
690
				Codes::ATTR_BLOCK_LEVEL => true,
691
				Codes::ATTR_REQUIRE_CHILDREN => array('li' => 'li'),
692
				Codes::ATTR_DISALLOW_CHILDREN => isset($this->inside_tag[Codes::ATTR_DISALLOW_CHILDREN]) ? $this->inside_tag[Codes::ATTR_DISALLOW_CHILDREN] : null,
693
				Codes::ATTR_LENGTH => 4,
694
				Codes::ATTR_AUTOLINK => true,
695
			));
696
			$code = '<ul' . ($tag === '' ? '' : ' style="list-style-type: ' . $tag . '"') . ' class="bbc_list">';
697
		}
698
		// We're in a list item already: another itemcode?  Close it first.
699
		elseif ($this->inside_tag[Codes::ATTR_TAG] === 'li')
700
		{
701
			$this->closeOpenedTag();
702
			$code = '</li>';
703
		}
704
		else
705
		{
706
			$code = '';
707
		}
708
709
		// Now we open a new tag.
710
		$this->addOpenTag(array(
711
			Codes::ATTR_TAG => 'li',
712
			Codes::ATTR_TYPE => Codes::TYPE_PARSED_CONTENT,
713
			Codes::ATTR_AFTER => '</li>',
714
			Codes::ATTR_TRIM => Codes::TRIM_OUTSIDE,
715
			Codes::ATTR_BLOCK_LEVEL => true,
716
			Codes::ATTR_DISALLOW_CHILDREN => isset($this->inside_tag[Codes::ATTR_DISALLOW_CHILDREN]) ? $this->inside_tag[Codes::ATTR_DISALLOW_CHILDREN] : null,
717
			Codes::ATTR_AUTOLINK => true,
718
			Codes::ATTR_LENGTH => 2,
719
		));
720
721
		// First, open the tag...
722
		$code .= '<li>';
723
724
		$tmp = $this->noSmileys($code);
725
		$this->message = substr_replace($this->message, $tmp, $this->pos, 3);
726
		$this->pos += strlen($tmp) - 1;
727
728
		// Next, find the next break (if any.)  If there's more itemcode after it, keep it going - otherwise close!
729
		$this->pos2 = strpos($this->message, '<br />', $this->pos);
730
		$this->pos3 = strpos($this->message, '[/', $this->pos);
731
732
		$num_open_tags = count($this->open_tags);
733
		if ($this->pos2 !== false && ($this->pos3 === false || $this->pos2 <= $this->pos3))
734
		{
735
			// Can't use offset because of the ^
736
			preg_match('~^(<br />|&nbsp;|\s|\[)+~', substr($this->message, $this->pos2 + 6), $matches);
737
			//preg_match('~(<br />|&nbsp;|\s|\[)+~', $this->message, $matches, 0, $this->pos2 + 6);
0 ignored issues
show
Unused Code Comprehensibility introduced by
60% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
738
739
			// Keep the list open if the next character after the break is a [. Otherwise, close it.
740
			$replacement = !empty($matches[0]) && substr_compare($matches[0], '[', -1, 1) === 0 ? '[/li]' : '[/li][/list]';
741
742
			$this->message = substr_replace($this->message, $replacement, $this->pos2, 0);
743
			$this->open_tags[$num_open_tags - 2][Codes::ATTR_AFTER] = '</ul>';
744
		}
745
		// Tell the [list] that it needs to close specially.
746
		else
747
		{
748
			// Move the li over, because we're not sure what we'll hit.
749
			$this->open_tags[$num_open_tags - 1][Codes::ATTR_AFTER] = '';
750
			$this->open_tags[$num_open_tags - 2][Codes::ATTR_AFTER] = '</li></ul>';
751
		}
752
	}
753
754
	/**
755
	 * Handle codes that are of the parsed context type
756
	 * @param array $tag
757
	 *
758
	 * @return bool
759
	 */
760
	protected function handleTypeParsedContext(array $tag)
761
	{
762
		// @todo Check for end tag first, so people can say "I like that [i] tag"?
763
		$this->addOpenTag($tag);
764
		$tmp = $this->noSmileys($tag[Codes::ATTR_BEFORE]);
765
		$this->message = substr_replace($this->message, $tmp, $this->pos, $this->pos1 - $this->pos);
766
		$this->pos += strlen($tmp) - 1;
767
768
		return false;
769
	}
770
771
	/**
772
	 * Handle codes that are of the unparsed context type
773
	 * @param array $tag
774
	 *
775
	 * @return bool
776
	 */
777
	protected function handleTypeUnparsedContext(array $tag)
778
	{
779
		// Find the next closer
780
		$this->pos2 = stripos($this->message, '[/' . $tag[Codes::ATTR_TAG] . ']', $this->pos1);
781
782
		// No closer
783
		if ($this->pos2 === false)
784
		{
785
			return true;
786
		}
787
788
		$data = substr($this->message, $this->pos1, $this->pos2 - $this->pos1);
789
790
		if (!empty($tag[Codes::ATTR_BLOCK_LEVEL]) && isset($data[0]) && substr_compare($data, '<br />', 0, 6) === 0)
791
		{
792
			$data = substr($data, 6);
793
		}
794
795
		if (isset($tag[Codes::ATTR_VALIDATE]))
796
		{
797
			//$tag[Codes::ATTR_VALIDATE]($tag, $data, $this->bbc->getDisabled());
0 ignored issues
show
Unused Code Comprehensibility introduced by
73% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
798
			$this->filterData($tag, $data);
799
		}
800
801
		$code = strtr($tag[Codes::ATTR_CONTENT], array('$1' => $data));
802
		$tmp = $this->noSmileys($code);
803
		$this->message = substr_replace($this->message, $tmp, $this->pos, $this->pos2 + 3 + $tag[Codes::ATTR_LENGTH] - $this->pos);
804
		$this->pos += strlen($tmp) - 1;
805
		$this->last_pos = $this->pos + 1;
806
807
		return false;
808
	}
809
810
	/**
811
	 * Handle codes that are of the unparsed equals context type
812
	 * @param array $tag
813
	 *
814
	 * @return bool
815
	 */
816
	protected function handleUnparsedEqualsContext(array $tag)
817
	{
818
		// The value may be quoted for some tags - check.
819
		if (isset($tag[Codes::ATTR_QUOTED]))
820
		{
821
			$quoted = substr_compare($this->message, '&quot;', $this->pos1, 6) === 0;
822
			if ($tag[Codes::ATTR_QUOTED] !== Codes::OPTIONAL && !$quoted)
823
			{
824
				return true;
825
			}
826
827
			if ($quoted)
828
			{
829
				$this->pos1 += 6;
830
			}
831
		}
832
		else
833
		{
834
			$quoted = false;
835
		}
836
837
		$this->pos2 = strpos($this->message, $quoted === false ? ']' : '&quot;]', $this->pos1);
838
		if ($this->pos2 === false)
839
		{
840
			return true;
841
		}
842
843
		$this->pos3 = stripos($this->message, '[/' . $tag[Codes::ATTR_TAG] . ']', $this->pos2);
844
		if ($this->pos3 === false)
845
		{
846
			return true;
847
		}
848
849
		$data = array(
850
			substr($this->message, $this->pos2 + ($quoted === false ? 1 : 7), $this->pos3 - ($this->pos2 + ($quoted === false ? 1 : 7))),
851
			substr($this->message, $this->pos1, $this->pos2 - $this->pos1)
852
		);
853
854
		if (!empty($tag[Codes::ATTR_BLOCK_LEVEL]) && substr_compare($data[0], '<br />', 0, 6) === 0)
855
		{
856
			$data[0] = substr($data[0], 6);
857
		}
858
859
		// Validation for my parking, please!
860
		if (isset($tag[Codes::ATTR_VALIDATE]))
861
		{
862
			//$tag[Codes::ATTR_VALIDATE]($tag, $data, $this->bbc->getDisabled());
0 ignored issues
show
Unused Code Comprehensibility introduced by
73% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
863
			$this->filterData($tag, $data);
864
		}
865
866
		$code = strtr($tag[Codes::ATTR_CONTENT], array('$1' => $data[0], '$2' => $data[1]));
867
		$tmp = $this->noSmileys($code);
868
		$this->message = substr_replace($this->message, $tmp, $this->pos, $this->pos3 + 3 + $tag[Codes::ATTR_LENGTH] - $this->pos);
869
		$this->pos += strlen($tmp) - 1;
870
871
		return false;
872
	}
873
874
	/**
875
	 * Handle codes that are of the closed type
876
	 * @param array $tag
877
	 *
878
	 * @return bool
879
	 */
880
	protected function handleTypeClosed(array $tag)
881
	{
882
		$this->pos2 = strpos($this->message, ']', $this->pos);
883
		$tmp = $this->noSmileys($tag[Codes::ATTR_CONTENT]);
884
		$this->message = substr_replace($this->message, $tmp, $this->pos, $this->pos2 + 1 - $this->pos);
885
		$this->pos += strlen($tmp) - 1;
886
887
		return false;
888
	}
889
890
	/**
891
	 * Handle codes that are of the unparsed commas context type
892
	 * @param array $tag
893
	 *
894
	 * @return bool
895
	 */
896
	protected function handleUnparsedCommasContext(array $tag)
897
	{
898
		$this->pos2 = strpos($this->message, ']', $this->pos1);
899
		if ($this->pos2 === false)
900
		{
901
			return true;
902
		}
903
904
		$this->pos3 = stripos($this->message, '[/' . $tag[Codes::ATTR_TAG] . ']', $this->pos2);
905
		if ($this->pos3 === false)
906
		{
907
			return true;
908
		}
909
910
		// We want $1 to be the content, and the rest to be csv.
911
		$data = explode(',', ',' . substr($this->message, $this->pos1, $this->pos2 - $this->pos1));
912
		$data[0] = substr($this->message, $this->pos2 + 1, $this->pos3 - $this->pos2 - 1);
913
914
		if (isset($tag[Codes::ATTR_VALIDATE]))
915
		{
916
			//$tag[Codes::ATTR_VALIDATE]($tag, $data, $this->bbc->getDisabled());
0 ignored issues
show
Unused Code Comprehensibility introduced by
73% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
917
			$this->filterData($tag, $data);
918
		}
919
920
		$code = $tag[Codes::ATTR_CONTENT];
921
		foreach ($data as $k => $d)
922
		{
923
			$code = strtr($code, array('$' . ($k + 1) => trim($d)));
924
		}
925
926
		$tmp = $this->noSmileys($code);
927
		$this->message = substr_replace($this->message, $tmp, $this->pos, $this->pos3 + 3 + $tag[Codes::ATTR_LENGTH] - $this->pos);
928
		$this->pos += strlen($tmp) - 1;
929
930
		return false;
931
	}
932
933
	/**
934
	 * Handle codes that are of the unparsed commas type
935
	 * @param array $tag
936
	 *
937
	 * @return bool
938
	 */
939
	protected function handleUnparsedCommas(array $tag)
940
	{
941
		$this->pos2 = strpos($this->message, ']', $this->pos1);
942
		if ($this->pos2 === false)
943
		{
944
			return true;
945
		}
946
947
		$data = explode(',', substr($this->message, $this->pos1, $this->pos2 - $this->pos1));
948
949
		if (isset($tag[Codes::ATTR_VALIDATE]))
950
		{
951
			//$tag[Codes::ATTR_VALIDATE]($tag, $data, $this->bbc->getDisabled());
0 ignored issues
show
Unused Code Comprehensibility introduced by
73% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
952
			$this->filterData($tag, $data);
953
		}
954
955
		// Fix after, for disabled code mainly.
956
		foreach ($data as $k => $d)
957
		{
958
			$tag[Codes::ATTR_AFTER] = strtr($tag[Codes::ATTR_AFTER], array('$' . ($k + 1) => trim($d)));
959
		}
960
961
		$this->addOpenTag($tag);
962
963
		// Replace them out, $1, $2, $3, $4, etc.
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
964
		$code = $tag[Codes::ATTR_BEFORE];
965
		foreach ($data as $k => $d)
966
		{
967
			$code = strtr($code, array('$' . ($k + 1) => trim($d)));
968
		}
969
970
		$tmp = $this->noSmileys($code);
971
		$this->message = substr_replace($this->message, $tmp, $this->pos, $this->pos2 + 1 - $this->pos);
972
		$this->pos += strlen($tmp) - 1;
973
974
		return false;
975
	}
976
977
	/**
978
	 * Handle codes that are of the equals type
979
	 * @param array $tag
980
	 *
981
	 * @return bool
982
	 */
983
	protected function handleEquals(array $tag)
984
	{
985
		// The value may be quoted for some tags - check.
986
		if (isset($tag[Codes::ATTR_QUOTED]))
987
		{
988
			$quoted = substr_compare($this->message, '&quot;', $this->pos1, 6) === 0;
989
			if ($tag[Codes::ATTR_QUOTED] !== Codes::OPTIONAL && !$quoted)
990
			{
991
				return true;
992
			}
993
994
			if ($quoted)
995
			{
996
				$this->pos1 += 6;
997
			}
998
		}
999
		else
1000
		{
1001
			$quoted = false;
1002
		}
1003
1004
		$this->pos2 = strpos($this->message, $quoted === false ? ']' : '&quot;]', $this->pos1);
1005
		if ($this->pos2 === false)
1006
		{
1007
			return true;
1008
		}
1009
1010
		$data = substr($this->message, $this->pos1, $this->pos2 - $this->pos1);
1011
1012
		// Validation for my parking, please!
1013
		if (isset($tag[Codes::ATTR_VALIDATE]))
1014
		{
1015
			//$tag[Codes::ATTR_VALIDATE]($tag, $data, $this->bbc->getDisabled());
0 ignored issues
show
Unused Code Comprehensibility introduced by
73% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1016
			$this->filterData($tag, $data);
1017
		}
1018
1019
		// For parsed content, we must recurse to avoid security problems.
1020
		if ($tag[Codes::ATTR_TYPE] === Codes::TYPE_PARSED_EQUALS)
1021
		{
1022
			$this->recursiveParser($data, $tag);
1023
		}
1024
1025
		$tag[Codes::ATTR_AFTER] = strtr($tag[Codes::ATTR_AFTER], array('$1' => $data));
1026
1027
		$this->addOpenTag($tag);
1028
1029
		$code = strtr($tag[Codes::ATTR_BEFORE], array('$1' => $data));
1030
		$tmp = $this->noSmileys($code);
1031
		$this->message = substr_replace($this->message, $tmp, $this->pos, $this->pos2 + ($quoted === false ? 1 : 7) - $this->pos);
1032
		$this->pos += strlen($tmp) - 1;
1033
1034
		return false;
1035
	}
1036
1037
	/**
1038
	 * Handles a tag by its type. Offloads the actual handling to handle*() method
1039
	 * @param array $tag
1040
	 *
1041
	 * @return bool true if there was something wrong and the parser should advance
1042
	 */
1043
	protected function handleTag(array $tag)
1044
	{
1045
		switch ($tag[Codes::ATTR_TYPE])
1046
		{
1047
			case Codes::TYPE_PARSED_CONTENT:
1048
				return $this->handleTypeParsedContext($tag);
1049
1050
			// Don't parse the content, just skip it.
1051
			case Codes::TYPE_UNPARSED_CONTENT:
1052
				return $this->handleTypeUnparsedContext($tag);
1053
1054
			// Don't parse the content, just skip it.
1055
			case Codes::TYPE_UNPARSED_EQUALS_CONTENT:
1056
				return $this->handleUnparsedEqualsContext($tag);
1057
1058
			// A closed tag, with no content or value.
1059
			case Codes::TYPE_CLOSED:
1060
				return $this->handleTypeClosed($tag);
1061
1062
			// This one is sorta ugly... :/
1063
			case Codes::TYPE_UNPARSED_COMMAS_CONTENT:
1064
				return $this->handleUnparsedCommasContext($tag);
1065
1066
			// This has parsed content, and a csv value which is unparsed.
1067
			case Codes::TYPE_UNPARSED_COMMAS:
1068
				return $this->handleUnparsedCommas($tag);
1069
1070
			// A tag set to a value, parsed or not.
1071
			case Codes::TYPE_PARSED_EQUALS:
1072
			case Codes::TYPE_UNPARSED_EQUALS:
1073
				return $this->handleEquals($tag);
1074
		}
1075
1076
		return false;
1077
	}
1078
1079
	// @todo I don't know what else to call this. It's the area that isn't a tag.
1080
	protected function betweenTags()
0 ignored issues
show
Coding Style introduced by
betweenTags uses the super-global variable $GLOBALS which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
1081
	{
1082
		// Make sure the $this->last_pos is not negative.
1083
		$this->last_pos = max($this->last_pos, 0);
1084
1085
		// Pick a block of data to do some raw fixing on.
1086
		$data = substr($this->message, $this->last_pos, $this->pos - $this->last_pos);
1087
1088
		// This happens when the pos is > last_pos and there is a trailing \n from one of the tags having "AFTER"
1089
		// In micro-optimization tests, using substr() here doesn't prove to be slower. This is much easier to read so leave it.
1090
		if ($data === $this->smiley_marker)
1091
		{
1092
			return;
1093
		}
1094
1095
		// Take care of some HTML!
1096
		if ($this->possible_html && strpos($data, '&lt;') !== false)
1097
		{
1098
			// @todo new \Parser\BBC\HTML;
1099
			$this->parseHTML($data);
1100
		}
1101
1102
		if (!empty($GLOBALS['modSettings']['autoLinkUrls']))
1103
		{
1104
			$this->autoLink($data);
1105
		}
1106
1107
		// This cannot be moved earlier. It breaks tests
1108
		$data = str_replace("\t", '&nbsp;&nbsp;&nbsp;', $data);
1109
1110
		// If it wasn't changed, no copying or other boring stuff has to happen!
1111
		if (substr_compare($this->message, $data, $this->last_pos, $this->pos - $this->last_pos))
1112
		{
1113
			$this->message = substr_replace($this->message, $data, $this->last_pos, $this->pos - $this->last_pos);
1114
1115
			// Since we changed it, look again in case we added or removed a tag.  But we don't want to skip any.
1116
			$old_pos = strlen($data) + $this->last_pos;
1117
			$this->pos = strpos($this->message, '[', $this->last_pos);
1118
			$this->pos = $this->pos === false ? $old_pos : min($this->pos, $old_pos);
1119
		}
1120
	}
1121
1122
	protected function handleFootnotes()
1123
	{
1124
		global $fn_num, $fn_content, $fn_count;
1125
		static $fn_total;
1126
1127
		// @todo temporary until we have nesting
1128
		$this->message = str_replace(array('[footnote]', '[/footnote]'), '', $this->message);
1129
1130
		$fn_num = 0;
1131
		$fn_content = array();
1132
		$fn_count = isset($fn_total) ? $fn_total : 0;
1133
1134
		// Replace our footnote text with a [1] link, save the text for use at the end of the message
1135
		$this->message = preg_replace_callback('~(%fn%(.*?)%fn%)~is', array($this, 'footnoteCallback'), $this->message);
1136
		$fn_total += $fn_num;
1137
1138
		// If we have footnotes, add them in at the end of the message
1139
		if (!empty($fn_num))
1140
		{
1141
			$this->message .= '<div class="bbc_footnotes">' . implode('', $fn_content) . '</div>';
1142
		}
1143
	}
1144
1145
	/**
1146
	 * @param array $matches
1147
	 * @return string
1148
	 */
1149
	protected function footnoteCallback(array $matches)
1150
	{
1151
		global $fn_num, $fn_content, $fn_count;
1152
1153
		$fn_num++;
1154
		$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>';
1155
1156
		return '<a class="target" href="#fn' . $fn_num . '_' . $fn_count . '" id="ref' . $fn_num . '_' . $fn_count . '">[' . $fn_num . ']</a>';
1157
	}
1158
1159
	/**
1160
	 * Parse a tag that is disabled
1161
	 * @param array $tag
1162
	 */
1163
	protected function handleDisabled(array &$tag)
1164
	{
1165
		if (!isset($tag[Codes::ATTR_DISABLED_BEFORE]) && !isset($tag[Codes::ATTR_DISABLED_AFTER]) && !isset($tag[Codes::ATTR_DISABLED_CONTENT]))
1166
		{
1167
			$tag[Codes::ATTR_BEFORE] = !empty($tag[Codes::ATTR_BLOCK_LEVEL]) ? '<div>' : '';
1168
			$tag[Codes::ATTR_AFTER] = !empty($tag[Codes::ATTR_BLOCK_LEVEL]) ? '</div>' : '';
1169
			$tag[Codes::ATTR_CONTENT] = $tag[Codes::ATTR_TYPE] === Codes::TYPE_CLOSED ? '' : (!empty($tag[Codes::ATTR_BLOCK_LEVEL]) ? '<div>$1</div>' : '$1');
1170
		}
1171
		elseif (isset($tag[Codes::ATTR_DISABLED_BEFORE]) || isset($tag[Codes::ATTR_DISABLED_AFTER]))
1172
		{
1173
			$tag[Codes::ATTR_BEFORE] = isset($tag[Codes::ATTR_DISABLED_BEFORE]) ? $tag[Codes::ATTR_DISABLED_BEFORE] : (!empty($tag[Codes::ATTR_BLOCK_LEVEL]) ? '<div>' : '');
1174
			$tag[Codes::ATTR_AFTER] = isset($tag[Codes::ATTR_DISABLED_AFTER]) ? $tag[Codes::ATTR_DISABLED_AFTER] : (!empty($tag[Codes::ATTR_BLOCK_LEVEL]) ? '</div>' : '');
1175
		}
1176
		else
1177
		{
1178
			$tag[Codes::ATTR_CONTENT] = $tag[Codes::ATTR_DISABLED_CONTENT];
1179
		}
1180
	}
1181
1182
	/**
1183
	 * @param array &$possible
1184
	 * @param array &$matches
1185
	 * @return bool
1186
	 */
1187
	protected function matchParameters(array &$possible, &$matches)
1188
	{
1189
		if (!isset($possible['regex_cache']))
1190
		{
1191
			$possible['regex_cache'] = array();
1192
			foreach ($possible[Codes::ATTR_PARAM] as $p => $info)
0 ignored issues
show
Bug introduced by
The expression $possible[\BBC\Codes::ATTR_PARAM] of type null|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1193
			{
1194
				$quote = empty($info[Codes::PARAM_ATTR_QUOTED]) ? '' : '&quot;';
1195
1196
				$possible['regex_cache'][] = '(\s+' . $p . '=' . $quote . (isset($info[Codes::PARAM_ATTR_MATCH]) ? $info[Codes::PARAM_ATTR_MATCH] : '(.+?)') . $quote . ')' . (empty($info[Codes::PARAM_ATTR_OPTIONAL]) ? '' : '?');
1197
			}
1198
			$possible['regex_size'] = count($possible['regex_cache']) - 1;
1199
			$possible['regex_keys'] = range(0, $possible['regex_size']);
1200
		}
1201
1202
		// Okay, this may look ugly and it is, but it's not going to happen much and it is the best way
1203
		// of allowing any order of parameters but still parsing them right.
1204
		$message_stub = substr($this->message, $this->pos1 - 1);
1205
1206
		// If an addon adds many parameters we can exceed max_execution time, lets prevent that
1207
		// 5040 = 7, 40,320 = 8, (N!) etc
0 ignored issues
show
Unused Code Comprehensibility introduced by
48% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1208
		$max_iterations = self::MAX_PERMUTE_ITERATIONS;
1209
1210
		// Use the same range to start each time. Most BBC is in the order that it should be in when it starts.
1211
		$keys = $possible['regex_keys'];
1212
1213
		// Step, one by one, through all possible permutations of the parameters until we have a match
1214
		do
1215
		{
1216
			$match_preg = '~^';
1217
			foreach ($keys as $key)
1218
			{
1219
				$match_preg .= $possible['regex_cache'][$key];
1220
			}
1221
			$match_preg .= '\]~i';
1222
1223
			// Check if this combination of parameters matches the user input
1224
			$match = preg_match($match_preg, $message_stub, $matches) !== 0;
1225
		} while (!$match && --$max_iterations && ($keys = pc_next_permutation($keys, $possible['regex_size'])));
1226
1227
		return $match;
1228
	}
1229
1230
	/**
1231
	 * Recursively call the parser with a new Codes object
1232
	 * This allows to parse BBC in parameters like [quote author="[url]www.quotes.com[/url]"]Something famous.[/quote]
1233
	 *
1234
	 * @param string &$data
1235
	 * @param array $tag
1236
	 */
1237
	protected function recursiveParser(&$data, array $tag)
1238
	{
1239
		// @todo if parsed tags allowed is empty, return?
1240
		$bbc = clone $this->bbc;
1241
1242
		if (!empty($tag[Codes::ATTR_PARSED_TAGS_ALLOWED]))
1243
		{
1244
			$bbc->setParsedTags($tag[Codes::ATTR_PARSED_TAGS_ALLOWED]);
1245
		}
1246
1247
		// Do not use $this->autolinker. For some reason it causes a recursive loop
1248
		$autolinker = null;
1249
		$html = null;
1250
		call_integration_hook('integrate_recursive_bbc_parser', array(&$autolinker, &$html));
1251
1252
		$parser = new \BBC\Parser($bbc, $autolinker, $html);
1253
		$data = $parser->enableSmileys(empty($tag[Codes::ATTR_PARSED_TAGS_ALLOWED]))->parse($data);
1254
	}
1255
1256
	/**
1257
	 * @return array
1258
	 */
1259
	public function getBBC()
1260
	{
1261
		return $this->bbc_codes;
1262
	}
1263
1264
	/**
1265
	 * Enable the parsing of smileys
1266
	 * @param bool|true $enable
1267
	 *
1268
	 * @return $this
1269
	 */
1270
	public function enableSmileys($enable = true)
1271
	{
1272
		$this->do_smileys = (bool) $enable;
1273
		return $this;
1274
	}
1275
1276
	/**
1277
	 * Open a tag
1278
	 * @param array $tag
1279
	 */
1280
	protected function addOpenTag(array $tag)
1281
	{
1282
		$this->open_tags[] = $tag;
1283
	}
1284
1285
	/**
1286
	 * @param string|false $tag = false False closes the last open tag. Anything else finds that tag LIFO
1287
	 *
1288
	 * @return mixed
1289
	 */
1290
	protected function closeOpenedTag($tag = false)
1291
	{
1292
		if ($tag === false)
1293
		{
1294
			return array_pop($this->open_tags);
1295
		}
1296
		elseif (isset($this->open_tags[$tag]))
1297
		{
1298
			$return = $this->open_tags[$tag];
1299
			unset($this->open_tags[$tag]);
1300
			return $return;
1301
		}
1302
	}
1303
1304
	/**
1305
	 * Check if there are any tags that are open
1306
	 * @return bool
1307
	 */
1308
	protected function hasOpenTags()
1309
	{
1310
		return !empty($this->open_tags);
1311
	}
1312
1313
	/**
1314
	 * Get the last opened tag
1315
	 * @return array
1316
	 */
1317
	protected function getLastOpenedTag()
1318
	{
1319
		return end($this->open_tags);
1320
	}
1321
1322
	/**
1323
	 * Get the currently opened tags
1324
	 * @param bool|false $tags_only True if you want just the tag or false for the whole code
1325
	 *
1326
	 * @return array
1327
	 */
1328
	protected function getOpenedTags($tags_only = false)
1329
	{
1330
		if (!$tags_only)
1331
		{
1332
			return $this->open_tags;
1333
		}
1334
1335
		$tags = array();
1336
		foreach ($this->open_tags as $tag)
1337
		{
1338
			$tags[] = $tag[Codes::ATTR_TAG];
1339
		}
1340
		return $tags;
1341
	}
1342
1343
	/**
1344
	 * @param string &$message
1345
	 * @param null|int $offset = null
1346
	 */
1347
	protected function trimWhiteSpace(&$message, $offset = null)
0 ignored issues
show
Unused Code introduced by
The parameter $message is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1348
	{
1349
		if (preg_match('~(<br />|&nbsp;|\s)*~', $this->message, $matches, null, $offset) !== 0 && isset($matches[0]) && $matches[0] !== '')
1350
		{
1351
			$this->message = substr_replace($this->message, '', $this->pos, strlen($matches[0]));
1352
		}
1353
	}
1354
1355
	/**
1356
	 * @param array $possible
1357
	 * @param array $matches
1358
	 *
1359
	 * @return array
1360
	 */
1361
	protected function setupTagParameters(array $possible, array $matches)
1362
	{
1363
		$params = array();
1364
		for ($i = 1, $n = count($matches); $i < $n; $i += 2)
1365
		{
1366
			$key = strtok(ltrim($matches[$i]), '=');
1367
1368
			if (isset($possible[Codes::ATTR_PARAM][$key][Codes::PARAM_ATTR_VALUE]))
1369
			{
1370
				$params['{' . $key . '}'] = strtr($possible[Codes::ATTR_PARAM][$key][Codes::PARAM_ATTR_VALUE], array('$1' => $matches[$i + 1]));
1371
			}
1372
			elseif (isset($possible[Codes::ATTR_PARAM][$key][Codes::ATTR_VALIDATE]))
1373
			{
1374
				$params['{' . $key . '}'] = $possible[Codes::ATTR_PARAM][$key][Codes::ATTR_VALIDATE]($matches[$i + 1]);
1375
			}
1376
			else
1377
			{
1378
				$params['{' . $key . '}'] = $matches[$i + 1];
1379
			}
1380
1381
			// Just to make sure: replace any $ or { so they can't interpolate wrongly.
1382
			$params['{' . $key . '}'] = str_replace(array('$', '{'), array('&#036;', '&#123;'), $params['{' . $key . '}']);
1383
		}
1384
1385
		foreach ($possible[Codes::ATTR_PARAM] as $p => $info)
1386
		{
1387
			if (!isset($params['{' . $p . '}']))
1388
			{
1389
				$params['{' . $p . '}'] = '';
1390
			}
1391
		}
1392
1393
		// We found our tag
1394
		$tag = $possible;
1395
1396
		// Put the parameters into the string.
1397
		if (isset($tag[Codes::ATTR_BEFORE]))
1398
		{
1399
			$tag[Codes::ATTR_BEFORE] = strtr($tag[Codes::ATTR_BEFORE], $params);
1400
		}
1401
		if (isset($tag[Codes::ATTR_AFTER]))
1402
		{
1403
			$tag[Codes::ATTR_AFTER] = strtr($tag[Codes::ATTR_AFTER], $params);
1404
		}
1405
		if (isset($tag[Codes::ATTR_CONTENT]))
1406
		{
1407
			$tag[Codes::ATTR_CONTENT] = strtr($tag[Codes::ATTR_CONTENT], $params);
1408
		}
1409
1410
		$this->pos1 += strlen($matches[0]) - 1;
1411
1412
		return $tag;
1413
	}
1414
1415
	/**
1416
	 * Check if a tag (not a code) is open
1417
	 * @param string $tag
1418
	 *
1419
	 * @return bool
1420
	 */
1421
	protected function isOpen($tag)
1422
	{
1423
		foreach ($this->open_tags as $open)
1424
		{
1425
			if ($open[Codes::ATTR_TAG] === $tag)
1426
			{
1427
				return true;
1428
			}
1429
		}
1430
1431
		return false;
1432
	}
1433
1434
	/**
1435
	 * Check if a character is an item code
1436
	 * @param string $char
1437
	 *
1438
	 * @return bool
1439
	 */
1440
	protected function isItemCode($char)
1441
	{
1442
		return isset($this->item_codes[$char]);
1443
	}
1444
1445
	/**
1446
	 * Close any open codes that aren't block level.
1447
	 * Used before opening a code that *is* block level
1448
	 */
1449
	protected function closeNonBlockLevel()
1450
	{
1451
		$n = count($this->open_tags) - 1;
1452
		while (empty($this->open_tags[$n][Codes::ATTR_BLOCK_LEVEL]) && $n >= 0)
1453
		{
1454
			$n--;
1455
		}
1456
1457
		// Close all the non block level tags so this tag isn't surrounded by them.
1458
		for ($i = count($this->open_tags) - 1; $i > $n; $i--)
1459
		{
1460
			$tmp = $this->noSmileys($this->open_tags[$i][Codes::ATTR_AFTER]);
1461
			$this->message = substr_replace($this->message, $tmp, $this->pos, 0);
1462
			$ot_strlen = strlen($tmp);
1463
			$this->pos += $ot_strlen;
1464
			$this->pos1 += $ot_strlen;
1465
1466
			// Trim or eat trailing stuff... see comment at the end of the big loop.
1467
			if (!empty($this->open_tags[$i][Codes::ATTR_BLOCK_LEVEL]) && substr_compare($this->message, '<br />', $this->pos, 6) === 0)
1468
			{
1469
				$this->message = substr_replace($this->message, '', $this->pos, 6);
1470
			}
1471
1472
			if (isset($tag[Codes::ATTR_TRIM]) && $tag[Codes::ATTR_TRIM] !== Codes::TRIM_INSIDE)
1473
			{
1474
				$this->trimWhiteSpace($this->message, $this->pos);
1475
			}
1476
1477
			$this->closeOpenedTag();
1478
		}
1479
	}
1480
1481
	/**
1482
	 * Add markers around a string to denote that smileys should not be parsed
1483
	 *
1484
	 * @param string $string
1485
	 *
1486
	 * @return string
1487
	 */
1488
	protected function noSmileys($string)
1489
	{
1490
		return $this->smiley_marker . $string . $this->smiley_marker;
1491
	}
1492
1493
	public function canCache()
1494
	{
1495
		return $this->can_cache;
1496
	}
1497
1498
	// This is just so I can profile it.
1499
	protected function filterData(array $tag, &$data)
1500
	{
1501
		$tag[Codes::ATTR_VALIDATE]($tag, $data, $this->bbc->getDisabled());
1502
	}
1503
}