Issues (1686)

sources/ElkArte/BBC/BBCParser.php (30 issues)

1
<?php
2
3
/**
4
 *
5
 * @package   ElkArte Forum
6
 * @copyright ElkArte Forum contributors
7
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
8
 *
9
 * This file contains code covered by:
10
 * copyright: 2011 Simple Machines (http://www.simplemachines.org)
11
 *
12
 * @version 2.0 dev
13
 *
14
 */
15
16
namespace BBC;
17
18
/**
19
 * Class BBCParser
20
 *
21
 * One of the primary functions, parsing BBC to HTML
22
 *
23
 * @package BBC
24
 */
25
class BBCParser
26
{
27
	/** The max number of iterations to perform while solving for out of order attributes */
28
	public const MAX_PERMUTE_ITERATIONS = 5040;
29
30
	/** @var string */
31
	protected $message;
32
33
	/** @var Codes */
34
	protected $bbc;
35
36
	/** @var array */
37
	protected $bbc_codes;
38
39
	/** @var array */
40
	protected $item_codes;
41
42
	/** @var array */
43
	protected $tags;
44
45
	/** @var int parser position in message */
46
	protected $pos;
47
48
	/** @var int */
49
	protected $pos1;
50
51
	/** @var int */
52
	protected $pos2;
53
54
	/** @var int */
55
	protected $pos3;
56
57
	/** @var int */
58
	protected $last_pos;
59
60
	/** @var bool */
61
	protected $do_smileys = true;
62
63
	/** @var array */
64
	protected $open_tags = [];
65
66
	/** @var string|null This is the actual tag that's open */
67
	protected $inside_tag;
68
69
	/** @var Autolink|null */
70
	protected $autolinker;
71
72
	/** @var bool */
73
	protected $possible_html;
74
75
	/** @var bool */
76
	protected $possible_markdown;
77
78
	/** @var HtmlParser|null */
79
	protected $html_parser;
80
81
	/** @var MarkdownParser|null */
82
	protected $markdown_parser;
83
84
	/** @var bool if we can cache the message or not (some tags disallow caching) */
85
	protected $can_cache = true;
86 4
87
	/** @var int footnote tracker */
88 4
	protected $num_footnotes = 0;
89
90 4
	/** @var string used to mark smiles in a message */
91 4
	protected $smiley_marker = "\r";
92
93 4
	/** @var int */
94 4
	protected $lastAutoPos = 0;
95
96 4
	/** @var array content fo the footnotes */
97 4
	protected $fn_content = [];
98
99
	/** @var array */
100
	protected $tag_possible = [];
101
102 28
	/** @var int */
103
	protected $fn_count = 0;
104 28
105 28
	/** @var int */
106 28
	protected $fn_num = 0;
107 28
108 28
	/**
109 28
	 * BBCParser constructor.
110 28
	 *
111 28
	 * @param Codes $bbc
112 28
	 * @param Autolink|null $autolinker
113 28
	 * @param HtmlParser|null $html_parser
114
	 * @param MarkdownParser|null $markdown_parser
115
	 */
116
	public function __construct(Codes $bbc, Autolink $autolinker = null, HtmlParser $html_parser = null, MarkdownParser $markdown_parser = null)
117
	{
118
		$this->bbc = $bbc;
119
120
		$this->bbc_codes = $this->bbc->getForParsing();
121
		$this->item_codes = $this->bbc->getItemCodes();
122 32
123
		$this->autolinker = $autolinker;
124
		$this->loadAutolink();
125
126 32
		$this->html_parser = $html_parser;
127
		$this->markdown_parser = $markdown_parser;
128 32
	}
129
130
	/**
131 32
	 * Reset the parser's properties for a new message
132
	 */
133 4
	public function resetParser()
134
	{
135
		$this->pos = -1;
136
		$this->pos1 = null;
137 28
		$this->pos2 = null;
138
		$this->last_pos = null;
139
		$this->open_tags = array();
140
		$this->inside_tag = null;
141
		$this->lastAutoPos = 0;
142 28
		$this->can_cache = true;
143
		$this->num_footnotes = 0;
144
	}
145 28
146
	/**
147
	 * Parse the BBC in a string/message
148 28
	 *
149
	 * @param string $message
150 28
	 *
151
	 * @return string
152
	 */
153 28
	public function parse($message)
154
	{
155 2
		// Allow addons access before entering the main parse loop, formally
156
		// called as integrate_pre_parsebbc
157
		call_integration_hook('integrate_pre_bbc_parser', array(&$message, &$this->bbc));
158
159 28
		$this->message = (string) $message;
160
161
		// Don't waste cycles
162 28
		if ($this->message === '')
163
		{
164 2
			return '';
165
		}
166
167 28
		// @todo remove from here and make the caller figure it out
168
		if (!$this->parsingEnabled())
169
		{
170
			return $this->message;
171
		}
172
173
		$this->resetParser();
174 28
175
		$this->message = str_replace("\n", '<br />', $this->message);
176
177 28
		// Check if the message might have a link or email to save a bunch of parsing in autolink()
178
		$this->autolinker->setPossibleAutolink($this->message);
0 ignored issues
show
The method setPossibleAutolink() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

178
		$this->autolinker->/** @scrutinizer ignore-call */ 
179
                     setPossibleAutolink($this->message);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
179 2
180
		$this->possible_html = !empty($GLOBALS['modSettings']['enablePostHTML']) && strpos($message, '&lt;') !== false;
181
182
		// Don't load the HTML Parser unless we have to
183
		if ($this->possible_html && $this->html_parser === null)
184 28
		{
185 28
			$this->loadHtmlParser();
186 28
		}
187
188 28
		$this->possible_markdown = !empty($GLOBALS['modSettings']['enablePostMarkdown']);
189
190
		// Don't load the Markdown Parser unless we have to
191
		if ($this->possible_markdown)
192
		{
193
			if ($this->markdown_parser === null)
194
			{
195
				$this->loadMarkdownParser();
196 28
			}
197
198 28
			// To protect bbc tags inside `icode` and ```code``` blocks, we need to process them up front
199
			$this->message = $this->markdown_parser->inlineCodeTags($this->message);
200 28
		}
201 28
202
		// Allow addons access before entering the main parse loop
203
		call_integration_hook('integrate_pre_bbc_parser_loop', array(&$this->message, &$this->bbc));
204 28
205
		// This handles pretty much all the parsing. It is a separate method, so it is easier to override and profile.
206 28
		$this->parse_loop();
207
208
		// Close any remaining tags.
209
		while ($tag = $this->closeOpenedTag())
210 28
		{
211
			$this->message .= $this->noSmileys($tag[Codes::ATTR_AFTER]);
212 28
		}
213
214
		if (isset($this->message[0]) && $this->message[0] === ' ')
215
		{
216 28
			$this->message = substr_replace($this->message, '&nbsp;', 0, 1);
0 ignored issues
show
Documentation Bug introduced by
It seems like substr_replace($this->message, '&nbsp;', 0, 1) can also be of type array. However, the property $message is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
217
			//$this->message = '&nbsp;' . substr($this->message, 1);
218 28
		}
219
220
		// Cleanup whitespace.
221 16
		$this->message = str_replace(array('  ', '<br /> ', '&#13;'), array('&nbsp; ', '<br />&nbsp;', "\n"), $this->message);
222
223
		// Finish footnotes if we have any.
224 16
		if ($this->num_footnotes > 0)
225
		{
226 16
			$this->handleFootnotes();
227
		}
228 14
229
		// Allow addons access to what the parser created, formally
230
		// called as integrate_post_parsebbc
231
		$message = $this->message;
232 16
		call_integration_hook('integrate_post_bbc_parser', array(&$message));
233
		$this->message = $message;
234
235
		return $this->message;
236 16
	}
237
238 2
	/**
239
	 * The BBC parsing loop-o-love
240
	 *
241 16
	 * Walks the string to parse, looking for BBC tags and passing items to the required translation functions
242
	 */
243 16
	protected function parse_loop()
244
	{
245
		while ($this->pos !== false)
246 2
		{
247
			$this->last_pos = $this->last_pos !== null ? max($this->pos, $this->last_pos) : $this->pos;
248
			$this->pos = strpos($this->message, '[', $this->pos + 1);
249 2
250
			// Failsafe.
251
			if ($this->pos === false || $this->last_pos > $this->pos)
252
			{
253 2
				$this->pos = strlen($this->message) + 1;
254
			}
255
256
			// Can't have a one letter smiley, URL, or email! (sorry.)
257 16
			if ($this->last_pos < $this->pos - 1)
258
			{
259
				$this->betweenTags();
260
			}
261 16
262
			// Are we there yet?  Are we there yet?
263
			if ($this->pos >= strlen($this->message) - 1)
264
			{
265
				return;
266
			}
267
268
			$next_char = strtolower($this->message[$this->pos + 1]);
269
270 16
			// Possibly a closer?
271
			if ($next_char === '/')
272 2
			{
273
				if ($this->hasOpenTags())
274
				{
275
					$this->handleOpenTags();
276 14
				}
277
278
				// We don't allow / to be used for anything but the closing character, so this can't be a tag
279
				continue;
280
			}
281
282 14
			// No tags for this character, so just keep going (fastest possible course.)
283
			if (!isset($this->bbc_codes[$next_char]))
284
			{
285
				continue;
286
			}
287
288 14
			$this->inside_tag = $this->hasOpenTags() ? $this->getLastOpenedTag() : null;
289
290 6
			if (isset($this->message[$this->pos + 2]) && $this->isItemCode($next_char) && $this->message[$this->pos + 2] === ']' && !$this->bbc->isDisabled('list') && !$this->bbc->isDisabled('li'))
291
			{
292
				// Itemcodes cannot be 0 and must be proceeded by a semi-colon, space, tab, new line, or greater than sign
293
				if (!($this->message[$this->pos + 1] === '0' && !in_array($this->message[$this->pos - 1], array(';', ' ', "\t", "\n", '>'))))
294 14
				{
295
					// Item codes are complicated buggers... they are implicit [li]s and can make [list]s!
296
					$this->handleItemCode();
297
				}
298
299
				// No matter what, we have to continue here.
300 14
				continue;
301
			}
302 2
303
			$tag = $this->findTag($this->bbc_codes[$next_char]);
304
305
			// Implicitly close lists and tables if something other than what's required is in them. This is needed for itemcode.
306 14
			if ($tag === null && $this->inside_tag !== null && !empty($this->inside_tag[Codes::ATTR_REQUIRE_CHILDREN]))
307
			{
308 4
				$this->closeOpenedTag();
309
				$tmp = $this->noSmileys($this->inside_tag[Codes::ATTR_AFTER]);
310
				$this->message = substr_replace($this->message, $tmp, $this->pos, 0);
0 ignored issues
show
Documentation Bug introduced by
It seems like substr_replace($this->me...e, $tmp, $this->pos, 0) can also be of type array. However, the property $message is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
311
				$this->pos += strlen($tmp) - 1;
312
			}
313
314
			// No tag?  Keep looking, then.  Silly people using brackets without actual tags.
315
			if ($tag === null)
316 14
			{
317
				continue;
318
			}
319 14
320
			// Propagate the list to the child (so wrapping the disallowed tag won't work either.)
321
			if (isset($this->inside_tag[Codes::ATTR_DISALLOW_CHILDREN]))
322 14
			{
323
				$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];
324
			}
325
326
			// Is this tag disabled?
327
			if ($this->bbc->isDisabled($tag[Codes::ATTR_TAG]))
328 14
			{
329 14
				$this->handleDisabled($tag);
330 14
			}
331
332
			// The only special case is 'html', which doesn't need to close things.
333
			if ($tag[Codes::ATTR_BLOCK_LEVEL] && $tag[Codes::ATTR_TAG] !== 'html' && empty($this->inside_tag[Codes::ATTR_BLOCK_LEVEL]))
334
			{
335 14
				$this->closeNonBlockLevel();
336
			}
337
338 14
			// This is the part where we actually handle the tags. I know, crazy how long it took.
339
			if ($this->handleTag($tag))
340
			{
341
				continue;
342
			}
343 14
344
			// If this is block level, eat any breaks after it.
345
			if ($tag[Codes::ATTR_BLOCK_LEVEL] && isset($this->message[$this->pos + 1]) && substr_compare($this->message, '<br />', $this->pos + 1, 6) === 0)
0 ignored issues
show
It seems like $this->message can also be of type array; however, parameter $haystack of substr_compare() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

345
			if ($tag[Codes::ATTR_BLOCK_LEVEL] && isset($this->message[$this->pos + 1]) && substr_compare(/** @scrutinizer ignore-type */ $this->message, '<br />', $this->pos + 1, 6) === 0)
Loading history...
346 4
			{
347
				$this->message = substr_replace($this->message, '', $this->pos + 1, 6);
348
			}
349
350
			// Are we trimming outside this tag?
351
			if (!empty($tag[Codes::ATTR_TRIM]) && $tag[Codes::ATTR_TRIM] !== Codes::TRIM_OUTSIDE)
352
			{
353 4
				$this->trimWhiteSpace($this->pos + 1);
354
			}
355 4
		}
356
	}
