parserCache = $parserCache; $this->wikiPageFactory = $wikiPageFactory; $this->globalIdGenerator = $globalIdGenerator; } /** * @param Title $title * @param RevisionRecord|null $revision */ public function init( Title $title, ?RevisionRecord $revision = null ) { $this->title = $title; $this->revision = $revision; } /** * @return ParserOutput * @throws LocalizedHttpException */ private function parse(): ParserOutput { $parsoid = $this->createParsoid(); $pageConfig = $this->createPageConfig(); try { $pageBundle = $parsoid->wikitext2html( $pageConfig, [ 'discardDataParsoid' => true, 'pageBundle' => true, ] ); $fakeParserOutput = new ParserOutput( $pageBundle->html ); return $fakeParserOutput; } catch ( ClientError $e ) { throw new LocalizedHttpException( MessageValue::new( 'rest-html-backend-error' ), 400, [ 'reason' => $e->getMessage() ] ); } catch ( ResourceLimitExceededException $e ) { throw new LocalizedHttpException( MessageValue::new( 'rest-resource-limit-exceeded' ), 413, [ 'reason' => $e->getMessage() ] ); } } /** * Assert that Parsoid services are available. * TODO: once parsoid glue services are in core, * this will become a no-op and will be removed. * See T265518 * @throws LocalizedHttpException */ private function assertParsoidInstalled() { $services = MediaWikiServices::getInstance(); if ( $services->has( 'ParsoidSiteConfig' ) && $services->has( 'ParsoidPageConfigFactory' ) && $services->has( 'ParsoidDataAccess' ) ) { return; } throw new LocalizedHttpException( MessageValue::new( 'rest-html-backend-error' ), 501 ); } /** * @return Parsoid * @throws LocalizedHttpException */ private function createParsoid(): Parsoid { $this->assertParsoidInstalled(); if ( $this->parsoid === null ) { // TODO: Once parsoid glue services are in core, // this will need to use normal DI. // At that point, we may want to extract a more high level // service for rendering a revision, and inject that into this class. // See T265518 $services = MediaWikiServices::getInstance(); $this->parsoid = new Parsoid( $services->get( 'ParsoidSiteConfig' ), $services->get( 'ParsoidDataAccess' ) ); } return $this->parsoid; } /** * @return PageConfig * @throws LocalizedHttpException */ private function createPageConfig(): PageConfig { $this->assertParsoidInstalled(); // Currently everything is parsed as anon since Parsoid // can't report the used options. // Already checked that title/revision exist and accessible. // TODO: make ParsoidPageConfigFactory take a RevisionRecord return MediaWikiServices::getInstance() ->get( 'ParsoidPageConfigFactory' ) ->create( $this->title, null, $this->revision ? $this->revision->getId() : null ); } /** * @return ParserOutput a tuple with html and content-type * @throws LocalizedHttpException */ public function getHtml(): ParserOutput { $wikiPage = $this->wikiPageFactory->newFromLinkTarget( $this->title ); $parserOptions = ParserOptions::newCanonical( 'canonical' ); $revId = $this->revision ? $this->revision->getId() : $wikiPage->getLatest(); $isOld = $revId !== $wikiPage->getLatest(); // TODO: caching for old revisions, see T269663 if ( !$isOld ) { $parserOutput = $this->parserCache->get( $wikiPage, $parserOptions ); if ( $parserOutput ) { return $parserOutput; } } $fakeParserOutput = $this->parse(); // XXX: ParserOutput should just always record the revision ID and timestamp $now = wfTimestampNow(); $fakeParserOutput->setCacheRevisionId( $revId ); $fakeParserOutput->setCacheTime( $now ); // TODO: when we make tighter integration with Parsoid, render ID should become // a standard ParserOutput property. Nothing else needs it now, so don't generate // it in ParserCache just yet. $fakeParserOutput->setExtensionData( self::RENDER_ID_KEY, $this->globalIdGenerator->newUUIDv1() ); if ( !$isOld ) { $this->parserCache->save( $fakeParserOutput, $wikiPage, $parserOptions, $now ); } return $fakeParserOutput; } /** * Returns an ETag uniquely identifying the HTML output. * @return string|null */ public function getETag(): ?string { $parserOutput = $this->getHtml(); $renderId = $parserOutput->getExtensionData( self::RENDER_ID_KEY ); // Fallback for backwards compatibility with older cached entries. if ( !$renderId ) { $renderId = $this->getLastModified(); } return "\"{$parserOutput->getCacheRevisionId()}/{$renderId}\""; } /** * Returns the time at which the HTML was rendered. * * @return string|null */ public function getLastModified(): ?string { return $this->getHtml()->getCacheTime(); } }