[ Index ]

PHP Cross Reference of phpBB-3.3.5-deutsch

title

Body

[close]

/vendor/s9e/text-formatter/src/Plugins/BBCodes/Configurator/ -> BBCodeMonkey.php (source)

   1  <?php
   2  
   3  /**
   4  * @package   s9e\TextFormatter
   5  * @copyright Copyright (c) 2010-2021 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  }


Generated: Mon Oct 4 17:42:11 2021 Cross-referenced by PHPXref 0.7.1