Verzeichnisstruktur phpBB-3.3.15
- Veröffentlicht
- 28.08.2024
So funktioniert es
|
Auf das letzte Element klicken. Dies geht jeweils ein Schritt zurück |
Auf das Icon klicken, dies öffnet das Verzeichnis. Nochmal klicken schließt das Verzeichnis. |
|
(Beispiel Datei-Icons)
|
Auf das Icon klicken um den Quellcode anzuzeigen |
BBCodeMonkey.php
001 <?php
002
003 /**
004 * @package s9e\TextFormatter
005 * @copyright Copyright (c) 2010-2022 The s9e authors
006 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
007 */
008 namespace s9e\TextFormatter\Plugins\BBCodes\Configurator;
009
010 use Exception;
011 use InvalidArgumentException;
012 use RuntimeException;
013 use s9e\TextFormatter\Configurator;
014 use s9e\TextFormatter\Configurator\Helpers\RegexpBuilder;
015 use s9e\TextFormatter\Configurator\Items\Attribute;
016 use s9e\TextFormatter\Configurator\Items\ProgrammableCallback;
017 use s9e\TextFormatter\Configurator\Items\Tag;
018 use s9e\TextFormatter\Configurator\Items\Template;
019
020 class BBCodeMonkey
021 {
022 /**
023 * Expression that matches a regexp such as /foo/i
024 */
025 const REGEXP = '(.).*?(?<!\\\\)(?>\\\\\\\\)*+\\g{-1}[DSUisu]*';
026
027 /**
028 * @var array List of pre- and post- filters that are explicitly allowed in BBCode definitions.
029 * We use a whitelist approach because there are so many different risky callbacks
030 * that it would be too easy to let something dangerous slip by, e.g.: unlink,
031 * system, etc...
032 */
033 public $allowedFilters = [
034 'addslashes',
035 'dechex',
036 'intval',
037 'json_encode',
038 'ltrim',
039 'mb_strtolower',
040 'mb_strtoupper',
041 'rawurlencode',
042 'rtrim',
043 'str_rot13',
044 'stripslashes',
045 'strrev',
046 'strtolower',
047 'strtotime',
048 'strtoupper',
049 'trim',
050 'ucfirst',
051 'ucwords',
052 'urlencode'
053 ];
054
055 /**
056 * @var Configurator Instance of Configurator;
057 */
058 protected $configurator;
059
060 /**
061 * @var array Regexps used in the named subpatterns generated automatically for composite
062 * attributes. For instance, "foo={NUMBER},{NUMBER}" will be transformed into
063 * 'foo={PARSE=#^(?<foo0>\\d+),(?<foo1>\\d+)$#D}'
064 */
065 public $tokenRegexp = [
066 'ANYTHING' => '[\\s\\S]*?',
067 'COLOR' => '[a-zA-Z]+|#[0-9a-fA-F]+',
068 'EMAIL' => '[^@]+@.+?',
069 'FLOAT' => '(?>0|-?[1-9]\\d*)(?>\\.\\d+)?(?>e[1-9]\\d*)?',
070 'ID' => '[-a-zA-Z0-9_]+',
071 'IDENTIFIER' => '[-a-zA-Z0-9_]+',
072 'INT' => '0|-?[1-9]\\d*',
073 'INTEGER' => '0|-?[1-9]\\d*',
074 'NUMBER' => '\\d+',
075 'RANGE' => '\\d+',
076 'SIMPLETEXT' => '[-a-zA-Z0-9+.,_ ]+',
077 'TEXT' => '[\\s\\S]*?',
078 'UINT' => '0|[1-9]\\d*'
079 ];
080
081 /**
082 * @var array List of token types that are used to represent raw, unfiltered content
083 */
084 public $unfilteredTokens = [
085 'ANYTHING',
086 'TEXT'
087 ];
088
089 /**
090 * Constructor
091 *
092 * @param Configurator $configurator Instance of Configurator
093 */
094 public function __construct(Configurator $configurator)
095 {
096 $this->configurator = $configurator;
097 }
098
099 /**
100 * Create a BBCode and its underlying tag and template(s) based on its reference usage
101 *
102 * @param string $usage BBCode usage, e.g. [B]{TEXT}[/b]
103 * @param string|Template $template BBCode's template
104 * @return array An array containing three elements: 'bbcode', 'bbcodeName'
105 * and 'tag'
106 */
107 public function create($usage, $template)
108 {
109 // Parse the BBCode usage
110 $config = $this->parse($usage);
111
112 // Create a template object for manipulation
113 if (!($template instanceof Template))
114 {
115 $template = new Template($template);
116 }
117
118 // Replace the passthrough token in the BBCode's template
119 $template->replaceTokens(
120 '#\\{(?:[A-Z]+[A-Z_0-9]*|@[-\\w]+)\\}#',
121 function ($m) use ($config)
122 {
123 $tokenId = substr($m[0], 1, -1);
124
125 // Acknowledge {@foo} as an XPath expression even outside of attribute value
126 // templates
127 if ($tokenId[0] === '@')
128 {
129 return ['expression', $tokenId];
130 }
131
132 // Test whether this is a known token
133 if (isset($config['tokens'][$tokenId]))
134 {
135 // Replace with the corresponding attribute
136 return ['expression', '@' . $config['tokens'][$tokenId]];
137 }
138
139 // Test whether the token is used as passthrough
140 if ($tokenId === $config['passthroughToken'])
141 {
142 return ['passthrough'];
143 }
144
145 // Undefined token. If it's the name of a filter, consider it's an error
146 if ($this->isFilter($tokenId))
147 {
148 throw new RuntimeException('Token {' . $tokenId . '} is ambiguous or undefined');
149 }
150
151 // Use the token's name as parameter name
152 return ['expression', '$' . $tokenId];
153 }
154 );
155
156 // Prepare the return array
157 $return = [
158 'bbcode' => $config['bbcode'],
159 'bbcodeName' => $config['bbcodeName'],
160 'tag' => $config['tag']
161 ];
162
163 // Set the template for this BBCode's tag
164 $return['tag']->template = $template;
165
166 return $return;
167 }
168
169 /**
170 * Create a BBCode based on its reference usage
171 *
172 * @param string $usage BBCode usage, e.g. [B]{TEXT}[/b]
173 * @return array
174 */
175 protected function parse($usage)
176 {
177 $tag = new Tag;
178 $bbcode = new BBCode;
179
180 // This is the config we will return
181 $config = [
182 'tag' => $tag,
183 'bbcode' => $bbcode,
184 'passthroughToken' => null
185 ];
186
187 // Encode maps to avoid special characters to interfere with definitions
188 $usage = preg_replace_callback(
189 '#(\\{(?>HASH)?MAP=)([^:]+:[^,;}]+(?>,[^:]+:[^,;}]+)*)(?=[;}])#',
190 function ($m)
191 {
192 return $m[1] . base64_encode($m[2]);
193 },
194 $usage
195 );
196
197 // Encode regexps to avoid special characters to interfere with definitions
198 $usage = preg_replace_callback(
199 '#(\\{(?:PARSE|REGEXP)=)(' . self::REGEXP . '(?:,' . self::REGEXP . ')*)#',
200 function ($m)
201 {
202 return $m[1] . base64_encode($m[2]);
203 },
204 $usage
205 );
206
207 $regexp = '(^'
208 // [BBCODE
209 . '\\[(?<bbcodeName>\\S+?)'
210 // ={TOKEN}
211 . '(?<defaultAttribute>=.+?)?'
212 // foo={TOKEN} bar={TOKEN1},{TOKEN2}
213 . '(?<attributes>(?:\\s+[^=]+=\\S+?)*?)?'
214 // ] or /] or ]{TOKEN}[/BBCODE]
215 . '\\s*(?:/?\\]|\\]\\s*(?<content>.*?)\\s*(?<endTag>\\[/\\1]))'
216 . '$)i';
217
218 if (!preg_match($regexp, trim($usage), $m))
219 {
220 throw new InvalidArgumentException('Cannot interpret the BBCode definition');
221 }
222
223 // Save the BBCode's name
224 $config['bbcodeName'] = BBCode::normalizeName($m['bbcodeName']);
225
226 // Prepare the attributes definition, e.g. "foo={BAR}"
227 $definitions = preg_split('#\\s+#', trim($m['attributes']), -1, PREG_SPLIT_NO_EMPTY);
228
229 // If there's a default attribute, we prepend it to the list using the BBCode's name as
230 // attribute name
231 if (!empty($m['defaultAttribute']))
232 {
233 array_unshift($definitions, $m['bbcodeName'] . $m['defaultAttribute']);
234 }
235
236 // Append the content token to the attributes list under the name "content" if it's anything
237 // but raw {TEXT} (or other unfiltered tokens)
238 if (!empty($m['content']))
239 {
240 $regexp = '#^\\{' . RegexpBuilder::fromList($this->unfilteredTokens) . '[0-9]*\\}$#D';
241
242 if (preg_match($regexp, $m['content']))
243 {
244 $config['passthroughToken'] = substr($m['content'], 1, -1);
245 }
246 else
247 {
248 $definitions[] = 'content=' . $m['content'];
249 $bbcode->contentAttributes[] = 'content';
250 }
251 }
252
253 // Separate the attribute definitions from the BBCode options
254 $attributeDefinitions = [];
255 foreach ($definitions as $definition)
256 {
257 $pos = strpos($definition, '=');
258 $name = substr($definition, 0, $pos);
259 $value = preg_replace('(^"(.*?)")s', '$1', substr($definition, 1 + $pos));
260
261 // Decode base64-encoded tokens
262 $value = preg_replace_callback(
263 '#(\\{(?>HASHMAP|MAP|PARSE|REGEXP)=)([A-Za-z0-9+/]+=*)#',
264 function ($m)
265 {
266 return $m[1] . base64_decode($m[2]);
267 },
268 $value
269 );
270
271 // If name starts with $ then it's a BBCode/tag option. If it starts with # it's a rule.
272 // Otherwise, it's an attribute definition
273 if ($name[0] === '$')
274 {
275 $optionName = substr($name, 1);
276
277 // Allow nestingLimit and tagLimit to be set on the tag itself. We don't necessarily
278 // want every other tag property to be modifiable this way, though
279 $object = ($optionName === 'nestingLimit' || $optionName === 'tagLimit') ? $tag : $bbcode;
280
281 $object->$optionName = $this->convertValue($value);
282 }
283 elseif ($name[0] === '#')
284 {
285 $ruleName = substr($name, 1);
286
287 // Supports #denyChild=foo,bar
288 foreach (explode(',', $value) as $value)
289 {
290 $tag->rules->$ruleName($this->convertValue($value));
291 }
292 }
293 else
294 {
295 $attrName = strtolower(trim($name));
296 $attributeDefinitions[] = [$attrName, $value];
297 }
298 }
299
300 // Add the attributes and get the token translation table
301 $tokens = $this->addAttributes($attributeDefinitions, $bbcode, $tag);
302
303 // Test whether the passthrough token is used for something else, in which case we need
304 // to unset it
305 if (isset($tokens[$config['passthroughToken']]))
306 {
307 $config['passthroughToken'] = null;
308 }
309
310 // Add the list of known (and only the known) tokens to the config
311 $config['tokens'] = array_filter($tokens);
312
313 return $config;
314 }
315
316 /**
317 * Parse a string of attribute definitions and add the attributes/options to the tag/BBCode
318 *
319 * Attributes come in two forms. Most commonly, in the form of a single token, e.g.
320 * [a href={URL} title={TEXT}]
321 *
322 * Sometimes, however, we need to parse more than one single token. For instance, the phpBB
323 * [FLASH] BBCode uses two tokens separated by a comma:
324 * [flash={NUMBER},{NUMBER}]{URL}[/flash]
325 *
326 * In addition, some custom BBCodes circulating for phpBB use a combination of token and static
327 * text such as:
328 * [youtube]http://www.youtube.com/watch?v={SIMPLETEXT}[/youtube]
329 *
330 * Any attribute that is not a single token is implemented as an attribute preprocessor, with
331 * each token generating a matching attribute. Tentatively, those of those attributes are
332 * created by taking the attribute preprocessor's name and appending a unique number counting the
333 * number of created attributes. In the [FLASH] example above, an attribute preprocessor named
334 * "flash" would be created as well as two attributes named "flash0" and "flash1" respectively.
335 *
336 * @link https://www.phpbb.com/community/viewtopic.php?f=46&t=2127991
337 * @link https://www.phpbb.com/community/viewtopic.php?f=46&t=579376
338 *
339 * @param array $definitions List of attributes definitions as [[name, definition]*]
340 * @param BBCode $bbcode Owner BBCode
341 * @param Tag $tag Owner tag
342 * @return array Array of [token id => attribute name] where FALSE in place of the
343 * name indicates that the token is ambiguous (e.g. used multiple
344 * times)
345 */
346 protected function addAttributes(array $definitions, BBCode $bbcode, Tag $tag)
347 {
348 /**
349 * @var array List of composites' tokens. Each element is composed of an attribute name, the
350 * composite's definition and an array of tokens
351 */
352 $composites = [];
353
354 /**
355 * @var array Map of [tokenId => attrName]. If the same token is used in multiple attributes
356 * it is set to FALSE
357 */
358 $table = [];
359
360 foreach ($definitions as list($attrName, $definition))
361 {
362 // The first attribute defined is set as default
363 if (!isset($bbcode->defaultAttribute))
364 {
365 $bbcode->defaultAttribute = $attrName;
366 }
367
368 // Parse the tokens in that definition
369 $tokens = $this->parseTokens($definition);
370
371 if (empty($tokens))
372 {
373 throw new RuntimeException('No valid tokens found in ' . $attrName . "'s definition " . $definition);
374 }
375
376 // Test whether this attribute has one single all-encompassing token
377 if ($tokens[0]['content'] === $definition)
378 {
379 $token = $tokens[0];
380
381 if ($token['type'] === 'PARSE')
382 {
383 foreach ($token['regexps'] as $regexp)
384 {
385 $tag->attributePreprocessors->add($attrName, $regexp);
386 }
387 }
388 elseif (isset($tag->attributes[$attrName]))
389 {
390 throw new RuntimeException("Attribute '" . $attrName . "' is declared twice");
391 }
392 else
393 {
394 // Remove the "useContent" option and add the attribute's name to the list of
395 // attributes to use this BBCode's content
396 if (!empty($token['options']['useContent']))
397 {
398 $bbcode->contentAttributes[] = $attrName;
399 }
400 unset($token['options']['useContent']);
401
402 // Add the attribute
403 $tag->attributes[$attrName] = $this->generateAttribute($token);
404
405 // Record the token ID if applicable
406 $tokenId = $token['id'];
407 $table[$tokenId] = (isset($table[$tokenId]))
408 ? false
409 : $attrName;
410 }
411 }
412 else
413 {
414 $composites[] = [$attrName, $definition, $tokens];
415 }
416 }
417
418 foreach ($composites as list($attrName, $definition, $tokens))
419 {
420 $regexp = '/^';
421 $lastPos = 0;
422
423 $usedTokens = [];
424
425 foreach ($tokens as $token)
426 {
427 $tokenId = $token['id'];
428 $tokenType = $token['type'];
429
430 if ($tokenType === 'PARSE')
431 {
432 // Disallow {PARSE} tokens because attribute preprocessors cannot feed into
433 // other attribute preprocessors
434 throw new RuntimeException('{PARSE} tokens can only be used has the sole content of an attribute');
435 }
436
437 // Ensure that tokens are only used once per definition so we don't have multiple
438 // subpatterns using the same name
439 if (isset($usedTokens[$tokenId]))
440 {
441 throw new RuntimeException('Token {' . $tokenId . '} used multiple times in attribute ' . $attrName . "'s definition");
442 }
443 $usedTokens[$tokenId] = 1;
444
445 // Find the attribute name associated with this token, or create an attribute
446 // otherwise
447 if (isset($table[$tokenId]))
448 {
449 $matchName = $table[$tokenId];
450
451 if ($matchName === false)
452 {
453 throw new RuntimeException('Token {' . $tokenId . "} used in attribute '" . $attrName . "' is ambiguous");
454 }
455 }
456 else
457 {
458 // The name of the named subpattern and the corresponding attribute is based on
459 // the attribute preprocessor's name, with an incremented ID that ensures we
460 // don't overwrite existing attributes
461 $i = 0;
462 do
463 {
464 $matchName = $attrName . $i;
465 ++$i;
466 }
467 while (isset($tag->attributes[$matchName]));
468
469 // Create the attribute that corresponds to this subpattern
470 $attribute = $tag->attributes->add($matchName);
471
472 // Append the corresponding filter if applicable
473 if (!in_array($tokenType, $this->unfilteredTokens, true))
474 {
475 $filter = $this->configurator->attributeFilters->get('#' . strtolower($tokenType));
476 $attribute->filterChain->append($filter);
477 }
478
479 // Record the attribute name associated with this token ID
480 $table[$tokenId] = $matchName;
481 }
482
483 // Append the literal text between the last position and current position.
484 // Replace whitespace with a flexible whitespace pattern
485 $literal = preg_quote(substr($definition, $lastPos, $token['pos'] - $lastPos), '/');
486 $literal = preg_replace('(\\s+)', '\\s+', $literal);
487 $regexp .= $literal;
488
489 // Grab the expression that corresponds to the token type, or use a catch-all
490 // expression otherwise
491 $expr = (isset($this->tokenRegexp[$tokenType]))
492 ? $this->tokenRegexp[$tokenType]
493 : '.+?';
494
495 // Append the named subpattern. Its name is made of the attribute preprocessor's
496 // name and the subpattern's position
497 $regexp .= '(?<' . $matchName . '>' . $expr . ')';
498
499 // Update the last position
500 $lastPos = $token['pos'] + strlen($token['content']);
501 }
502
503 // Append the literal text that follows the last token and finish the regexp
504 $regexp .= preg_quote(substr($definition, $lastPos), '/') . '$/D';
505
506 // Add the attribute preprocessor to the config
507 $tag->attributePreprocessors->add($attrName, $regexp);
508 }
509
510 // Now create attributes generated from attribute preprocessors. For instance, preprocessor
511 // #(?<width>\\d+),(?<height>\\d+)# will generate two attributes named "width" and height
512 // with a regexp filter "#^(?:\\d+)$#D", unless they were explicitly defined otherwise
513 $newAttributes = [];
514 foreach ($tag->attributePreprocessors as $attributePreprocessor)
515 {
516 foreach ($attributePreprocessor->getAttributes() as $attrName => $regexp)
517 {
518 if (isset($tag->attributes[$attrName]))
519 {
520 // This attribute was already explicitly defined, nothing else to add
521 continue;
522 }
523
524 if (isset($newAttributes[$attrName])
525 && $newAttributes[$attrName] !== $regexp)
526 {
527 throw new RuntimeException("Ambiguous attribute '" . $attrName . "' created using different regexps needs to be explicitly defined");
528 }
529
530 $newAttributes[$attrName] = $regexp;
531 }
532 }
533
534 foreach ($newAttributes as $attrName => $regexp)
535 {
536 $filter = $this->configurator->attributeFilters->get('#regexp');
537
538 // Create the attribute using this regexp as filter
539 $tag->attributes->add($attrName)->filterChain->append($filter)->setRegexp($regexp);
540 }
541
542 return $table;
543 }
544
545 /**
546 * Convert a human-readable value to a typed PHP value
547 *
548 * @param string $value Original value
549 * @return bool|string Converted value
550 */
551 protected function convertValue($value)
552 {
553 if ($value === 'true')
554 {
555 return true;
556 }
557
558 if ($value === 'false')
559 {
560 return false;
561 }
562
563 return $value;
564 }
565
566 /**
567 * Parse and return all the tokens contained in a definition
568 *
569 * @param string $definition
570 * @return array
571 */
572 protected function parseTokens($definition)
573 {
574 $tokenTypes = [
575 'choice' => 'CHOICE[0-9]*=(?<choices>.+?)',
576 'map' => '(?:HASH)?MAP[0-9]*=(?<map>.+?)',
577 'parse' => 'PARSE=(?<regexps>' . self::REGEXP . '(?:,' . self::REGEXP . ')*)',
578 'range' => 'RANGE[0-9]*=(?<min>-?[0-9]+),(?<max>-?[0-9]+)',
579 'regexp' => 'REGEXP[0-9]*=(?<regexp>' . self::REGEXP . ')',
580 'other' => '(?<other>[A-Z_]+[0-9]*)'
581 ];
582
583 // Capture the content of every token in that attribute's definition. Usually there will
584 // only be one, as in "foo={URL}" but some older BBCodes use a form of composite
585 // attributes such as [FLASH={NUMBER},{NUMBER}]
586 preg_match_all(
587 '#\\{(' . implode('|', $tokenTypes) . ')(?<options>\\??(?:;[^;]*)*)\\}#',
588 $definition,
589 $matches,
590 PREG_SET_ORDER | PREG_OFFSET_CAPTURE
591 );
592
593 $tokens = [];
594 foreach ($matches as $m)
595 {
596 if (isset($m['other'][0])
597 && preg_match('#^(?:CHOICE|HASHMAP|MAP|REGEXP|PARSE|RANGE)#', $m['other'][0]))
598 {
599 throw new RuntimeException("Malformed token '" . $m['other'][0] . "'");
600 }
601
602 $token = [
603 'pos' => $m[0][1],
604 'content' => $m[0][0],
605 'options' => (isset($m['options'][0])) ? $this->parseOptionString($m['options'][0]) : []
606 ];
607
608 // Get this token's type by looking at the start of the match
609 $head = $m[1][0];
610 $pos = strpos($head, '=');
611
612 if ($pos === false)
613 {
614 // {FOO}
615 $token['id'] = $head;
616 }
617 else
618 {
619 // {FOO=...}
620 $token['id'] = substr($head, 0, $pos);
621
622 // Copy the content of named subpatterns into the token's config
623 foreach ($m as $k => $v)
624 {
625 if (!is_numeric($k) && $k !== 'options' && $v[1] !== -1)
626 {
627 $token[$k] = $v[0];
628 }
629 }
630 }
631
632 // The token's type is its id minus the number, e.g. NUMBER1 => NUMBER
633 $token['type'] = rtrim($token['id'], '0123456789');
634
635 // {PARSE} tokens can have several regexps separated with commas, we split them up here
636 if ($token['type'] === 'PARSE')
637 {
638 // Match all occurences of a would-be regexp followed by a comma or the end of the
639 // string
640 preg_match_all('#' . self::REGEXP . '(?:,|$)#', $token['regexps'], $m);
641
642 $regexps = [];
643 foreach ($m[0] as $regexp)
644 {
645 // remove the potential comma at the end
646 $regexps[] = rtrim($regexp, ',');
647 }
648
649 $token['regexps'] = $regexps;
650 }
651
652 $tokens[] = $token;
653 }
654
655 return $tokens;
656 }
657
658 /**
659 * Generate an attribute based on a token
660 *
661 * @param array $token Token this attribute is based on
662 * @return Attribute
663 */
664 protected function generateAttribute(array $token)
665 {
666 $attribute = new Attribute;
667
668 if (isset($token['options']['preFilter']))
669 {
670 $this->appendFilters($attribute, $token['options']['preFilter']);
671 unset($token['options']['preFilter']);
672 }
673
674 if ($token['type'] === 'REGEXP')
675 {
676 $filter = $this->configurator->attributeFilters->get('#regexp');
677 $attribute->filterChain->append($filter)->setRegexp($token['regexp']);
678 }
679 elseif ($token['type'] === 'RANGE')
680 {
681 $filter = $this->configurator->attributeFilters->get('#range');
682 $attribute->filterChain->append($filter)->setRange($token['min'], $token['max']);
683 }
684 elseif ($token['type'] === 'CHOICE')
685 {
686 $filter = $this->configurator->attributeFilters->get('#choice');
687 $attribute->filterChain->append($filter)->setValues(
688 explode(',', $token['choices']),
689 !empty($token['options']['caseSensitive'])
690 );
691 unset($token['options']['caseSensitive']);
692 }
693 elseif ($token['type'] === 'HASHMAP' || $token['type'] === 'MAP')
694 {
695 // Build the map from the string
696 $map = [];
697 foreach (explode(',', $token['map']) as $pair)
698 {
699 $pos = strpos($pair, ':');
700
701 if ($pos === false)
702 {
703 throw new RuntimeException("Invalid map assignment '" . $pair . "'");
704 }
705
706 $map[substr($pair, 0, $pos)] = substr($pair, 1 + $pos);
707 }
708
709 // Create the filter then append it to the attribute
710 if ($token['type'] === 'HASHMAP')
711 {
712 $filter = $this->configurator->attributeFilters->get('#hashmap');
713 $attribute->filterChain->append($filter)->setMap(
714 $map,
715 !empty($token['options']['strict'])
716 );
717 }
718 else
719 {
720 $filter = $this->configurator->attributeFilters->get('#map');
721 $attribute->filterChain->append($filter)->setMap(
722 $map,
723 !empty($token['options']['caseSensitive']),
724 !empty($token['options']['strict'])
725 );
726 }
727
728 // Remove options that are not needed anymore
729 unset($token['options']['caseSensitive']);
730 unset($token['options']['strict']);
731 }
732 elseif (!in_array($token['type'], $this->unfilteredTokens, true))
733 {
734 $filter = $this->configurator->attributeFilters->get('#' . $token['type']);
735 $attribute->filterChain->append($filter);
736 }
737
738 if (isset($token['options']['postFilter']))
739 {
740 $this->appendFilters($attribute, $token['options']['postFilter']);
741 unset($token['options']['postFilter']);
742 }
743
744 // Set the "required" option if "required" or "optional" is set, then remove
745 // the "optional" option
746 if (isset($token['options']['required']))
747 {
748 $token['options']['required'] = (bool) $token['options']['required'];
749 }
750 elseif (isset($token['options']['optional']))
751 {
752 $token['options']['required'] = !$token['options']['optional'];
753 }
754 unset($token['options']['optional']);
755
756 foreach ($token['options'] as $k => $v)
757 {
758 $attribute->$k = $v;
759 }
760
761 return $attribute;
762 }
763
764 /**
765 * Append a list of filters to an attribute's filterChain
766 *
767 * @param Attribute $attribute
768 * @param string $filters List of filters, separated with commas
769 * @return void
770 */
771 protected function appendFilters(Attribute $attribute, $filters)
772 {
773 foreach (preg_split('#\\s*,\\s*#', $filters) as $filterName)
774 {
775 if (substr($filterName, 0, 1) !== '#'
776 && !in_array($filterName, $this->allowedFilters, true))
777 {
778 throw new RuntimeException("Filter '" . $filterName . "' is not allowed in BBCodes");
779 }
780
781 $filter = $this->configurator->attributeFilters->get($filterName);
782 $attribute->filterChain->append($filter);
783 }
784 }
785
786 /**
787 * Test whether a token's name is the name of a filter
788 *
789 * @param string $tokenId Token ID, e.g. "TEXT1"
790 * @return bool
791 */
792 protected function isFilter($tokenId)
793 {
794 $filterName = rtrim($tokenId, '0123456789');
795
796 if (in_array($filterName, $this->unfilteredTokens, true))
797 {
798 return true;
799 }
800
801 // Try to load the filter
802 try
803 {
804 if ($this->configurator->attributeFilters->get('#' . $filterName))
805 {
806 return true;
807 }
808 }
809 catch (Exception $e)
810 {
811 // Nothing to do here
812 }
813
814 return false;
815 }
816
817 /**
818 * Parse the option string into an associative array
819 *
820 * @param string $string Serialized options
821 * @return array Associative array of options
822 */
823 protected function parseOptionString($string)
824 {
825 // Use the first "?" as an alias for the "optional" option
826 $string = preg_replace('(^\\?)', ';optional', $string);
827
828 $options = [];
829 foreach (preg_split('#;+#', $string, -1, PREG_SPLIT_NO_EMPTY) as $pair)
830 {
831 $pos = strpos($pair, '=');
832 if ($pos === false)
833 {
834 // Options with no value are set to true, e.g. {FOO;useContent}
835 $k = $pair;
836 $v = true;
837 }
838 else
839 {
840 $k = substr($pair, 0, $pos);
841 $v = substr($pair, 1 + $pos);
842 }
843
844 $options[$k] = $v;
845 }
846
847 return $options;
848 }
849 }