loadBalancer = $loadBalancer; $this->roleRegistry = $roleRegistry; $this->contentRenderer = $contentRenderer; $this->dbDomain = $dbDomain; $this->saveParseLogger = new NullLogger(); } /** * @param LoggerInterface $saveParseLogger */ public function setLogger( LoggerInterface $saveParseLogger ) { $this->saveParseLogger = $saveParseLogger; } // phpcs:disable Generic.Files.LineLength.TooLong /** * @param RevisionRecord $rev * @param ParserOptions|null $options * @param Authority|null $forPerformer User for privileged access. Default is unprivileged * (public) access, unless the 'audience' hint is set to something else RevisionRecord::RAW. * @param array{use-master?:bool,audience?:int,known-revision-output?:ParserOutput,causeAction?:?string,previous-output?:?ParserOutput} $hints * Hints given as an associative array. Known keys: * - 'use-master' Use primary DB when rendering for the parser cache during save. * Default is to use a replica. * - 'audience' the audience to use for content access. Default is * RevisionRecord::FOR_PUBLIC if $forUser is not set, RevisionRecord::FOR_THIS_USER * if $forUser is set. Can be set to RevisionRecord::RAW to disable audience checks. * - 'known-revision-output' a combined ParserOutput for the revision, perhaps from * some cache. the caller is responsible for ensuring that the ParserOutput indeed * matched the $rev and $options. This mechanism is intended as a temporary stop-gap, * for the time until caches have been changed to store RenderedRevision states instead * of ParserOutput objects. * - 'previous-output' A previously-rendered ParserOutput for this page. This * can be used by Parsoid for selective updates. * - 'causeAction' the reason for rendering. This should be informative, for used for * logging and debugging. * * @return RenderedRevision|null The rendered revision, or null if the audience checks fails. * @throws BadRevisionException * @throws RevisionAccessException */ // phpcs:enable Generic.Files.LineLength.TooLong public function getRenderedRevision( RevisionRecord $rev, ?ParserOptions $options = null, ?Authority $forPerformer = null, array $hints = [] ) { if ( $rev->getWikiId() !== $this->dbDomain ) { throw new InvalidArgumentException( 'Mismatching wiki ID ' . $rev->getWikiId() ); } $audience = $hints['audience'] ?? ( $forPerformer ? RevisionRecord::FOR_THIS_USER : RevisionRecord::FOR_PUBLIC ); if ( !$rev->audienceCan( RevisionRecord::DELETED_TEXT, $audience, $forPerformer ) ) { // Returning null here is awkward, but consistent with the signature of // RevisionRecord::getContent(). return null; } if ( !$options ) { $options = $forPerformer ? ParserOptions::newFromUser( $forPerformer->getUser() ) : ParserOptions::newFromAnon(); } if ( isset( $hints['causeAction'] ) ) { $options->setRenderReason( $hints['causeAction'] ); } $usePrimary = $hints['use-master'] ?? false; $dbIndex = $usePrimary ? DB_PRIMARY // use latest values : DB_REPLICA; // T154554 $options->setSpeculativeRevIdCallback( function () use ( $dbIndex ) { return $this->getSpeculativeRevId( $dbIndex ); } ); $options->setSpeculativePageIdCallback( function () use ( $dbIndex ) { return $this->getSpeculativePageId( $dbIndex ); } ); if ( !$rev->getId() && $rev->getTimestamp() ) { // This is an unsaved revision with an already determined timestamp. // Make the "current" time used during parsing match that of the revision. // Any REVISION* parser variables will match up if the revision is saved. $options->setTimestamp( $rev->getTimestamp() ); } $previousOutput = $hints['previous-output'] ?? null; $renderedRevision = new RenderedRevision( $rev, $options, $this->contentRenderer, function ( RenderedRevision $rrev, array $hints ) use ( $options, $previousOutput ) { $h = [ 'previous-output' => $previousOutput ] + $hints; return $this->combineSlotOutput( $rrev, $options, $h ); }, $audience, $forPerformer ); $renderedRevision->setSaveParseLogger( $this->saveParseLogger ); if ( isset( $hints['known-revision-output'] ) ) { $renderedRevision->setRevisionParserOutput( $hints['known-revision-output'] ); } return $renderedRevision; } private function getSpeculativeRevId( $dbIndex ) { // Use a separate primary DB connection in order to see the latest data, by avoiding // stale data from REPEATABLE-READ snapshots. $flags = ILoadBalancer::CONN_TRX_AUTOCOMMIT; $db = $this->loadBalancer->getConnection( $dbIndex, [], $this->dbDomain, $flags ); return 1 + (int)$db->newSelectQueryBuilder() ->select( 'MAX(rev_id)' ) ->from( 'revision' ) ->caller( __METHOD__ )->fetchField(); } private function getSpeculativePageId( $dbIndex ) { // Use a separate primary DB connection in order to see the latest data, by avoiding // stale data from REPEATABLE-READ snapshots. $flags = ILoadBalancer::CONN_TRX_AUTOCOMMIT; $db = $this->loadBalancer->getConnection( $dbIndex, [], $this->dbDomain, $flags ); return 1 + (int)$db->newSelectQueryBuilder() ->select( 'MAX(page_id)' ) ->from( 'page' ) ->caller( __METHOD__ )->fetchField(); } /** * This implements the layout for combining the output of multiple slots. * * @todo Use placement hints from SlotRoleHandlers instead of hard-coding the layout. * * @param RenderedRevision $rrev * @param ParserOptions $options * @param array $hints see RenderedRevision::getRevisionParserOutput() * * @return ParserOutput * @throws SuppressedDataException * @throws BadRevisionException * @throws RevisionAccessException */ private function combineSlotOutput( RenderedRevision $rrev, ParserOptions $options, array $hints = [] ) { $revision = $rrev->getRevision(); $slots = $revision->getSlots()->getSlots(); $withHtml = $hints['generate-html'] ?? true; $previousOutputs = $this->splitSlotOutput( $rrev, $options, $hints['previous-output'] ?? null ); // short circuit if there is only the main slot // T351026 hack: if use-parsoid is set, only return main slot output for now // T351113 will remove this hack. if ( array_keys( $slots ) === [ SlotRecord::MAIN ] || $options->getUseParsoid() ) { $h = [ 'previous-output' => $previousOutputs[SlotRecord::MAIN] ] + $hints; return $rrev->getSlotParserOutput( SlotRecord::MAIN, $h ); } // move main slot to front if ( isset( $slots[SlotRecord::MAIN] ) ) { $slots = [ SlotRecord::MAIN => $slots[SlotRecord::MAIN] ] + $slots; } $combinedOutput = new ParserOutput( null ); $slotOutput = []; $options = $rrev->getOptions(); $options->registerWatcher( [ $combinedOutput, 'recordOption' ] ); foreach ( $slots as $role => $slot ) { $h = [ 'previous-output' => $previousOutputs[$role] ] + $hints; $out = $rrev->getSlotParserOutput( $role, $h ); $slotOutput[$role] = $out; // XXX: should the SlotRoleHandler be able to intervene here? $combinedOutput->mergeInternalMetaDataFrom( $out ); $combinedOutput->mergeTrackingMetaDataFrom( $out ); } if ( $withHtml ) { $html = ''; $first = true; /** @var ParserOutput $out */ foreach ( $slotOutput as $role => $out ) { $roleHandler = $this->roleRegistry->getRoleHandler( $role ); // TODO: put more fancy layout logic here, see T200915. $layout = $roleHandler->getOutputLayoutHints(); $display = $layout['display'] ?? 'section'; if ( $display === 'none' ) { continue; } if ( $first ) { // skip header for the first slot $first = false; } else { // NOTE: this placeholder is hydrated by ParserOutput::getText(). $headText = Html::element( 'mw:slotheader', [], $role ); $html .= Html::rawElement( 'h1', [ 'class' => 'mw-slot-header' ], $headText ); } // XXX: do we want to put a wrapper div around the output? // Do we want to let $roleHandler do that? $html .= $out->getRawText(); $combinedOutput->mergeHtmlMetaDataFrom( $out ); } $combinedOutput->setRawText( $html ); } $options->registerWatcher( null ); return $combinedOutput; } /** * This reverses ::combineSlotOutput() in order to enable selective * update of individual slots. * * @todo Currently this doesn't do much other than disable selective * update if there is more than one slot. But in the case where * slot combination is reversible, this should reverse it and attempt * to reconstruct the original split ParserOutputs from the merged * ParserOutput. * * @param RenderedRevision $rrev * @param ParserOptions $options * @param ?ParserOutput $previousOutput A combined ParserOutput for a * previous parse, or null if none available. * @return array A mapping from role name to a * previous ParserOutput for that slot in the previous parse */ private function splitSlotOutput( RenderedRevision $rrev, ParserOptions $options, ?ParserOutput $previousOutput ) { // If there is no previous parse, then there is nothing to split. $revision = $rrev->getRevision(); $revslots = $revision->getSlots(); if ( $previousOutput === null ) { return array_fill_keys( $revslots->getSlotRoles(), null ); } // short circuit if there is only the main slot // T351026 hack: if use-parsoid is set, only return main slot output for now // T351113 will remove this hack. if ( $revslots->getSlotRoles() === [ SlotRecord::MAIN ] || $options->getUseParsoid() ) { return [ SlotRecord::MAIN => $previousOutput ]; } // @todo Currently slot combination is not reversible return array_fill_keys( $revslots->getSlotRoles(), null ); } }