[ Index ]

PHP Cross Reference of phpBB-3.2.11-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-2019 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  use Exception;
  10  use InvalidArgumentException;
  11  use RuntimeException;
  12  use s9e\TextFormatter\Configurator;
  13  use s9e\TextFormatter\Configurator\Helpers\RegexpBuilder;
  14  use s9e\TextFormatter\Configurator\Items\Attribute;
  15  use s9e\TextFormatter\Configurator\Items\ProgrammableCallback;
  16  use s9e\TextFormatter\Configurator\Items\Tag;
  17  use s9e\TextFormatter\Configurator\Items\Template;
  18  class BBCodeMonkey
  19  {
  20      const REGEXP = '(.).*?(?<!\\\\)(?>\\\\\\\\)*+\\g{-1}[DSUisu]*';
  21      public $allowedFilters = [
  22          'addslashes',
  23          'dechex',
  24          'intval',
  25          'json_encode',
  26          'ltrim',
  27          'mb_strtolower',
  28          'mb_strtoupper',
  29          'rawurlencode',
  30          'rtrim',
  31          'str_rot13',
  32          'stripslashes',
  33          'strrev',
  34          'strtolower',
  35          'strtotime',
  36          'strtoupper',
  37          'trim',
  38          'ucfirst',
  39          'ucwords',
  40          'urlencode'
  41      ];
  42      protected $configurator;
  43      public $tokenRegexp = [
  44          'ANYTHING'   => '[\\s\\S]*?',
  45          'COLOR'      => '[a-zA-Z]+|#[0-9a-fA-F]+',
  46          'EMAIL'      => '[^@]+@.+?',
  47          'FLOAT'      => '(?>0|-?[1-9]\\d*)(?>\\.\\d+)?(?>e[1-9]\\d*)?',
  48          'ID'         => '[-a-zA-Z0-9_]+',
  49          'IDENTIFIER' => '[-a-zA-Z0-9_]+',
  50          'INT'        => '0|-?[1-9]\\d*',
  51          'INTEGER'    => '0|-?[1-9]\\d*',
  52          'NUMBER'     => '\\d+',
  53          'RANGE'      => '\\d+',
  54          'SIMPLETEXT' => '[-a-zA-Z0-9+.,_ ]+',
  55          'TEXT'       => '[\\s\\S]*?',
  56          'UINT'       => '0|[1-9]\\d*'
  57      ];
  58      public $unfilteredTokens = [
  59          'ANYTHING',
  60          'TEXT'
  61      ];
  62  	public function __construct(Configurator $configurator)
  63      {
  64          $this->configurator = $configurator;
  65      }
  66  	public function create($usage, $template)
  67      {
  68          $config = $this->parse($usage);
  69          if (!($template instanceof Template))
  70              $template = new Template($template);
  71          $template->replaceTokens(
  72              '#\\{(?:[A-Z]+[A-Z_0-9]*|@[-\\w]+)\\}#',
  73              function ($m) use ($config)
  74              {
  75                  $tokenId = \substr($m[0], 1, -1);
  76                  if ($tokenId[0] === '@')
  77                      return ['expression', $tokenId];
  78                  if (isset($config['tokens'][$tokenId]))
  79                      return ['expression', '@' . $config['tokens'][$tokenId]];
  80                  if ($tokenId === $config['passthroughToken'])
  81                      return ['passthrough'];
  82                  if ($this->isFilter($tokenId))
  83                      throw new RuntimeException('Token {' . $tokenId . '} is ambiguous or undefined');
  84                  return ['expression', '$' . $tokenId];
  85              }
  86          );
  87          $return = [
  88              'bbcode'     => $config['bbcode'],
  89              'bbcodeName' => $config['bbcodeName'],
  90              'tag'        => $config['tag']
  91          ];
  92          $return['tag']->template = $template;
  93          return $return;
  94      }
  95  	protected function parse($usage)
  96      {
  97          $tag    = new Tag;
  98          $bbcode = new BBCode;
  99          $config = [
 100              'tag'              => $tag,
 101              'bbcode'           => $bbcode,
 102              'passthroughToken' => \null
 103          ];
 104          $usage = \preg_replace_callback(
 105              '#(\\{(?>HASH)?MAP=)([^:]+:[^,;}]+(?>,[^:]+:[^,;}]+)*)(?=[;}])#',
 106              function ($m)
 107              {
 108                  return $m[1] . \base64_encode($m[2]);
 109              },
 110              $usage
 111          );
 112          $usage = \preg_replace_callback(
 113              '#(\\{(?:PARSE|REGEXP)=)(' . self::REGEXP . '(?:,' . self::REGEXP . ')*)#',
 114              function ($m)
 115              {
 116                  return $m[1] . \base64_encode($m[2]);
 117              },
 118              $usage
 119          );
 120          $regexp = '(^'
 121                  . '\\[(?<bbcodeName>\\S+?)'
 122                  . '(?<defaultAttribute>=.+?)?'
 123                  . '(?<attributes>(?:\\s+[^=]+=\\S+?)*?)?'
 124                  . '\\s*(?:/?\\]|\\]\\s*(?<content>.*?)\\s*(?<endTag>\\[/\\1]))$)i';
 125          if (!\preg_match($regexp, \trim($usage), $m))
 126              throw new InvalidArgumentException('Cannot interpret the BBCode definition');
 127          $config['bbcodeName'] = BBCode::normalizeName($m['bbcodeName']);
 128          $definitions = \preg_split('#\\s+#', \trim($m['attributes']), -1, \PREG_SPLIT_NO_EMPTY);
 129          if (!empty($m['defaultAttribute']))
 130              \array_unshift($definitions, $m['bbcodeName'] . $m['defaultAttribute']);
 131          if (!empty($m['content']))
 132          {
 133              $regexp = '#^\\{' . RegexpBuilder::fromList($this->unfilteredTokens) . '[0-9]*\\}$#D';
 134              if (\preg_match($regexp, $m['content']))
 135                  $config['passthroughToken'] = \substr($m['content'], 1, -1);
 136              else
 137              {
 138                  $definitions[] = 'content=' . $m['content'];
 139                  $bbcode->contentAttributes[] = 'content';
 140              }
 141          }
 142          $attributeDefinitions = [];
 143          foreach ($definitions as $definition)
 144          {
 145              $pos   = \strpos($definition, '=');
 146              $name  = \substr($definition, 0, $pos);
 147              $value = \preg_replace('(^"(.*?)")s', '$1', \substr($definition, 1 + $pos));
 148              $value = \preg_replace_callback(
 149                  '#(\\{(?>HASHMAP|MAP|PARSE|REGEXP)=)([A-Za-z0-9+/]+=*)#',
 150                  function ($m)
 151                  {
 152                      return $m[1] . \base64_decode($m[2]);
 153                  },
 154                  $value
 155              );
 156              if ($name[0] === '$')
 157              {
 158                  $optionName = \substr($name, 1);
 159                  $object = ($optionName === 'nestingLimit' || $optionName === 'tagLimit') ? $tag : $bbcode;
 160                  $object->$optionName = $this->convertValue($value);
 161              }
 162              elseif ($name[0] === '#')
 163              {
 164                  $ruleName = \substr($name, 1);
 165                  foreach (\explode(',', $value) as $value)
 166                      $tag->rules->$ruleName($this->convertValue($value));
 167              }
 168              else
 169              {
 170                  $attrName = \strtolower(\trim($name));
 171                  $attributeDefinitions[] = [$attrName, $value];
 172              }
 173          }
 174          $tokens = $this->addAttributes($attributeDefinitions, $bbcode, $tag);
 175          if (isset($tokens[$config['passthroughToken']]))
 176              $config['passthroughToken'] = \null;
 177          $config['tokens'] = \array_filter($tokens);
 178          return $config;
 179      }
 180  	protected function addAttributes(array $definitions, BBCode $bbcode, Tag $tag)
 181      {
 182          $composites = [];
 183          $table = [];
 184          foreach ($definitions as $_e874cdc7)
 185          {
 186              list($attrName, $definition) = $_e874cdc7;
 187              if (!isset($bbcode->defaultAttribute))
 188                  $bbcode->defaultAttribute = $attrName;
 189              $tokens = $this->parseTokens($definition);
 190              if (empty($tokens))
 191                  throw new RuntimeException('No valid tokens found in ' . $attrName . "'s definition " . $definition);
 192              if ($tokens[0]['content'] === $definition)
 193              {
 194                  $token = $tokens[0];
 195                  if ($token['type'] === 'PARSE')
 196                      foreach ($token['regexps'] as $regexp)
 197                          $tag->attributePreprocessors->add($attrName, $regexp);
 198                  elseif (isset($tag->attributes[$attrName]))
 199                      throw new RuntimeException("Attribute '" . $attrName . "' is declared twice");
 200                  else
 201                  {
 202                      if (!empty($token['options']['useContent']))
 203                          $bbcode->contentAttributes[] = $attrName;
 204                      unset($token['options']['useContent']);
 205                      $tag->attributes[$attrName] = $this->generateAttribute($token);
 206                      $tokenId = $token['id'];
 207                      $table[$tokenId] = (isset($table[$tokenId]))
 208                                       ? \false
 209                                       : $attrName;
 210                  }
 211              }
 212              else
 213                  $composites[] = [$attrName, $definition, $tokens];
 214          }
 215          foreach ($composites as $_2d84f0a0)
 216          {
 217              list($attrName, $definition, $tokens) = $_2d84f0a0;
 218              $regexp  = '/^';
 219              $lastPos = 0;
 220              $usedTokens = [];
 221              foreach ($tokens as $token)
 222              {
 223                  $tokenId   = $token['id'];
 224                  $tokenType = $token['type'];
 225                  if ($tokenType === 'PARSE')
 226                      throw new RuntimeException('{PARSE} tokens can only be used has the sole content of an attribute');
 227                  if (isset($usedTokens[$tokenId]))
 228                      throw new RuntimeException('Token {' . $tokenId . '} used multiple times in attribute ' . $attrName . "'s definition");
 229                  $usedTokens[$tokenId] = 1;
 230                  if (isset($table[$tokenId]))
 231                  {
 232                      $matchName = $table[$tokenId];
 233                      if ($matchName === \false)
 234                          throw new RuntimeException('Token {' . $tokenId . "} used in attribute '" . $attrName . "' is ambiguous");
 235                  }
 236                  else
 237                  {
 238                      $i = 0;
 239                      do
 240                      {
 241                          $matchName = $attrName . $i;
 242                          ++$i;
 243                      }
 244                      while (isset($tag->attributes[$matchName]));
 245                      $attribute = $tag->attributes->add($matchName);
 246                      if (!\in_array($tokenType, $this->unfilteredTokens, \true))
 247                      {
 248                          $filter = $this->configurator->attributeFilters->get('#' . \strtolower($tokenType));
 249                          $attribute->filterChain->append($filter);
 250                      }
 251                      $table[$tokenId] = $matchName;
 252                  }
 253                  $literal = \preg_quote(\substr($definition, $lastPos, $token['pos'] - $lastPos), '/');
 254                  $literal = \preg_replace('(\\s+)', '\\s+', $literal);
 255                  $regexp .= $literal;
 256                  $expr = (isset($this->tokenRegexp[$tokenType]))
 257                        ? $this->tokenRegexp[$tokenType]
 258                        : '.+?';
 259                  $regexp .= '(?<' . $matchName . '>' . $expr . ')';
 260                  $lastPos = $token['pos'] + \strlen($token['content']);
 261              }
 262              $regexp .= \preg_quote(\substr($definition, $lastPos), '/') . '$/D';
 263              $tag->attributePreprocessors->add($attrName, $regexp);
 264          }
 265          $newAttributes = [];
 266          foreach ($tag->attributePreprocessors as $attributePreprocessor)
 267              foreach ($attributePreprocessor->getAttributes() as $attrName => $regexp)
 268              {
 269                  if (isset($tag->attributes[$attrName]))
 270                      continue;
 271                  if (isset($newAttributes[$attrName])
 272                   && $newAttributes[$attrName] !== $regexp)
 273                      throw new RuntimeException("Ambiguous attribute '" . $attrName . "' created using different regexps needs to be explicitly defined");
 274                  $newAttributes[$attrName] = $regexp;
 275              }
 276          foreach ($newAttributes as $attrName => $regexp)
 277          {
 278              $filter = $this->configurator->attributeFilters->get('#regexp');
 279              $tag->attributes->add($attrName)->filterChain->append($filter)->setRegexp($regexp);
 280          }
 281          return $table;
 282      }
 283  	protected function convertValue($value)
 284      {
 285          if ($value === 'true')
 286              return \true;
 287          if ($value === 'false')
 288              return \false;
 289          return $value;
 290      }
 291  	protected function parseTokens($definition)
 292      {
 293          $tokenTypes = [
 294              'choice' => 'CHOICE[0-9]*=(?<choices>.+?)',
 295              'map'    => '(?:HASH)?MAP[0-9]*=(?<map>.+?)',
 296              'parse'  => 'PARSE=(?<regexps>' . self::REGEXP . '(?:,' . self::REGEXP . ')*)',
 297              'range'  => 'RANGE[0-9]*=(?<min>-?[0-9]+),(?<max>-?[0-9]+)',
 298              'regexp' => 'REGEXP[0-9]*=(?<regexp>' . self::REGEXP . ')',
 299              'other'  => '(?<other>[A-Z_]+[0-9]*)'
 300          ];
 301          \preg_match_all(
 302              '#\\{(' . \implode('|', $tokenTypes) . ')(?<options>\\??(?:;[^;]*)*)\\}#',
 303              $definition,
 304              $matches,
 305              \PREG_SET_ORDER | \PREG_OFFSET_CAPTURE
 306          );
 307          $tokens = [];
 308          foreach ($matches as $m)
 309          {
 310              if (isset($m['other'][0])
 311               && \preg_match('#^(?:CHOICE|HASHMAP|MAP|REGEXP|PARSE|RANGE)#', $m['other'][0]))
 312                  throw new RuntimeException("Malformed token '" . $m['other'][0] . "'");
 313              $token = [
 314                  'pos'     => $m[0][1],
 315                  'content' => $m[0][0],
 316                  'options' => (isset($m['options'][0])) ? $this->parseOptionString($m['options'][0]) : []
 317              ];
 318              $head = $m[1][0];
 319              $pos  = \strpos($head, '=');
 320              if ($pos === \false)
 321                  $token['id'] = $head;
 322              else
 323              {
 324                  $token['id'] = \substr($head, 0, $pos);
 325                  foreach ($m as $k => $v)
 326                      if (!\is_numeric($k) && $k !== 'options' && $v[1] !== -1)
 327                          $token[$k] = $v[0];
 328              }
 329              $token['type'] = \rtrim($token['id'], '0123456789');
 330              if ($token['type'] === 'PARSE')
 331              {
 332                  \preg_match_all('#' . self::REGEXP . '(?:,|$)#', $token['regexps'], $m);
 333                  $regexps = [];
 334                  foreach ($m[0] as $regexp)
 335                      $regexps[] = \rtrim($regexp, ',');
 336                  $token['regexps'] = $regexps;
 337              }
 338              $tokens[] = $token;
 339          }
 340          return $tokens;
 341      }
 342  	protected function generateAttribute(array $token)
 343      {
 344          $attribute = new Attribute;
 345          if (isset($token['options']['preFilter']))
 346          {
 347              $this->appendFilters($attribute, $token['options']['preFilter']);
 348              unset($token['options']['preFilter']);
 349          }
 350          if ($token['type'] === 'REGEXP')
 351          {
 352              $filter = $this->configurator->attributeFilters->get('#regexp');
 353              $attribute->filterChain->append($filter)->setRegexp($token['regexp']);
 354          }
 355          elseif ($token['type'] === 'RANGE')
 356          {
 357              $filter = $this->configurator->attributeFilters->get('#range');
 358              $attribute->filterChain->append($filter)->setRange($token['min'], $token['max']);
 359          }
 360          elseif ($token['type'] === 'CHOICE')
 361          {
 362              $filter = $this->configurator->attributeFilters->get('#choice');
 363              $attribute->filterChain->append($filter)->setValues(
 364                  \explode(',', $token['choices']),
 365                  !empty($token['options']['caseSensitive'])
 366              );
 367              unset($token['options']['caseSensitive']);
 368          }
 369          elseif ($token['type'] === 'HASHMAP' || $token['type'] === 'MAP')
 370          {
 371              $map = [];
 372              foreach (\explode(',', $token['map']) as $pair)
 373              {
 374                  $pos = \strpos($pair, ':');
 375                  if ($pos === \false)
 376                      throw new RuntimeException("Invalid map assignment '" . $pair . "'");
 377                  $map[\substr($pair, 0, $pos)] = \substr($pair, 1 + $pos);
 378              }
 379              if ($token['type'] === 'HASHMAP')
 380              {
 381                  $filter = $this->configurator->attributeFilters->get('#hashmap');
 382                  $attribute->filterChain->append($filter)->setMap(
 383                      $map,
 384                      !empty($token['options']['strict'])
 385                  );
 386              }
 387              else
 388              {
 389                  $filter = $this->configurator->attributeFilters->get('#map');
 390                  $attribute->filterChain->append($filter)->setMap(
 391                      $map,
 392                      !empty($token['options']['caseSensitive']),
 393                      !empty($token['options']['strict'])
 394                  );
 395              }
 396              unset($token['options']['caseSensitive']);
 397              unset($token['options']['strict']);
 398          }
 399          elseif (!\in_array($token['type'], $this->unfilteredTokens, \true))
 400          {
 401              $filter = $this->configurator->attributeFilters->get('#' . $token['type']);
 402              $attribute->filterChain->append($filter);
 403          }
 404          if (isset($token['options']['postFilter']))
 405          {
 406              $this->appendFilters($attribute, $token['options']['postFilter']);
 407              unset($token['options']['postFilter']);
 408          }
 409          if (isset($token['options']['required']))
 410              $token['options']['required'] = (bool) $token['options']['required'];
 411          elseif (isset($token['options']['optional']))
 412              $token['options']['required'] = !$token['options']['optional'];
 413          unset($token['options']['optional']);
 414          foreach ($token['options'] as $k => $v)
 415              $attribute->$k = $v;
 416          return $attribute;
 417      }
 418  	protected function appendFilters(Attribute $attribute, $filters)
 419      {
 420          foreach (\preg_split('#\\s*,\\s*#', $filters) as $filterName)
 421          {
 422              if (\substr($filterName, 0, 1) !== '#'
 423               && !\in_array($filterName, $this->allowedFilters, \true))
 424                  throw new RuntimeException("Filter '" . $filterName . "' is not allowed in BBCodes");
 425              $filter = $this->configurator->attributeFilters->get($filterName);
 426              $attribute->filterChain->append($filter);
 427          }
 428      }
 429  	protected function isFilter($tokenId)
 430      {
 431          $filterName = \rtrim($tokenId, '0123456789');
 432          if (\in_array($filterName, $this->unfilteredTokens, \true))
 433              return \true;
 434          try
 435          {
 436              if ($this->configurator->attributeFilters->get('#' . $filterName))
 437                  return \true;
 438          }
 439          catch (Exception $e)
 440          {
 441              }
 442          return \false;
 443      }
 444  	protected function parseOptionString($string)
 445      {
 446          $string = \preg_replace('(^\\?)', ';optional', $string);
 447          $options = [];
 448          foreach (\preg_split('#;+#', $string, -1, \PREG_SPLIT_NO_EMPTY) as $pair)
 449          {
 450              $pos = \strpos($pair, '=');
 451              if ($pos === \false)
 452              {
 453                  $k = $pair;
 454                  $v = \true;
 455              }
 456              else
 457              {
 458                  $k = \substr($pair, 0, $pos);
 459                  $v = \substr($pair, 1 + $pos);
 460              }
 461              $options[$k] = $v;
 462          }
 463          return $options;
 464      }
 465  }


Generated: Wed Nov 11 20:33:01 2020 Cross-referenced by PHPXref 0.7.1