parserCache = $parserCache; $this->globalIdGenerator = $globalIdGenerator; $this->revisionOutputCache = $revisionOutputCache; } /** * @param PageRecord $page * @param RevisionRecord|null $revision */ public function init( PageRecord $page, ?RevisionRecord $revision = null ) { $this->page = $page; $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 // TODO: make ParsoidPageConfigFactory take PageReference as well return MediaWikiServices::getInstance() ->get( 'ParsoidPageConfigFactory' ) ->create( TitleValue::newFromPage( $this->page ), null, $this->revision ? $this->revision->getId() : null ); } /** * @return ParserOutput a tuple with html and content-type * @throws LocalizedHttpException */ public function getHtml(): ParserOutput { $parserOptions = ParserOptions::newCanonical( 'canonical' ); $revId = $this->revision ? $this->revision->getId() : $this->page->getLatest(); $isOld = $revId !== $this->page->getLatest(); if ( $isOld ) { $parserOutput = $this->revisionOutputCache->get( $this->revision, $parserOptions ); } else { $parserOutput = $this->parserCache->get( $this->page, $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->revisionOutputCache->save( $fakeParserOutput, $this->revision, $parserOptions, $now ); } else { $this->parserCache->save( $fakeParserOutput, $this->page, $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(); } }