\\\\\\\\)*+\\g{-1}[DSUisu]*'; public $allowedFilters = [ 'addslashes', 'dechex', 'intval', 'json_encode', 'ltrim', 'mb_strtolower', 'mb_strtoupper', 'rawurlencode', 'rtrim', 'str_rot13', 'stripslashes', 'strrev', 'strtolower', 'strtotime', 'strtoupper', 'trim', 'ucfirst', 'ucwords', 'urlencode' ]; protected $configurator; public $tokenRegexp = [ 'ANYTHING' => '[\\s\\S]*?', 'COLOR' => '[a-zA-Z]+|#[0-9a-fA-F]+', 'EMAIL' => '[^@]+@.+?', 'FLOAT' => '(?>0|-?[1-9]\\d*)(?>\\.\\d+)?(?>e[1-9]\\d*)?', 'ID' => '[-a-zA-Z0-9_]+', 'IDENTIFIER' => '[-a-zA-Z0-9_]+', 'INT' => '0|-?[1-9]\\d*', 'INTEGER' => '0|-?[1-9]\\d*', 'NUMBER' => '\\d+', 'RANGE' => '\\d+', 'SIMPLETEXT' => '[-a-zA-Z0-9+.,_ ]+', 'TEXT' => '[\\s\\S]*?', 'UINT' => '0|[1-9]\\d*' ]; public $unfilteredTokens = [ 'ANYTHING', 'TEXT' ]; public function __construct(Configurator $configurator) { $this->configurator = $configurator; } public function create($usage, $template) { $config = $this->parse($usage); if (!($template instanceof Template)) $template = new Template($template); $template->replaceTokens( '#\\{(?:[A-Z]+[A-Z_0-9]*|@[-\\w]+)\\}#', function ($m) use ($config) { $tokenId = \substr($m[0], 1, -1); if ($tokenId[0] === '@') return ['expression', $tokenId]; if (isset($config['tokens'][$tokenId])) return ['expression', '@' . $config['tokens'][$tokenId]]; if ($tokenId === $config['passthroughToken']) return ['passthrough']; if ($this->isFilter($tokenId)) throw new RuntimeException('Token {' . $tokenId . '} is ambiguous or undefined'); return ['expression', '$' . $tokenId]; } ); $return = [ 'bbcode' => $config['bbcode'], 'bbcodeName' => $config['bbcodeName'], 'tag' => $config['tag'] ]; $return['tag']->template = $template; return $return; } protected function parse($usage) { $tag = new Tag; $bbcode = new BBCode; $config = [ 'tag' => $tag, 'bbcode' => $bbcode, 'passthroughToken' => \null ]; $usage = \preg_replace_callback( '#(\\{(?>HASH)?MAP=)([^:]+:[^,;}]+(?>,[^:]+:[^,;}]+)*)(?=[;}])#', function ($m) { return $m[1] . \base64_encode($m[2]); }, $usage ); $usage = \preg_replace_callback( '#(\\{(?:PARSE|REGEXP)=)(' . self::REGEXP . '(?:,' . self::REGEXP . ')*)#', function ($m) { return $m[1] . \base64_encode($m[2]); }, $usage ); $regexp = '(^' . '\\[(?\\S+?)' . '(?=.+?)?' . '(?(?:\\s+[^=]+=\\S+?)*?)?' . '\\s*(?:/?\\]|\\]\\s*(?.*?)\\s*(?\\[/\\1]))$)i'; if (!\preg_match($regexp, \trim($usage), $m)) throw new InvalidArgumentException('Cannot interpret the BBCode definition'); $config['bbcodeName'] = BBCode::normalizeName($m['bbcodeName']); $definitions = \preg_split('#\\s+#', \trim($m['attributes']), -1, \PREG_SPLIT_NO_EMPTY); if (!empty($m['defaultAttribute'])) \array_unshift($definitions, $m['bbcodeName'] . $m['defaultAttribute']); if (!empty($m['content'])) { $regexp = '#^\\{' . RegexpBuilder::fromList($this->unfilteredTokens) . '[0-9]*\\}$#D'; if (\preg_match($regexp, $m['content'])) $config['passthroughToken'] = \substr($m['content'], 1, -1); else { $definitions[] = 'content=' . $m['content']; $bbcode->contentAttributes[] = 'content'; } } $attributeDefinitions = []; foreach ($definitions as $definition) { $pos = \strpos($definition, '='); $name = \substr($definition, 0, $pos); $value = \preg_replace('(^"(.*?)")s', '$1', \substr($definition, 1 + $pos)); $value = \preg_replace_callback( '#(\\{(?>HASHMAP|MAP|PARSE|REGEXP)=)([A-Za-z0-9+/]+=*)#', function ($m) { return $m[1] . \base64_decode($m[2]); }, $value ); if ($name[0] === '$') { $optionName = \substr($name, 1); $object = ($optionName === 'nestingLimit' || $optionName === 'tagLimit') ? $tag : $bbcode; $object->$optionName = $this->convertValue($value); } elseif ($name[0] === '#') { $ruleName = \substr($name, 1); foreach (\explode(',', $value) as $value) $tag->rules->$ruleName($this->convertValue($value)); } else { $attrName = \strtolower(\trim($name)); $attributeDefinitions[] = [$attrName, $value]; } } $tokens = $this->addAttributes($attributeDefinitions, $bbcode, $tag); if (isset($tokens[$config['passthroughToken']])) $config['passthroughToken'] = \null; $config['tokens'] = \array_filter($tokens); return $config; } protected function addAttributes(array $definitions, BBCode $bbcode, Tag $tag) { $composites = []; $table = []; foreach ($definitions as $_e874cdc7) { list($attrName, $definition) = $_e874cdc7; if (!isset($bbcode->defaultAttribute)) $bbcode->defaultAttribute = $attrName; $tokens = $this->parseTokens($definition); if (empty($tokens)) throw new RuntimeException('No valid tokens found in ' . $attrName . "'s definition " . $definition); if ($tokens[0]['content'] === $definition) { $token = $tokens[0]; if ($token['type'] === 'PARSE') foreach ($token['regexps'] as $regexp) $tag->attributePreprocessors->add($attrName, $regexp); elseif (isset($tag->attributes[$attrName])) throw new RuntimeException("Attribute '" . $attrName . "' is declared twice"); else { if (!empty($token['options']['useContent'])) $bbcode->contentAttributes[] = $attrName; unset($token['options']['useContent']); $tag->attributes[$attrName] = $this->generateAttribute($token); $tokenId = $token['id']; $table[$tokenId] = (isset($table[$tokenId])) ? \false : $attrName; } } else $composites[] = [$attrName, $definition, $tokens]; } foreach ($composites as $_2d84f0a0) { list($attrName, $definition, $tokens) = $_2d84f0a0; $regexp = '/^'; $lastPos = 0; $usedTokens = []; foreach ($tokens as $token) { $tokenId = $token['id']; $tokenType = $token['type']; if ($tokenType === 'PARSE') throw new RuntimeException('{PARSE} tokens can only be used has the sole content of an attribute'); if (isset($usedTokens[$tokenId])) throw new RuntimeException('Token {' . $tokenId . '} used multiple times in attribute ' . $attrName . "'s definition"); $usedTokens[$tokenId] = 1; if (isset($table[$tokenId])) { $matchName = $table[$tokenId]; if ($matchName === \false) throw new RuntimeException('Token {' . $tokenId . "} used in attribute '" . $attrName . "' is ambiguous"); } else { $i = 0; do { $matchName = $attrName . $i; ++$i; } while (isset($tag->attributes[$matchName])); $attribute = $tag->attributes->add($matchName); if (!\in_array($tokenType, $this->unfilteredTokens, \true)) { $filter = $this->configurator->attributeFilters->get('#' . \strtolower($tokenType)); $attribute->filterChain->append($filter); } $table[$tokenId] = $matchName; } $literal = \preg_quote(\substr($definition, $lastPos, $token['pos'] - $lastPos), '/'); $literal = \preg_replace('(\\s+)', '\\s+', $literal); $regexp .= $literal; $expr = (isset($this->tokenRegexp[$tokenType])) ? $this->tokenRegexp[$tokenType] : '.+?'; $regexp .= '(?<' . $matchName . '>' . $expr . ')'; $lastPos = $token['pos'] + \strlen($token['content']); } $regexp .= \preg_quote(\substr($definition, $lastPos), '/') . '$/D'; $tag->attributePreprocessors->add($attrName, $regexp); } $newAttributes = []; foreach ($tag->attributePreprocessors as $attributePreprocessor) foreach ($attributePreprocessor->getAttributes() as $attrName => $regexp) { if (isset($tag->attributes[$attrName])) continue; if (isset($newAttributes[$attrName]) && $newAttributes[$attrName] !== $regexp) throw new RuntimeException("Ambiguous attribute '" . $attrName . "' created using different regexps needs to be explicitly defined"); $newAttributes[$attrName] = $regexp; } foreach ($newAttributes as $attrName => $regexp) { $filter = $this->configurator->attributeFilters->get('#regexp'); $tag->attributes->add($attrName)->filterChain->append($filter)->setRegexp($regexp); } return $table; } protected function convertValue($value) { if ($value === 'true') return \true; if ($value === 'false') return \false; return $value; } protected function parseTokens($definition) { $tokenTypes = [ 'choice' => 'CHOICE[0-9]*=(?.+?)', 'map' => '(?:HASH)?MAP[0-9]*=(?.+?)', 'parse' => 'PARSE=(?' . self::REGEXP . '(?:,' . self::REGEXP . ')*)', 'range' => 'RANGE[0-9]*=(?-?[0-9]+),(?-?[0-9]+)', 'regexp' => 'REGEXP[0-9]*=(?' . self::REGEXP . ')', 'other' => '(?[A-Z_]+[0-9]*)' ]; \preg_match_all( '#\\{(' . \implode('|', $tokenTypes) . ')(?\\??(?:;[^;]*)*)\\}#', $definition, $matches, \PREG_SET_ORDER | \PREG_OFFSET_CAPTURE ); $tokens = []; foreach ($matches as $m) { if (isset($m['other'][0]) && \preg_match('#^(?:CHOICE|HASHMAP|MAP|REGEXP|PARSE|RANGE)#', $m['other'][0])) throw new RuntimeException("Malformed token '" . $m['other'][0] . "'"); $token = [ 'pos' => $m[0][1], 'content' => $m[0][0], 'options' => (isset($m['options'][0])) ? $this->parseOptionString($m['options'][0]) : [] ]; $head = $m[1][0]; $pos = \strpos($head, '='); if ($pos === \false) $token['id'] = $head; else { $token['id'] = \substr($head, 0, $pos); foreach ($m as $k => $v) if (!\is_numeric($k) && $k !== 'options' && $v[1] !== -1) $token[$k] = $v[0]; } $token['type'] = \rtrim($token['id'], '0123456789'); if ($token['type'] === 'PARSE') { \preg_match_all('#' . self::REGEXP . '(?:,|$)#', $token['regexps'], $m); $regexps = []; foreach ($m[0] as $regexp) $regexps[] = \rtrim($regexp, ','); $token['regexps'] = $regexps; } $tokens[] = $token; } return $tokens; } protected function generateAttribute(array $token) { $attribute = new Attribute; if (isset($token['options']['preFilter'])) { $this->appendFilters($attribute, $token['options']['preFilter']); unset($token['options']['preFilter']); } if ($token['type'] === 'REGEXP') { $filter = $this->configurator->attributeFilters->get('#regexp'); $attribute->filterChain->append($filter)->setRegexp($token['regexp']); } elseif ($token['type'] === 'RANGE') { $filter = $this->configurator->attributeFilters->get('#range'); $attribute->filterChain->append($filter)->setRange($token['min'], $token['max']); } elseif ($token['type'] === 'CHOICE') { $filter = $this->configurator->attributeFilters->get('#choice'); $attribute->filterChain->append($filter)->setValues( \explode(',', $token['choices']), !empty($token['options']['caseSensitive']) ); unset($token['options']['caseSensitive']); } elseif ($token['type'] === 'HASHMAP' || $token['type'] === 'MAP') { $map = []; foreach (\explode(',', $token['map']) as $pair) { $pos = \strpos($pair, ':'); if ($pos === \false) throw new RuntimeException("Invalid map assignment '" . $pair . "'"); $map[\substr($pair, 0, $pos)] = \substr($pair, 1 + $pos); } if ($token['type'] === 'HASHMAP') { $filter = $this->configurator->attributeFilters->get('#hashmap'); $attribute->filterChain->append($filter)->setMap( $map, !empty($token['options']['strict']) ); } else { $filter = $this->configurator->attributeFilters->get('#map'); $attribute->filterChain->append($filter)->setMap( $map, !empty($token['options']['caseSensitive']), !empty($token['options']['strict']) ); } unset($token['options']['caseSensitive']); unset($token['options']['strict']); } elseif (!\in_array($token['type'], $this->unfilteredTokens, \true)) { $filter = $this->configurator->attributeFilters->get('#' . $token['type']); $attribute->filterChain->append($filter); } if (isset($token['options']['postFilter'])) { $this->appendFilters($attribute, $token['options']['postFilter']); unset($token['options']['postFilter']); } if (isset($token['options']['required'])) $token['options']['required'] = (bool) $token['options']['required']; elseif (isset($token['options']['optional'])) $token['options']['required'] = !$token['options']['optional']; unset($token['options']['optional']); foreach ($token['options'] as $k => $v) $attribute->$k = $v; return $attribute; } protected function appendFilters(Attribute $attribute, $filters) { foreach (\preg_split('#\\s*,\\s*#', $filters) as $filterName) { if (\substr($filterName, 0, 1) !== '#' && !\in_array($filterName, $this->allowedFilters, \true)) throw new RuntimeException("Filter '" . $filterName . "' is not allowed in BBCodes"); $filter = $this->configurator->attributeFilters->get($filterName); $attribute->filterChain->append($filter); } } protected function isFilter($tokenId) { $filterName = \rtrim($tokenId, '0123456789'); if (\in_array($filterName, $this->unfilteredTokens, \true)) return \true; try { if ($this->configurator->attributeFilters->get('#' . $filterName)) return \true; } catch (Exception $e) { } return \false; } protected function parseOptionString($string) { $string = \preg_replace('(^\\?)', ';optional', $string); $options = []; foreach (\preg_split('#;+#', $string, -1, \PREG_SPLIT_NO_EMPTY) as $pair) { $pos = \strpos($pair, '='); if ($pos === \false) { $k = $pair; $v = \true; } else { $k = \substr($pair, 0, $pos); $v = \substr($pair, 1 + $pos); } $options[$k] = $v; } return $options; } }