357 4
358
	/**
359 4
	 * Process a tag once the closing character / has been found
360 4
	 */
361
	protected function handleOpenTags()
362
	{
363
		// Next closing bracket after the first character
364
		$this->pos2 = strpos($this->message, ']', $this->pos + 1);
365 4
366
		// Playing games? string = [/]
367
		if ($this->pos2 === $this->pos + 2)
368
		{
369
			return;
370
		}
371
372
		// Get everything between [/ and ]
373 14
		$look_for = strtolower(substr($this->message, $this->pos + 2, $this->pos2 - $this->pos - 2));
374 14
		$to_close = array();
375
		$block_level = null;
376
377 14
		do
378
		{
379
			// Get the last opened tag
380
			$tag = $this->closeOpenedTag();
381
382
			// No open tags
383 14
			if (!$tag)
384
			{
385
				break;
386
			}
387
388
			if (!empty($tag[Codes::ATTR_BLOCK_LEVEL]))
389
			{
390
				// Only find out if we need to.
391
				if ($block_level === false)
392
				{
393
					$this->addOpenTag($tag);
394
					break;
395
				}
396
397
				// The idea is, if we are LOOKING for a block level tag, we can close them on the way.
398
				if (isset($look_for[1], $this->bbc_codes[$look_for[0]]))
399
				{
400
					foreach ($this->bbc_codes[$look_for[0]] as $temp)
401
					{
402
						if ($temp[Codes::ATTR_TAG] === $look_for)
403
						{
404
							$block_level = $temp[Codes::ATTR_BLOCK_LEVEL];
405
							break;
406
						}
407
					}
408
				}
409 14
410
				if ($block_level !== true)
411 14
				{
412 14
					$block_level = false;
413 14
					$this->addOpenTag($tag);
414 14
					break;
415
				}
416
			}
417 14
418
			$to_close[] = $tag;
419
		} while (isset($tag[Codes::ATTR_TAG]) && $tag[Codes::ATTR_TAG] !== $look_for);
420
421
		// Did we just eat through everything and not find it?
422
		if ((empty($tag) || $tag[Codes::ATTR_TAG] !== $look_for) && !$this->hasOpenTags())
423 14
		{
424
			$this->open_tags = $to_close;
425 9
426
			return;
427
		}
428
429 14
		if (!empty($to_close) && isset($tag[Codes::ATTR_TAG]) && $tag[Codes::ATTR_TAG] !== $look_for)
430
		{
431 14
			if ($block_level === null && isset($look_for[0], $this->bbc_codes[$look_for[0]]))
432
			{
433 14
				foreach ($this->bbc_codes[$look_for[0]] as $temp)
434
				{
435
					if ($temp[Codes::ATTR_TAG] === $look_for)
436
					{
437
						$block_level = !empty($temp[Codes::ATTR_BLOCK_LEVEL]);
438
						break;
439
					}
440
				}
441
			}
442
443
			// We're not looking for a block level tag (or maybe even a tag that exists...)
444
			if (!$block_level)
0 ignored issues
show
Bug Best Practice introduced by
The expression $block_level of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
445
			{
446
				foreach ($to_close as $tag)
447
				{
448
					$this->addOpenTag($tag);
449
				}
450
451
				return;
452
			}
453 28
		}
454
455 28
		foreach ($to_close as $tag)
456
		{
457
			$tmp = $this->noSmileys($tag[Codes::ATTR_AFTER]);
458
			$this->message = substr_replace($this->message, $tmp, $this->pos, $this->pos2 + 1 - $this->pos);
0 ignored issues
show
Documentation Bug introduced by
It seems like substr_replace($this->me...>pos2 + 1 - $this->pos) can also be of type array. However, the property $message is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
459
			$this->pos += strlen($tmp);
460
			$this->pos2 = $this->pos - 1;
461 2
462
			// See the comment at the end of the big loop - just eating whitespace ;).
463 2
			if (!empty($tag[Codes::ATTR_BLOCK_LEVEL]) && isset($this->message[$this->pos]) && substr_compare($this->message, '<br />', $this->pos, 6) === 0)
0 ignored issues
show
It seems like $this->message can also be of type array; however, parameter $haystack of substr_compare() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

463
			if (!empty($tag[Codes::ATTR_BLOCK_LEVEL]) && isset($this->message[$this->pos]) && substr_compare(/** @scrutinizer ignore-type */ $this->message, '<br />', $this->pos, 6) === 0)
Loading history...
464 2
			{
465 2
				$this->message = substr_replace($this->message, '', $this->pos, 6);
466 2
			}
467
			// Trim inside whitespace
468
			if (empty($tag[Codes::ATTR_TRIM]))
469
			{
470
				continue;
471
			}
472
			if ($tag[Codes::ATTR_TRIM] === Codes::TRIM_INSIDE)
473
			{
474
				continue;
475 2
			}
476
			$this->trimWhiteSpace($this->pos);
477 2
		}
478
479
		if (!empty($to_close))
480
		{
481
			$this->pos--;
482
		}
483
	}
484
485
	/**
486
	 * Turn smiley parsing on/off
487 28
	 *
488
	 * @param bool $toggle
489 28
	 * @return BBCParser
490
	 */
491 20
	public function doSmileys($toggle)
492
	{
493
		$this->do_smileys = (bool) $toggle;
494
495 14
		return $this;
496
	}
497 14
498
	/**
499 14
	 * Check if parsing is enabled
500
	 *
501 13
	 * @return bool
502
	 */
503
	public function parsingEnabled()
504
	{
505
		return !empty($GLOBALS['modSettings']['enableBBC']);
506 14
	}
507
508
	/**
509
	 * Load the HTML parsing engine
510
	 */
511
	public function loadHtmlParser()
512 4
	{
513
		$parser = new HtmlParser();
514 4
		call_integration_hook('integrate_bbc_load_html_parser', array(&$parser));
515
		$this->html_parser = $parser;
516 2
	}
