[ Index ] |
PHP Cross Reference of phpBB-3.3.14-deutsch |
[Summary view] [Print] [Text view]
1 <?php 2 3 /** 4 * @package s9e\TextFormatter 5 * @copyright Copyright (c) 2010-2022 The s9e authors 6 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 7 */ 8 namespace s9e\TextFormatter\Plugins\BBCodes\Configurator; 9 10 use Exception; 11 use InvalidArgumentException; 12 use RuntimeException; 13 use s9e\TextFormatter\Configurator; 14 use s9e\TextFormatter\Configurator\Helpers\RegexpBuilder; 15 use s9e\TextFormatter\Configurator\Items\Attribute; 16 use s9e\TextFormatter\Configurator\Items\ProgrammableCallback; 17 use s9e\TextFormatter\Configurator\Items\Tag; 18 use s9e\TextFormatter\Configurator\Items\Template; 19 20 class BBCodeMonkey 21 { 22 /** 23 * Expression that matches a regexp such as /foo/i 24 */ 25 const REGEXP = '(.).*?(?<!\\\\)(?>\\\\\\\\)*+\\g{-1}[DSUisu]*'; 26 27 /** 28 * @var array List of pre- and post- filters that are explicitly allowed in BBCode definitions. 29 * We use a whitelist approach because there are so many different risky callbacks 30 * that it would be too easy to let something dangerous slip by, e.g.: unlink, 31 * system, etc... 32 */ 33 public $allowedFilters = [ 34 'addslashes', 35 'dechex', 36 'intval', 37 'json_encode', 38 'ltrim', 39 'mb_strtolower', 40 'mb_strtoupper', 41 'rawurlencode', 42 'rtrim', 43 'str_rot13', 44 'stripslashes', 45 'strrev', 46 'strtolower', 47 'strtotime', 48 'strtoupper', 49 'trim', 50 'ucfirst', 51 'ucwords', 52 'urlencode' 53 ]; 54 55 /** 56 * @var Configurator Instance of Configurator; 57 */ 58 protected $configurator; 59 60 /** 61 * @var array Regexps used in the named subpatterns generated automatically for composite 62 * attributes. For instance, "foo={NUMBER},{NUMBER}" will be transformed into 63 * 'foo={PARSE=#^(?<foo0>\\d+),(?<foo1>\\d+)$#D}' 64 */ 65 public $tokenRegexp = [ 66 'ANYTHING' => '[\\s\\S]*?', 67 'COLOR' => '[a-zA-Z]+|#[0-9a-fA-F]+', 68 'EMAIL' => '[^@]+@.+?', 69 'FLOAT' => '(?>0|-?[1-9]\\d*)(?>\\.\\d+)?(?>e[1-9]\\d*)?', 70 'ID' => '[-a-zA-Z0-9_]+', 71 'IDENTIFIER' => '[-a-zA-Z0-9_]+', 72 'INT' => '0|-?[1-9]\\d*', 73 'INTEGER' => '0|-?[1-9]\\d*', 74 'NUMBER' => '\\d+', 75 'RANGE' => '\\d+', 76 'SIMPLETEXT' => '[-a-zA-Z0-9+.,_ ]+', 77 'TEXT' => '[\\s\\S]*?', 78 'UINT' => '0|[1-9]\\d*' 79 ]; 80 81 /** 82 * @var array List of token types that are used to represent raw, unfiltered content 83 */ 84 public $unfilteredTokens = [ 85 'ANYTHING', 86 'TEXT' 87 ]; 88 89 /** 90 * Constructor 91 * 92 * @param Configurator $configurator Instance of Configurator 93 */ 94 public function __construct(Configurator $configurator) 95 { 96 $this->configurator = $configurator; 97 } 98 99 /** 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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Mon Nov 25 19:05:08 2024 | Cross-referenced by PHPXref 0.7.1 |