Message parse depth limit exceeded'; private ParserFactory $parserFactory; private OutputTransformPipeline $outputPipeline; private LanguageFactory $langFactory; private LoggerInterface $logger; /** @var ParserOptions|null Lazy-initialised */ private ?ParserOptions $parserOptions = null; /** @var Parser[] Cached Parser objects */ private array $parsers = []; /** @var int Index into $this->parsers for the active Parser */ private int $curParser = -1; /** * Parsing some messages may require parsing another message first, due to special page * transclusion and some hooks (T372891). This constant is the limit of nesting depth where * we'll display an error instead of the other message. */ private const MAX_PARSER_DEPTH = 5; public function __construct( ParserFactory $parserFactory, OutputTransformPipeline $outputPipeline, LanguageFactory $languageFactory, LoggerInterface $logger ) { $this->parserFactory = $parserFactory; $this->outputPipeline = $outputPipeline; $this->langFactory = $languageFactory; $this->logger = $logger; } private function getParserOptions(): ParserOptions { if ( !$this->parserOptions ) { $context = RequestContext::getMain(); $user = $context->getUser(); if ( !$user->isSafeToLoad() ) { // It isn't safe to use the context user yet, so don't try to get a // ParserOptions for it. And don't cache this ParserOptions // either. $po = ParserOptions::newFromAnon(); $po->setAllowUnsafeRawHtml( false ); return $po; } $this->parserOptions = ParserOptions::newFromContext( $context ); // Messages may take parameters that could come // from malicious sources. As a precaution, disable // the parser tag when parsing messages. $this->parserOptions->setAllowUnsafeRawHtml( false ); } return $this->parserOptions; } /** * Run message text through the preprocessor, expanding parser functions * * @param string $message * @param bool $interface * @param Language|string|null $language * @param PageReference|null $page * @return string */ public function transform( $message, $interface = false, $language = null, ?PageReference $page = null ) { // Avoid creating parser if nothing to transform if ( !str_contains( $message, '{{' ) ) { return $message; } if ( is_string( $language ) ) { $language = $this->langFactory->getLanguage( $language ); } $popts = $this->getParserOptions(); $popts->setInterfaceMessage( $interface ); $popts->setTargetLanguage( $language ); if ( $language ) { $oldUserLang = $popts->setUserLang( $language ); } else { $oldUserLang = null; } $page ??= $this->getPlaceholderTitle(); $parser = $this->acquireParser(); if ( !$parser ) { return self::DEPTH_EXCEEDED_MESSAGE; } try { return $parser->transformMsg( $message, $popts, $page ); } finally { $this->releaseParser( $parser ); if ( $oldUserLang ) { $popts->setUserLang( $oldUserLang ); } } } /** * @param string $text * @param ?PageReference $contextPage The context page, or null to use a placeholder * @param bool $lineStart Whether this should be parsed in start-of-line context * @param bool $interface Whether this is an interface message * @param Language|StubUserLang|string|null $language Language code * @return ParserOutput */ public function parse( string $text, ?PageReference $contextPage = null, bool $lineStart = true, bool $interface = false, $language = null ): ParserOutput { $options = [ 'allowTOC' => false, 'enableSectionEditLinks' => false, // Wrapping messages in an extra