517
518 4
	/**
519
	 * Parse the HTML in a string
520
	 *
521
	 * @param string $data
522
	 *
523
	 * @return string
524
	 */
525
	protected function parseHTML($data)
526
	{
527 16
		return $this->html_parser->parse($data);
0 ignored issues
show
The method parse() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

527
		return $this->html_parser->/** @scrutinizer ignore-call */ parse($data);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
528
	}
529 16
530 16
	/**
531
	 * Load the Markdown parsing engine
532 16
	 */
533
	public function loadMarkdownParser()
534
	{
535 16
		$parser = new MarkdownParser();
536
		call_integration_hook('integrate_bbc_load_markdown_parser', [&$parser]);
537 6
		$this->markdown_parser = $parser;
538
	}
539
540
	/**
541 16
	 * Parse any Markdown in a string
542
	 *
543
	 * @param string $data
544 16
	 *
545
	 * @return string
546
	 */
547
	protected function parseMarkdown($data)
548
	{
549
		return $this->markdown_parser->parse($data);
0 ignored issues
show
The method parse() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

549
		return $this->markdown_parser->/** @scrutinizer ignore-call */ parse($data);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
550 16
	}
551
552 16
	/**
553 16
	 * Parse URIs and email addresses in a string to url and email BBC tags to be parsed by the BBC parser
554
	 *
555
	 * @param string $data
556
	 *
557 16
	 * @return null|string|string[]
558
	 */
559 4
	protected function autoLink($data)
560 4
	{
561
		if ($data === '' || $data === $this->smiley_marker || !$this->autolinker->hasPossible())
562
		{
563 16
			return $data;
564 16
		}
565
566 16
		// Are we inside tags that should be auto linked?
567
		if ($this->hasOpenTags())
568
		{
569
			foreach ($this->getOpenedTags() as $open_tag)
570 14
			{
571
				if (empty($open_tag[Codes::ATTR_AUTOLINK]))
572 2
				{
573
					return $data;
574
				}
575 14
			}
576
		}
577
578
		return $this->autolinker->parse($data);
579 16
	}
580
581 14
	/**
582
	 * Load the autolink regular expression to be used in autoLink()
583
	 */
584
	protected function loadAutolink()
585 16
	{
586
		if ($this->autolinker === null)
587 2
		{
588
			$this->autolinker = new Autolink($this->bbc);
589
		}
590 16
	}
591
592
	/**
593
	 * Find if the current character is the start of a tag and get it
594
	 *
595
	 * @param array $possible_codes
596
	 *
597
	 * @return null|array the tag that was found or null if no tag found
598 2
	 */
599
	protected function findTag(array $possible_codes)
600
	{
601 2
		$tag = null;
602 2
		$last_check = null;
603
604
		foreach ($possible_codes as $possible)
605 2
		{
606
			// Skip tags that didn't match the next X characters
607 2
			if ($possible[Codes::ATTR_TAG] === $last_check)
608
			{
609
				continue;
610
			}
611
612
			// The character after the possible tag or nothing
613
			$next_c = $this->message[$this->pos + 1 + $possible[Codes::ATTR_LENGTH]] ?? '';
614
615 2
			// This only happens if the tag is the last character of the string
616 2
			if ($next_c === '')
617 2
			{
618
				break;
619
			}
620
621
			// The next character must be one of these or it's not a tag
622
			if ($next_c !== ' ' && $next_c !== ']' && $next_c !== '=' && $next_c !== '/')
623
			{
624
				$last_check = $possible[Codes::ATTR_TAG];
625
				continue;
626 16
			}
627
628
			// Not a match?
629 16
			if (substr_compare($this->message, $possible[Codes::ATTR_TAG], $this->pos + 1, $possible[Codes::ATTR_LENGTH], true) !== 0)
630
			{
631 4
				$last_check = $possible[Codes::ATTR_TAG];
632
				continue;
633 4
			}
634
635
			$tag = $this->checkCodeAttributes($next_c, $possible);
636
			if ($tag === null)
637 16
			{
638
				continue;
639 8
			}
640
641 8
			// Quotes can have alternate styling, we do this php-side due to all the permutations of quotes.
642
			if ($tag[Codes::ATTR_TAG] === 'quote')
643
			{
644
				$this->alternateQuoteStyle($tag);
645
			}
646
647 16
			break;
648
		}
649 4
650
		// If there is a code that says you can't cache, the message can't be cached
651
		if ($tag !== null && $this->can_cache)
0 ignored issues
show
The condition $tag !== null is always false.
Loading history...
652 16
		{
653
			$this->can_cache = empty($tag[Codes::ATTR_NO_CACHE]);
654
		}
655 16
656
		// If its a footnote, keep track of the number
657 12
		if (isset($tag[Codes::ATTR_TAG]) && $tag[Codes::ATTR_TAG] === 'footnote')
658
		{
659
			$this->num_footnotes++;
660 16
		}
661
662
		return $tag;
663
	}
664
665
	/**
666
	 * Just alternates the applied class for quotes for themes that want to distinguish them (legacy)
667
	 *
668 16
	 * - The alternating should be covered with CSS odd/even attributes
669
	 * - Now used to detect long quotes and wrap them in a container div with an input checkbox.
670
	 * - The wrapper and checkbox are styled by the CSS with a fade out effect and a read more click.
671
	 *
672
	 * @param array $tag
673 16
	 */
674
	protected function alternateQuoteStyle(array &$tag)
675 6
	{
676
		// Start with standard
677
		$quote_alt = false;
678
		$first_quote = 0;
679
680
		foreach ($this->open_tags as $open_quote)
681 6
		{
682
			// Every parent quote this quote has flips the styling
683
			if (isset($open_quote[Codes::ATTR_TAG]) && $open_quote[Codes::ATTR_TAG] === 'quote')
684
			{
685
				$quote_alt = !$quote_alt;
0 ignored issues
show
The condition $quote_alt is always false.
Loading history...
686
				$first_quote++;
687 6
			}
688
		}
689
690
		// First quote (of nested or single) receives a wrapper so its markup will be:
691
		// <div class="quote-read-more"> .. relative
692
		//		<input type="checkbox" class="quote-show-more">.. absolute over the below blockquote
693
		//		<blockquote class="bbc_quote">
694
		//			<cite>Peter Parker says</cite>
695
		//			No man can win every battle, but no man should fall without a struggle.
696
		//		</blockquote> .. with a max height that is removed on input click
697
		// </div>
698
		if ($first_quote === 0)
699 16
		{
700
			$tag[Codes::ATTR_BEFORE] = str_replace('<blockquote class="bbc_quote">', '<div class="quote-read-more"><input type="checkbox" title="show" class="quote-show-more"><blockquote class="bbc_quote">', $tag[Codes::ATTR_BEFORE]);
701 6
			$tag[Codes::ATTR_AFTER] = str_replace('</blockquote>', '</blockquote></div>', $tag[Codes::ATTR_AFTER]);
702
		}
703
		// Add a class to every other blockquote to style alternating blockquotes
704
		// This allows simpler CSS for themes (like default) which do not use the alternate styling,
705 16
		// but still allow it for legacy themes that want it.
706
		elseif ($quote_alt)
707
		{
708 16
			$tag[Codes::ATTR_BEFORE] = str_replace('<blockquote class="bbc_quote">', '<blockquote class="bbc_quote bbc_alternate_quote">', $tag[Codes::ATTR_BEFORE]);
709
		}
710 4
	}
711
712
	/**
713 4
	 * Parses BBC codes attributes for codes that may have them
714
	 *
715 4
	 * @param string $next_c
716
	 * @param array $possible
717
	 * @return array|null
718 2
	 */
719
	protected function checkCodeAttributes($next_c, array $possible)
720
	{
721 14
		// Do we want parameters?
722
		if (!empty($possible[Codes::ATTR_PARAM]))
723
		{
724
			if ($next_c !== ' ')
725
			{
726
				return null;
727
			}
728
		}
729
		// parsed_content demands an immediate ] without parameters!
730
		elseif ($possible[Codes::ATTR_TYPE] === Codes::TYPE_PARSED_CONTENT)
731 8
		{
732
			if ($next_c !== ']')
733 8
			{
734
				return null;
735
			}
736
		}
737
		else
738
		{
739 2
			// Do we need an equal sign?
740
			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)))
741 2
			{
742
				return null;
743
			}
744
745
			if ($next_c !== ']')
746 2
			{
747
				// An immediate ]?
748
				if ($possible[Codes::ATTR_TYPE] === Codes::TYPE_UNPARSED_CONTENT)
749 2
				{
750
					return null;
751 2
				}
752 2
753
				// Maybe we just want a /...
754 2
				if ($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)
755
				{
756
					return null;
757 2
				}
758 2
			}
759
		}
760
761 2
		// Check allowed tree?
762
		if (isset($possible[Codes::ATTR_REQUIRE_PARENTS]) && ($this->inside_tag === null || !isset($possible[Codes::ATTR_REQUIRE_PARENTS][$this->inside_tag[Codes::ATTR_TAG]])))
763
		{
764 2
			return null;
765
		}
766 2
767 2
		if ($this->inside_tag !== null)
768
		{
769
			if (isset($this->inside_tag[Codes::ATTR_REQUIRE_CHILDREN]) && !isset($this->inside_tag[Codes::ATTR_REQUIRE_CHILDREN][$possible[Codes::ATTR_TAG]]))
770
			{
771
				return null;
772
			}
773
774
			// If this is in the list of disallowed child tags, don't parse it.
775 2
			if (isset($this->inside_tag[Codes::ATTR_DISALLOW_CHILDREN]) && isset($this->inside_tag[Codes::ATTR_DISALLOW_CHILDREN][$possible[Codes::ATTR_TAG]]))
776 2
			{
777
				return null;
778 2
			}
779
780
			// Not allowed in this parent, replace the tags or show it like regular text
781 2
			if (isset($possible[Codes::ATTR_DISALLOW_PARENTS][$this->inside_tag[Codes::ATTR_TAG]]))
782
			{
783 2
				if (!isset($possible[Codes::ATTR_DISALLOW_BEFORE], $possible[Codes::ATTR_DISALLOW_AFTER]))
784
				{
785
					return null;
786
				}
787 2
788
				$possible[Codes::ATTR_BEFORE] = $possible[Codes::ATTR_DISALLOW_BEFORE] ?? $possible[Codes::ATTR_BEFORE];
789 2
				$possible[Codes::ATTR_AFTER] = $possible[Codes::ATTR_DISALLOW_AFTER] ?? $possible[Codes::ATTR_AFTER];
790 2
			}
791 2
		}
792
793
		if (isset($possible[Codes::ATTR_TEST]) && $this->handleTest($possible))
794 2
		{
795 2
			return null;
796
		}
797 2
798 2
		// +1 for [, then the length of the tag, then a space
799
		$this->pos1 = $this->pos + 1 + $possible[Codes::ATTR_LENGTH] + 1;
800
801
		// If we need to reset content attr, this is where we step in
802
		if (isset($possible[Codes::ATTR_RESET]))
803
		{
804
			$possible[Codes::ATTR_CONTENT] = $possible[Codes::ATTR_RESET];
805
		}
806
807
		// This is long, but it makes things much easier and cleaner.
808
		if (!empty($possible[Codes::ATTR_PARAM]))
809
		{
810
			$match = $this->matchParameters($possible, $matches);
811
812
			// Didn't match our parameter list, try the next possible.
813
			if (!$match)
814 2
			{
815 2
				return null;
816
			}
817 2
818
			return $this->setupTagParameters($possible, $matches);
819
		}
820
821
		return $possible;
822
	}
823
824
	/**
825
	 * Called when a code has defined a test parameter
826 6
	 *
827
	 * @param array $possible
828
	 *
829 6
	 * @return bool
830 6
	 */
831 6
	protected function handleTest(array $possible)
832 6
	{
833
		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;
834 6
	}
835
836
	/**
837
	 * Handles item codes by converting them to lists
838
	 */
839
	protected function handleItemCode()
840
	{
841
		if (!isset($this->item_codes[$this->message[$this->pos + 1]]))
842
		{
843
			return;
844 6
		}
845
846
		$tag = $this->item_codes[$this->message[$this->pos + 1]];
847 6
848
		// First let's set up the tree: it needs to be in a list, or after an li.
849
		if ($this->inside_tag === null || (isset($this->inside_tag[Codes::ATTR_TAG]) && $this->inside_tag[Codes::ATTR_TAG] !== 'list' && $this->inside_tag[Codes::ATTR_TAG] !== 'li'))
850 6
		{
851
			$this->addOpenTag(array(
852
				Codes::ATTR_TAG => 'list',
853
				Codes::ATTR_TYPE => Codes::TYPE_PARSED_CONTENT,
854
				Codes::ATTR_AFTER => '</ul>',
855 6
				Codes::ATTR_BLOCK_LEVEL => true,
856
				Codes::ATTR_REQUIRE_CHILDREN => array('li' => 'li'),
857 6
				Codes::ATTR_DISALLOW_CHILDREN => $this->inside_tag[Codes::ATTR_DISALLOW_CHILDREN] ?? null,
858
				Codes::ATTR_LENGTH => 4,
859
				Codes::ATTR_AUTOLINK => true,
860
			));
861
862 6
			$code = '<ul class="bbc_list">';
863
		}
864 6
		// We're in a list item already: another itemcode?  Close it first.
865
		elseif (isset($this->inside_tag[Codes::ATTR_TAG]) && $this->inside_tag[Codes::ATTR_TAG] === 'li')
866
		{
867 6
			$this->closeOpenedTag();
868 6
			$code = '</li>';
869 6
		}
870 6
		else
871 6
		{
872
			$code = '';
873 6
		}
874
875
		// Now we open a new tag.
876
		$this->addOpenTag(array(
877
			Codes::ATTR_TAG => 'li',
878
			Codes::ATTR_TYPE => Codes::TYPE_PARSED_CONTENT,
879
			Codes::ATTR_AFTER => '</li>',
880
			Codes::ATTR_TRIM => Codes::TRIM_OUTSIDE,
881
			Codes::ATTR_BLOCK_LEVEL => true,
882
			Codes::ATTR_DISALLOW_CHILDREN => $this->inside_tag[Codes::ATTR_DISALLOW_CHILDREN] ?? null,
883 2
			Codes::ATTR_AUTOLINK => true,
884
			Codes::ATTR_LENGTH => 2,
885
		));
886 2
887
		// First, open the tag...
888
		$code .= '<li style="list-style-type: ' . $tag . '">';
889
		$tmp = $this->noSmileys($code);
890
		$this->message = substr_replace($this->message, $tmp, $this->pos, 3);
0 ignored issues
show
Documentation Bug introduced by
It seems like substr_replace($this->me...e, $tmp, $this->pos, 3) can also be of type array. However, the property $message is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
891
		$this->pos += strlen($tmp) - 1;
892
893
		// Next, find the next break (if any.)  If there's more itemcode after it, keep it going - otherwise close!
894
		$this->pos2 = strpos($this->message, '<br />', $this->pos);
0 ignored issues
show
It seems like $this->message can also be of type array; however, parameter $haystack of strpos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

894
		$this->pos2 = strpos(/** @scrutinizer ignore-type */ $this->message, '<br />', $this->pos);
Loading history...
895
		$this->pos3 = strpos($this->message, '[/', $this->pos);
896
897
		$num_open_tags = count($this->open_tags);
898
		if ($this->pos2 !== false && ($this->pos3 === false || $this->pos2 <= $this->pos3))
899
		{
900
			// Can't use offset because of the ^
901 2
			preg_match('~^(<br />|&nbsp;|\s|\[)+~', substr($this->message, $this->pos2 + 6), $matches);
0 ignored issues
show
It seems like $this->message can also be of type array; however, parameter $string of substr() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

901
			preg_match('~^(<br />|&nbsp;|\s|\[)+~', substr(/** @scrutinizer ignore-type */ $this->message, $this->pos2 + 6), $matches);
Loading history...
902
			//preg_match('~(<br />|&nbsp;|\s|\[)+~', $this->message, $matches, 0, $this->pos2 + 6);
903
904 2
			// Keep the list open if the next character after the break is a [. Otherwise, close it.
905 2
			$replacement = !empty($matches[0]) && substr_compare($matches[0], '[', -1, 1) === 0 ? '[/li]' : '[/li][/list]';
906
907
			$this->message = substr_replace($this->message, $replacement, $this->pos2, 0);
908
			$this->open_tags[$num_open_tags - 2][Codes::ATTR_AFTER] = '</ul>';
909
		}
910 2
		// Tell the [list] that it needs to close specially.
911 2
		else
912
		{
913
			// Move the li over, because we're not sure what we'll hit.
914
			$this->open_tags[$num_open_tags - 1][Codes::ATTR_AFTER] = '';
915
			$this->open_tags[$num_open_tags - 2][Codes::ATTR_AFTER] = '</li></ul>';
916
		}
917 2
	}
918 2
919
	/**
920
	 * Handle codes that are of the parsed context type
921 2
	 *
922
	 * @param array $tag
923
	 *
924
	 * @return bool
925
	 */
926
	protected function handleTypeParsedContext(array $tag)
927 2
	{
928
		// @todo Check for end tag first, so people can say "I like that [i] tag"?
929 2
		$this->addOpenTag($tag);
930
		$tmp = $this->noSmileys($tag[Codes::ATTR_BEFORE]);
931
		$this->message = substr_replace($this->message, $tmp, $this->pos, $this->pos1 - $this->pos);
0 ignored issues
show
Documentation Bug introduced by
It seems like substr_replace($this->me...his->pos1 - $this->pos) can also be of type array. However, the property $message is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
932 2
		$this->pos += strlen($tmp) - 1;
933 2
934 2
		return false;
935 2
	}
936
937 2
	/**
938
	 * Handle codes that are of the unparsed context type
939
	 *
940
	 * @param array $tag
941
	 *
942
	 * @return bool
943
	 */
944
	protected function handleTypeUnparsedContext(array $tag)
945
	{
946
		// Find the next closer
947 4
		$this->pos2 = stripos($this->message, '[/' . $tag[Codes::ATTR_TAG] . ']', $this->pos1);
948
949 4
		// Account for basic tag nesting
950 4
		$this->handleUnparsedContentNesting($tag);
951 4
952 4
		// No closer
953
		if ($this->pos2 === false)
954 4
		{
955
			return true;
956
		}
957
958
		$data = substr($this->message, $this->pos1, $this->pos2 - $this->pos1);
959
960
		if (!empty($tag[Codes::ATTR_BLOCK_LEVEL]) && isset($data[0]) && substr_compare($data, '<br />', 0, 6) === 0)
961
		{
962
			$data = substr($data, 6);
963
		}
964
965
		if (isset($tag[Codes::ATTR_VALIDATE]))
966
		{
967
			$this->filterData($tag, $data);
968
		}
969
970
		$code = strtr($tag[Codes::ATTR_CONTENT], array('$1' => $data));
971
		$tmp = $this->noSmileys($code);
972
		$this->message = substr_replace($this->message, $tmp, $this->pos, $this->pos2 + 3 + $tag[Codes::ATTR_LENGTH] - $this->pos);
0 ignored issues
show
Documentation Bug introduced by
It seems like substr_replace($this->me...R_LENGTH] - $this->pos) can also be of type array. However, the property $message is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
973
		$this->pos += strlen($tmp) - 1;
974
		$this->last_pos = $this->pos + 1;
975
976
		return false;
977
	}
978
979
	/**
980
	 * Account for the most basic nesting of same unparsed tags
981
	 *  - [code][code]x[/code][/code]
982
	 *  - [code][code]x[/code]<br />[/code]
983
	 */
984
	protected function handleUnparsedContentNesting($tag)
985
	{
986
		$nest_advance = $this->pos2 + $tag[Codes::ATTR_LENGTH] + 3;
987
988
		$nest_check = stripos($this->message, '<br />[/' . $tag[Codes::ATTR_TAG] . ']', $nest_advance);
989
		if ($nest_check && $nest_check === $nest_advance)
990
		{
991
			$this->pos2 = $nest_advance + 6;
992
		}
993
		else
994
		{
995
			$nest_check = stripos($this->message, '[/' . $tag[Codes::ATTR_TAG] . ']', $nest_advance);
996
			if ($nest_check && $nest_check === $nest_advance)
997
			{
998
				$this->pos2 = $nest_advance;
999
			}
1000
		}
1001
	}
1002
1003
	/**
1004
	 * Handle codes that are of the unparsed equals context type
1005
	 *
1006
	 * @param array $tag
1007
	 *
1008
	 * @return bool
1009
	 */
1010
	protected function handleUnparsedEqualsContext(array $tag)
1011
	{
1012
		// The value may be quoted for some tags - check.
1013
		if (isset($tag[Codes::ATTR_QUOTED]))
1014
		{
1015
			$quoted = substr_compare($this->message, '&quot;', $this->pos1, 6) === 0;
1016
			if ($tag[Codes::ATTR_QUOTED] !== Codes::OPTIONAL && !$quoted)
1017
			{
1018
				return true;
1019
			}
1020
1021
			if ($quoted)
1022
			{
1023
				$this->pos1 += 6;
1024
			}
1025
		}
1026
		else
1027
		{
1028
			$quoted = false;
1029
		}
1030
1031
		$this->pos2 = strpos($this->message, $quoted ? '&quot;]' : ']', $this->pos1);
1032
		if ($this->pos2 === false)
1033
		{
1034
			return true;
1035
		}
1036
1037
		$this->pos3 = stripos($this->message, '[/' . $tag[Codes::ATTR_TAG] . ']', $this->pos2);
1038
		if ($this->pos3 === false)
1039
		{
1040
			return true;
1041
		}
1042
1043
		$data = array(
1044
			substr($this->message, $this->pos2 + ($quoted ? 7 : 1), $this->pos3 - ($this->pos2 + ($quoted ? 7 : 1))),
1045
			substr($this->message, $this->pos1, $this->pos2 - $this->pos1)
1046
		);
1047
1048
		if (!empty($tag[Codes::ATTR_BLOCK_LEVEL]) && substr_compare($data[0], '<br />', 0, 6) === 0)
1049
		{
1050
			$data[0] = substr($data[0], 6);
1051 14
		}
1052
1053
		// Validation for my parking, please!
1054 14
		if (isset($tag[Codes::ATTR_VALIDATE]))
1055
		{
1056 4
			$this->filterData($tag, $data);
1057 4
		}
1058
1059
		$code = strtr($tag[Codes::ATTR_CONTENT], array('$1' => $data[0], '$2' => $data[1]));
1060
		$tmp = $this->noSmileys($code);
1061
		$this->message = substr_replace($this->message, $tmp, $this->pos, $this->pos3 + 3 + $tag[Codes::ATTR_LENGTH] - $this->pos);
0 ignored issues
show
Documentation Bug introduced by
It seems like substr_replace($this->me...R_LENGTH] - $this->pos) can also be of type array. However, the property $message is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
1062 4
		$this->pos += strlen($tmp) - 1;
1063
1064 4
		return false;
1065
	}
1066
1067
	/**
1068
	 * Handle codes that are of the closed type
1069 14
	 *
1070
	 * @param array $tag
1071
	 *
1072 14
	 * @return bool
1073 14
	 */
1074
	protected function handleTypeClosed(array $tag)
1075
	{
1076
		$this->pos2 = strpos($this->message, ']', $this->pos);
1077
		$tmp = $this->noSmileys($tag[Codes::ATTR_CONTENT]);
1078 14
		$this->message = substr_replace($this->message, $tmp, $this->pos, $this->pos2 + 1 - $this->pos);
0 ignored issues
show
Documentation Bug introduced by
It seems like substr_replace($this->me...>pos2 + 1 - $this->pos) can also be of type array. However, the property $message is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
1079
		$this->pos += strlen($tmp) - 1;
1080
1081 14
		return false;
1082
	}
1083 12
1084
	/**
1085
	 * Handle codes that are of the unparsed commas context type
1086
	 *
1087 14
	 * @param array $tag
1088
	 *
1089 2
	 * @return bool
1090
	 */
1091
	protected function handleUnparsedCommasContext(array $tag)
1092 14
	{
1093
		$this->pos2 = strpos($this->message, ']', $this->pos1);
1094 14
		if ($this->pos2 === false)
1095
		{
1096 14
			return true;
1097 14
		}
1098 14
1099 14
		$this->pos3 = stripos($this->message, '[/' . $tag[Codes::ATTR_TAG] . ']', $this->pos2);
1100
		if ($this->pos3 === false)
1101 14
		{
1102
			return true;
1103
		}
1104
1105
		// We want $1 to be the content, and the rest to be csv.
1106
		$data = explode(',', ',' . substr($this->message, $this->pos1, $this->pos2 - $this->pos1));
1107
		$data[0] = substr($this->message, $this->pos2 + 1, $this->pos3 - $this->pos2 - 1);
1108
1109
		if (isset($tag[Codes::ATTR_VALIDATE]))
1110
		{
1111 14
			$this->filterData($tag, $data);
1112
		}
1113 14
1114
		$code = $tag[Codes::ATTR_CONTENT];
1115 7
		foreach ($data as $k => $d)
1116 6
		{
1117
			$code = strtr($code, array('$' . ($k + 1) => trim($d)));
1118
		}
1119 7
1120 6
		$tmp = $this->noSmileys($code);
1121
		$this->message = substr_replace($this->message, $tmp, $this->pos, $this->pos3 + 3 + $tag[Codes::ATTR_LENGTH] - $this->pos);
0 ignored issues
show
Documentation Bug introduced by
It seems like substr_replace($this->me...R_LENGTH] - $this->pos) can also be of type array. However, the property $message is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
1122
		$this->pos += strlen($tmp) - 1;
1123 7
1124 2
		return false;
1125
	}
1126
1127 7
	/**
1128 4
	 * Handle codes that are of the unparsed commas type
1129
	 *
1130
	 * @param array $tag
1131 7
	 *
1132
	 * @return bool
1133
	 */
1134
	protected function handleUnparsedCommas(array $tag)
1135 7
	{
1136
		$this->pos2 = strpos($this->message, ']', $this->pos1);
1137
		if ($this->pos2 === false)
1138
		{
1139 7
			return true;
1140 7
		}
1141 14
1142
		$data = explode(',', substr($this->message, $this->pos1, $this->pos2 - $this->pos1));
1143
1144
		if (isset($tag[Codes::ATTR_VALIDATE]))
1145
		{
1146
			$this->filterData($tag, $data);
1147
		}
1148
1149
		// Fix after, for disabled code mainly.
1150
		foreach ($data as $k => $d)
1151
		{
1152 28
			$tag[Codes::ATTR_AFTER] = strtr($tag[Codes::ATTR_AFTER], array('$' . ($k + 1) => trim($d)));
1153
		}
1154
1155 28
		$this->addOpenTag($tag);
1156
1157
		// Replace them out, $1, $2, $3, $4, etc.
1158 28
		$code = $tag[Codes::ATTR_BEFORE];
1159
		foreach ($data as $k => $d)
1160
		{
1161
			$code = strtr($code, array('$' . ($k + 1) => trim($d)));
1162 28
		}
1163
1164 4
		$tmp = $this->noSmileys($code);
1165
		$this->message = substr_replace($this->message, $tmp, $this->pos, $this->pos2 + 1 - $this->pos);
0 ignored issues
show
Documentation Bug introduced by
It seems like substr_replace($this->me...>pos2 + 1 - $this->pos) can also be of type array. However, the property $message is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
1166
		$this->pos += strlen($tmp) - 1;
1167
1168 28
		return false;
1169
	}
1170
1171 2
	/**
1172
	 * Handle codes that are of the equals type
1173
	 *
1174 28
	 * @param array $tag
1175
	 *
1176 28
	 * @return bool
1177
	 */
1178
	protected function handleEquals(array $tag)
1179
	{
1180 28
		// The value may be quoted for some tags - check.
1181
		if (isset($tag[Codes::ATTR_QUOTED]))
1182
		{
1183 28
			$quoted = substr_compare($this->message, '&quot;', $this->pos1, 6) === 0;
1184
			if ($tag[Codes::ATTR_QUOTED] !== Codes::OPTIONAL && !$quoted)
1185 4
			{
1186
				return true;
1187
			}
1188 4
1189 4
			if ($quoted)
1190 4
			{
1191
				$this->pos1 += 6;
1192 28
			}
1193
		}
1194
		else
1195
		{
1196
			$quoted = false;
1197 2
		}
1198
1199 2
		$this->pos2 = strpos($this->message, $quoted ? '&quot;]' : ']', $this->pos1);
1200
		if ($this->pos2 === false)
1201
		{
1202 2
			return true;
1203
		}
1204 2
1205 2
		$data = substr($this->message, $this->pos1, $this->pos2 - $this->pos1);
1206 2
1207
		// Validation for my parking, please!
1208
		if (isset($tag[Codes::ATTR_VALIDATE]))
1209
		{
1210 2
			$this->filterData($tag, $data);
1211 2
		}
1212 2
1213
		// For parsed content, we must recurse to avoid security problems.
1214
		if ($tag[Codes::ATTR_TYPE] === Codes::TYPE_PARSED_EQUALS)
1215 2
		{
1216
			$data = $this->recursiveParser($data, $tag);
1217 2
		}
1218
1219 2
		$tag[Codes::ATTR_AFTER] = strtr($tag[Codes::ATTR_AFTER], array('$1' => $data));
1220
1221
		$this->addOpenTag($tag);
1222
1223
		$code = strtr($tag[Codes::ATTR_BEFORE], array('$1' => $data));
1224
		$tmp = $this->noSmileys($code);
1225
		$this->message = substr_replace($this->message, $tmp, $this->pos, $this->pos2 + ($quoted ? 7 : 1) - $this->pos);
0 ignored issues
show
Documentation Bug introduced by
It seems like substr_replace($this->me...d ? 7 : 1 - $this->pos) can also be of type array. However, the property $message is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
1226
		$this->pos += strlen($tmp) - 1;
1227
1228 2
		return false;
1229
	}
1230 2
1231 2
	/**
1232
	 * Handles a tag by its type. Offloads the actual handling to handle*() method
1233 2
	 *
1234
	 * @param array $tag
1235
	 *
1236
	 * @return bool true if there was something wrong and the parser should advance
1237
	 */
1238
	protected function handleTag(array $tag)
1239
	{
1240
		return match ($tag[Codes::ATTR_TYPE])
1241
		{
1242
			Codes::TYPE_PARSED_CONTENT => $this->handleTypeParsedContext($tag),
1243
			Codes::TYPE_UNPARSED_CONTENT => $this->handleTypeUnparsedContext($tag),
1244
			Codes::TYPE_UNPARSED_EQUALS_CONTENT => $this->handleUnparsedEqualsContext($tag),
1245
			Codes::TYPE_CLOSED => $this->handleTypeClosed($tag),
1246
			Codes::TYPE_UNPARSED_COMMAS_CONTENT => $this->handleUnparsedCommasContext($tag),
1247
			Codes::TYPE_UNPARSED_COMMAS => $this->handleUnparsedCommas($tag),
1248
			Codes::TYPE_PARSED_EQUALS, Codes::TYPE_UNPARSED_EQUALS => $this->handleEquals($tag),
1249
			default => false,
1250
		};
1251
1252
	}
1253
1254
	/**
1255
	 * Text between tags
1256
	 *
1257
	 * @todo I don't know what else to call this. It's the area that isn't a tag.
1258
	 */
1259
	protected function betweenTags()
1260
	{
1261
		// Make sure the $this->last_pos is not negative.
1262
		$this->last_pos = max($this->last_pos, 0);
1263
1264
		// Pick a block of data to do some raw fixing on.
1265
		$data = substr($this->message, $this->last_pos, $this->pos - $this->last_pos);
1266
1267 4
		// This happens when the pos is > last_pos and there is a trailing \n from one of the tags having "AFTER"
1268
		// In micro-optimization tests, using substr() here doesn't prove to be slower. This is much easier to read so leave it.
1269 4
		if ($data === $this->smiley_marker)
1270
		{
1271 4
			return;
1272 4
		}
1273 4
1274
		// Take care of some HTML!
1275 4
		if ($this->possible_html && strpos($data, '&lt;') !== false)
1276
		{
1277 4
			// @todo new \Parser\BBC\HTML;
1278 4
			$data = $this->parseHTML($data);
1279 4
		}
1280 4
1281
		// Take care of some Markdown!
1282
		if ($this->possible_markdown)
1283 4
		{
1284
			$data = $this->parseMarkdown($data);
1285
		}
1286
1287 4
		if (!empty($GLOBALS['modSettings']['autoLinkUrls']))
1288
		{
1289
			$data = $this->autoLink($data);
1290
		}
1291 4
1292
		// This cannot be moved earlier. It breaks tests
1293
		$data = str_replace("\t", '&nbsp;&nbsp;&nbsp;', $data);
1294 4
1295
		// If it wasn't changed, no copying or other boring stuff has to happen!
1296
		if (substr_compare($this->message, $data, $this->last_pos, $this->pos - $this->last_pos) !== 0)
1297
		{
1298 4
			$this->message = substr_replace($this->message, $data, $this->last_pos, $this->pos - $this->last_pos);
0 ignored issues
show
Documentation Bug introduced by
It seems like substr_replace($this->me...>pos - $this->last_pos) can also be of type array. However, the property $message is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
1299
1300
			// Since we changed it, look again in case we added or removed a tag.  But we don't want to skip any.
1301 4
			$old_pos = strlen($data) + $this->last_pos;
1302
			$this->pos = strpos($this->message, '[', $this->last_pos);
0 ignored issues
show
It seems like $this->message can also be of type array; however, parameter $haystack of strpos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1302
			$this->pos = strpos(/** @scrutinizer ignore-type */ $this->message, '[', $this->last_pos);
Loading history...
1303
			$this->pos = $this->pos === false ? $old_pos : min($this->pos, $old_pos);
1304
		}
1305
	}
1306 4
1307 4
	/**
1308
	 * Handles special [footnote] tag processing as the tag is not rendered "inline"
1309 4
	 */
1310
	protected function handleFootnotes()
1311 4
	{
1312
		static $fn_total = 0;
1313
1314 4
		// @todo temporary until we have nesting
1315 4
		$this->message = str_replace(array('[footnote]', '[/footnote]'), '', $this->message);
1316
1317 4
		$this->fn_num = 0;
1318
		$this->fn_content = array();
1319
		$this->fn_count = $fn_total;
1320
1321
		// Replace our footnote text with a [1] link, save the text for use at the end of the message
1322
		$this->message = preg_replace_callback('~(%fn%(.*?)%fn%)~is', fn(array $matches) => $this->footnoteCallback($matches), $this->message);
1323
		$fn_total += $this->fn_num;
1324
1325
		// If we have footnotes, add them in at the end of the message
1326
		if ($this->fn_num !== 0) // Set in callback
1327
		{
1328 4
			$this->message .= $this->smiley_marker . '<div class="bbc_footnotes">' . implode('', $this->fn_content) . '</div>' . $this->smiley_marker;
1329
		}
1330 4
	}
1331 4
1332
	/**
1333
	 * Final footnote conversions, builds the proper link code to footnote at base of post
1334 4
	 *
1335
	 * @param array $matches
1336 4
	 *
1337
	 * @return string
1338 4
	 */
1339
	protected function footnoteCallback(array $matches)
1340 3
	{
1341
		$this->fn_num++;
1342
		$this->fn_content[] = '<div class="target" id="fn' . $this->fn_num . '_' . $this->fn_count . '"><sup>' . $this->fn_num . '&nbsp;</sup>' . $matches[2] . '<a class="footnote_return" href="#ref' . $this->fn_num . '_' . $this->fn_count . '">&crarr;</a></div>';
1343
1344 4
		return '<a class="target" href="#fn' . $this->fn_num . '_' . $this->fn_count . '" id="ref' . $this->fn_num . '_' . $this->fn_count . '">[' . $this->fn_num . ']</a>';
1345
	}
1346 4
1347
	/**
1348
	 * Parse a tag that is disabled
1349
	 *
1350
	 * @param array $tag
1351
	 */
1352
	protected function handleDisabled(array &$tag)
1353
	{
1354
		if (!isset($tag[Codes::ATTR_DISABLED_BEFORE]) && !isset($tag[Codes::ATTR_DISABLED_AFTER]) && !isset($tag[Codes::ATTR_DISABLED_CONTENT]))
1355
		{
1356
			$tag[Codes::ATTR_BEFORE] = empty($tag[Codes::ATTR_BLOCK_LEVEL]) ? '' : '<div>';
1357
			$tag[Codes::ATTR_AFTER] = empty($tag[Codes::ATTR_BLOCK_LEVEL]) ? '' : '</div>';
1358
			$tag[Codes::ATTR_CONTENT] = $tag[Codes::ATTR_TYPE] === Codes::TYPE_CLOSED ? '' : (empty($tag[Codes::ATTR_BLOCK_LEVEL]) ? '$1' : '<div>$1</div>');
1359
		}
1360 4
		elseif (isset($tag[Codes::ATTR_DISABLED_BEFORE]) || isset($tag[Codes::ATTR_DISABLED_AFTER]))
1361
		{
1362
			$tag[Codes::ATTR_BEFORE] = $tag[Codes::ATTR_DISABLED_BEFORE] ?? (empty($tag[Codes::ATTR_BLOCK_LEVEL]) ? '' : '<div>');
1363 4
			$tag[Codes::ATTR_AFTER] = $tag[Codes::ATTR_DISABLED_AFTER] ?? (empty($tag[Codes::ATTR_BLOCK_LEVEL]) ? '' : '</div>');
1364
		}
1365
		else
1366 4
		{
1367
			$tag[Codes::ATTR_CONTENT] = $tag[Codes::ATTR_DISABLED_CONTENT];
1368
		}
1369 4
	}
1370
1371 2
	/**
1372
	 * Map required / optional tag parameters to the found tag
1373 2
	 *
1374
	 * @param array &$possible
1375
	 * @param array &$matches
1376
	 * @return bool
1377 3
	 */
1378
	protected function matchParameters(array &$possible, &$matches)
1379
	{
1380
		if (!isset($possible['regex_cache']))
1381 4
		{
1382
			$possible['regex_cache'] = array();
1383
			$possible['param_check'] = array();
1384
			$possible['optionals'] = array();
1385
1386
			foreach ($possible[Codes::ATTR_PARAM] as $param => $info)
1387
			{
1388
				$quote = empty($info[Codes::PARAM_ATTR_QUOTED]) ? '' : '&quot;';
1389
				$possible['optionals'][] = !empty($info[Codes::PARAM_ATTR_OPTIONAL]);
1390
				$possible['param_check'][] = ' ' . $param . '=' . $quote;
1391
				$possible['regex_cache'][] = '(\s+' . $param . '=' . $quote . ($info[Codes::PARAM_ATTR_MATCH] ?? '(.+?)') . $quote . ')';
1392
			}
1393 4
1394
			$possible['regex_size'] = count($possible[Codes::ATTR_PARAM]) - 1;
1395
		}
1396 4
1397
		// Tag setup for this loop
1398
		$this->tag_possible = $possible;
1399 4
1400
		// Okay, this may look ugly and it is, but it's not going to happen much and it is the best way
1401 4
		// of allowing any order of parameters but still parsing them right.
1402
		$message_stub = $this->messageStub();
1403
1404 4
		// Set regex optional flags only if the param *is* optional and it *was not used* in this tag
1405
		$this->optionalParam($message_stub);
1406
1407
		// If an addon adds many parameters we can exceed max_execution time, lets prevent that
1408
		// 5040 = 7, 40,320 = 8, (N!) etc
1409
		$max_iterations = self::MAX_PERMUTE_ITERATIONS;
1410
1411
		// Use the same range to start each time. Most BBC is in the order that it should be in when it starts.
1412
		$keys = $this->setKeys();
1413
1414
		// Step, one by one, through all possible permutations of the parameters until we have a match
1415
		do
1416 2
		{
1417
			$match_preg = '~^';
1418
			foreach ($keys as $key)
1419 2
			{
1420
				$match_preg .= $this->tag_possible['regex_cache'][$key];
1421 2
			}
1422
1423 2
			$match_preg .= '\]~i';
1424
1425
			// Check if this combination of parameters matches the user input
1426
			$match = preg_match($match_preg, $message_stub, $matches) !== 0;
1427 2
		} while (!$match && --$max_iterations && ($keys = pc_next_permutation($keys, $possible['regex_size'])));
1428 2
1429 2
		return $match;
1430
	}
1431 2
1432
	/**
1433 2
	 * Sorts the params so they are in a required to optional order.
1434
	 *
1435
	 * Supports the assumption that the params as defined in CODES is the preferred / common
1436
	 * order they are found in, and inserted by, the editor toolbar.
1437
	 *
1438
	 * @return array
1439
	 */
1440
	private function setKeys()
1441
	{
1442
		$control_order = array();
1443
		$this->tag_possible['regex_keys'] = range(0, $this->tag_possible['regex_size']);
1444
1445
		// Push optional params to the end of the stack but maintain current order of required ones
1446
		foreach (array_keys($this->tag_possible['regex_keys']) as $index)
1447
		{
1448
			$control_order[$index] = $index;
1449
1450
			if ($this->tag_possible['optionals'][$index])
1451
			{
1452
				$control_order[$index] = $index + $this->tag_possible['regex_size'];
1453 2
			}
1454
		}
1455 2
1456
		array_multisort($control_order, SORT_ASC, $this->tag_possible['regex_cache']);
0 ignored issues
show
BBC\SORT_ASC cannot be passed to array_multisort() as the parameter $rest expects a reference. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1456
		array_multisort($control_order, /** @scrutinizer ignore-type */ SORT_ASC, $this->tag_possible['regex_cache']);
Loading history...
1457 2
1458
		return $this->tag_possible['regex_keys'];
1459
	}
1460
1461
	/**
1462
	 * Sets the optional parameter search flag only where needed.
1463
	 *
1464
	 * What it does:
1465 14
	 *
1466
	 * - Sets the optional ()? flag only for optional params that were not actually used
1467 14
	 * - This makes the permutation function match all required *and* passed parameters
1468 14
	 * - Returns false if an non optional tag was not found
1469
	 *
1470
	 * @param string $message_stub
1471
	 */
1472
	protected function optionalParam($message_stub)
1473
	{
1474
		// Set optional flag only if the param is optional and it was not used in this tag
1475 28
		foreach ($this->tag_possible['optionals'] as $index => $optional)
1476
		{
1477 28
			// @todo more robust, and slower, check would be a preg_match on $possible['regex_cache'][$index]
1478
			$param_exists = stripos($message_stub, (string) $this->tag_possible['param_check'][$index]) !== false;
1479 28
1480
			// Only make unused optional tags as optional
1481
			if ($optional)
1482
			{
1483
				if ($param_exists)
1484
				{
1485
					$this->tag_possible['optionals'][$index] = false;
1486
				}
1487
				else
1488
				{
1489
					$this->tag_possible['regex_cache'][$index] .= '?';
1490
				}
1491
			}
1492
		}
1493
	}
1494
1495 16
	/**
1496
	 * Given the position in a message, extracts the tag for analysis
1497 16
	 *
1498
	 * What it does:
1499
	 *
1500
	 * - Given ' width=100 height=100 alt=image]....[/img]more text and [tags]...'
1501
	 * - Returns ' width=100 height=100 alt=image]....[/img]'
1502
	 *
1503
	 * @return string
1504
	 */
1505 6
	protected function messageStub()
1506
	{
1507 6
		// For parameter searching, swap in \n's to reduce any regex greediness
1508
		$message_stub = str_replace('<br />', "\n", substr($this->message, $this->pos1 - 1)) . "\n";
1509
1510
		// Attempt to pull out just this tag
1511
		if (preg_match('~^(?:.+?)\](?>.|(?R))*?\[\/' . $this->tag_possible[Codes::ATTR_TAG] . '\](?:.|\s)~i', $message_stub, $matches) === 1)
1512
		{
1513
			return $matches[0];
1514
		}
1515
1516
		return $message_stub;
1517 14
	}
1518
1519 14
	/**
1520
	 * Recursively call the parser with a new Codes object
1521 14
	 * This allows to parse BBC in parameters like [quote author="[url]www.quotes.com[/url]"]Something famous.[/quote]
1522
	 *
1523
	 * @param string $data
1524
	 * @param array $tag
1525
	 *
1526
	 * @return string
1527
	 */
1528
	protected function recursiveParser($data, array $tag)
1529
	{
1530
		// @todo if parsed tags allowed is empty, return?
1531
		$bbc = clone $this->bbc;
1532
1533
		if (!empty($tag[Codes::ATTR_PARSED_TAGS_ALLOWED]))
1534
		{
1535
			$bbc->setParsedTags($tag[Codes::ATTR_PARSED_TAGS_ALLOWED]);
1536
		}
1537
1538 4
		// Do not use $this->autolinker. For some reason it causes a recursive loop
1539
		$autolinker = null;
1540 4
		$html = null;
1541
		call_integration_hook('integrate_recursive_bbc_parser', array(&$autolinker, &$html));
1542
1543
		$parser = new BBCParser($bbc, $autolinker, $html);
1544 4
1545
		return $parser->enableSmileys(empty($tag[Codes::ATTR_PARSED_TAGS_ALLOWED]))->parse($data);
1546
	}
1547
1548
	/**
1549
	 * Return the BBC codes in the system
1550
	 *
1551
	 * @return array
1552 2
	 */
1553
	public function getBBC()
1554 2
	{
1555 2
		return $this->bbc_codes;
1556
	}
1557 2
1558
	/**
1559 2
	 * Enable the parsing of smileys
1560
	 *
1561 2
	 * @param bool $enable
1562
	 *
1563 2
	 * @return $this
1564
	 */
1565 2
	public function enableSmileys($enable = true)
1566
	{
1567
		$this->do_smileys = (bool) $enable;
1568
1569 2
		return $this;
1570
	}
1571
1572
	/**
1573 2
	 * Open a tag
1574
	 *
1575
	 * @param array $tag
1576 2
	 */
1577
	protected function addOpenTag(array $tag)
1578 2
	{
1579
		$this->open_tags[] = $tag;
1580 2
	}
1581
1582
	/**
1583
	 * @param string|bool $tag = false False closes the last open tag. Anything else finds that tag LIFO
1584
	 *
1585 2
	 * @return mixed
1586
	 */
1587
	protected function closeOpenedTag($tag = false)
1588 2
	{
1589
		if ($tag === false)
1590 2
		{
1591
			return array_pop($this->open_tags);
1592
		}
1593 2
1594
		if (isset($this->open_tags[$tag]))
1595 2
		{
1596
			$return = $this->open_tags[$tag];
1597
			unset($this->open_tags[$tag]);
1598 2
1599
			return $return;
1600 2
		}
1601
	}
1602
1603 2
	/**
1604
	 * Check if there are any tags that are open
1605 2
	 *
1606
	 * @return bool
1607
	 */
1608
	protected function hasOpenTags()
1609
	{
1610
		return !empty($this->open_tags);
1611
	}
1612
1613
	/**
1614
	 * Get the last opened tag
1615
	 *
1616
	 * @return string
1617
	 */
1618
	protected function getLastOpenedTag()
1619
	{
1620
		return end($this->open_tags);
1621
	}
1622
1623
	/**
1624
	 * Get the currently opened tags
1625
	 *
1626
	 * @param bool|false $tags_only True if you want just the tag or false for the whole code
1627
	 *
1628
	 * @return array
1629
	 */
1630
	protected function getOpenedTags($tags_only = false)
1631
	{
1632
		if (!$tags_only)
1633
		{
1634
			return $this->open_tags;
1635 16
		}
1636
1637 16
		$tags = array();
1638
		foreach ($this->open_tags as $tag)
1639
		{
1640
			$tags[] = $tag[Codes::ATTR_TAG];
1641
		}
1642
1643
		return $tags;
1644 6
	}
1645
1646 6
	/**
1647 6
	 * Does what it says, removes whitespace
1648
	 *
1649
	 * @param null|int $offset = null
1650
	 */
1651
	protected function trimWhiteSpace($offset = null)
1652
	{
1653 6
		if (preg_match('~(<br />|&nbsp;|\s)*~', $this->message, $matches, 0, $offset) === 0)
0 ignored issues
show
It seems like $offset can also be of type null; however, parameter $offset of preg_match() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1653
		if (preg_match('~(<br />|&nbsp;|\s)*~', $this->message, $matches, 0, /** @scrutinizer ignore-type */ $offset) === 0)
Loading history...
1654
		{
1655
			return;
1656
		}
1657
		if (!isset($matches[0]))
1658
		{
1659
			return;
1660
		}
1661
		if ($matches[0] === '')
1662
		{
1663
			return;
1664
		}
1665
1666
		$this->message = substr_replace($this->message, '', $offset, strlen($matches[0]));
0 ignored issues
show
It seems like $offset can also be of type null; however, parameter $offset of substr_replace() does only seem to accept array|integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1666
		$this->message = substr_replace($this->message, '', /** @scrutinizer ignore-type */ $offset, strlen($matches[0]));
Loading history...
Documentation Bug introduced by
It seems like substr_replace($this->me...t, strlen($matches[0])) can also be of type array. However, the property $message is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
1667
	}
1668
1669
	/**
1670
	 * Substitutes parameter attribute values in to the tag context
1671
	 * e.g. tag width={width} => tag width=300px
1672
	 *
1673
	 * @param array $possible
1674 6
	 * @param array $matches
1675
	 *
1676
	 * @return array
1677
	 */
1678
	protected function setupTagParameters(array $possible, array $matches)
1679
	{
1680
		$params = array();
1681
		for ($i = 1, $n = count($matches); $i < $n; $i += 2)
1682
		{
1683 14
			$key = strtok(ltrim($matches[$i]), '=');
1684
1685 14
			if (isset($possible[Codes::ATTR_PARAM][$key][Codes::PARAM_ATTR_VALUE]))
1686
			{
1687
				$params['{' . $key . '}'] = strtr($possible[Codes::ATTR_PARAM][$key][Codes::PARAM_ATTR_VALUE], array('$1' => $matches[$i + 1]));
1688
			}
1689
			elseif (isset($possible[Codes::ATTR_PARAM][$key][Codes::PARAM_ATTR_VALIDATE]))
1690
			{
1691
				$params['{' . $key . '}'] = $possible[Codes::ATTR_PARAM][$key][Codes::PARAM_ATTR_VALIDATE]($matches[$i + 1]);
1692
			}
1693
			else
1694
			{
1695
				$params['{' . $key . '}'] = $matches[$i + 1];
1696
			}
1697
1698
			// Just to make sure: replace any $ or { so they can't interpolate wrongly.
1699
			$params['{' . $key . '}'] = str_replace(array('$', '{'), array('&#036;', '&#123;'), $params['{' . $key . '}']);
1700
		}
1701
1702
		foreach ($possible[Codes::ATTR_PARAM] as $p => $info)
1703
		{
1704 14
			if (!isset($params['{' . $p . '}']))
1705
			{
1706 14
				$params['{' . $p . '}'] = '';
1707 14
			}
1708
		}
1709
1710
		// We found our tag
1711
		$tag = $possible;
1712
1713
		// Put the parameters into the string.
1714
		if (isset($tag[Codes::ATTR_BEFORE]))
1715
		{
1716
			$tag[Codes::ATTR_BEFORE] = strtr($tag[Codes::ATTR_BEFORE], $params);
1717
		}
1718
1719
		if (isset($tag[Codes::ATTR_AFTER]))
1720
		{
1721
			$tag[Codes::ATTR_AFTER] = strtr($tag[Codes::ATTR_AFTER], $params);
1722
		}
1723
1724
		if (isset($tag[Codes::ATTR_CONTENT]))
1725
		{
1726
			$tag[Codes::ATTR_CONTENT] = strtr($tag[Codes::ATTR_CONTENT], $params);
1727
		}
1728
1729
		$this->pos1 += strlen($matches[0]) - 1;
1730
1731
		return $tag;
1732
	}
1733
1734
	/**
1735
	 * Check if a tag (not a code) is open
1736
	 *
1737
	 * @param string $tag
1738
	 *
1739
	 * @return bool
1740
	 */
1741
	protected function isOpen($tag)
1742
	{
1743
		foreach ($this->open_tags as $open)
1744
		{
1745
			if ($open[Codes::ATTR_TAG] === $tag)
1746
			{
1747
				return true;
1748
			}
1749
		}
1750
1751
		return false;
1752
	}
1753
1754
	/**
1755
	 * Check if a character is an item code
1756
	 *
1757
	 * @param string $char
1758
	 *
1759
	 * @return bool
1760
	 */
1761
	protected function isItemCode($char)
1762
	{
1763
		return isset($this->item_codes[$char]);
1764
	}
1765
1766
	/**
1767
	 * Close any open codes that aren't block level.
1768
	 * Used before opening a code that *is* block level
1769
	 */
1770
	protected function closeNonBlockLevel()
1771
	{
1772
		$n = count($this->open_tags) - 1;
1773
		while (empty($this->open_tags[$n][Codes::ATTR_BLOCK_LEVEL]) && $n >= 0)
1774
		{
1775
			$n--;
1776
		}
1777
1778
		// Close all the non block level tags so this tag isn't surrounded by them.
1779
		for ($i = count($this->open_tags) - 1; $i > $n; $i--)
1780
		{
1781
			$tmp = isset($this->open_tags[$i][Codes::ATTR_AFTER])
1782
				? $this->noSmileys($this->open_tags[$i][Codes::ATTR_AFTER])
1783
				: $this->noSmileys('');
1784
			$this->message = substr_replace($this->message, $tmp, $this->pos, 0);
0 ignored issues
show
Documentation Bug introduced by
It seems like substr_replace($this->me...e, $tmp, $this->pos, 0) can also be of type array. However, the property $message is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
1785
			$ot_strlen = strlen($tmp);
1786
			$this->pos += $ot_strlen;
1787
			$this->pos1 += $ot_strlen;
1788
1789
			// Trim or eat trailing stuff... see comment at the end of the big loop.
1790
			if (!empty($this->open_tags[$i][Codes::ATTR_BLOCK_LEVEL]) && substr_compare($this->message, '<br />', $this->pos, 6) === 0)
0 ignored issues
show
It seems like $this->message can also be of type array; however, parameter $haystack of substr_compare() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1790
			if (!empty($this->open_tags[$i][Codes::ATTR_BLOCK_LEVEL]) && substr_compare(/** @scrutinizer ignore-type */ $this->message, '<br />', $this->pos, 6) === 0)
Loading history...
1791
			{
1792
				$this->message = substr_replace($this->message, '', $this->pos, 6);
1793
			}
1794
1795
			if (isset($tag[Codes::ATTR_TRIM]) && $tag[Codes::ATTR_TRIM] !== Codes::TRIM_INSIDE)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $tag seems to never exist and therefore isset should always be false.
Loading history...
1796
			{
1797
				$this->trimWhiteSpace($this->pos);
1798
			}
1799
1800
			$this->closeOpenedTag();
1801
		}
1802
	}
1803
1804
	/**
1805
	 * Add markers around a string to denote that smileys should not be parsed
1806
	 *
1807
	 * @param string $string
1808
	 *
1809
	 * @return string
1810
	 */
1811
	protected function noSmileys($string)
1812
	{
1813
		return $this->smiley_marker . $string . $this->smiley_marker;
1814
	}
1815
1816
	/**
1817
	 * Checks if we can cache, some codes prevent this and require parsing each time
1818
	 *
1819
	 * @return bool
1820
	 */
1821
	public function canCache()
1822
	{
1823
		return $this->can_cache;
1824
	}
1825
1826
	/**
1827
	 * This calls Codes::ATTR_VALIDATE.
1828
	 *
1829
	 * @param array $tag
1830
	 * @param $data
1831
	 */
1832
	protected function filterData(array &$tag, &$data)
1833
	{
1834
		$tag[Codes::ATTR_VALIDATE]($data, $this->bbc->getDisabled(), $tag);
1835
	}
1836
}
1837