MediaWiki master
RevisionRenderer.php
Go to the documentation of this file.
1<?php
9namespace MediaWiki\Revision;
10
11use InvalidArgumentException;
18use Psr\Log\LoggerAwareInterface;
19use Psr\Log\LoggerInterface;
20use Psr\Log\NullLogger;
22
33class RevisionRenderer implements LoggerAwareInterface {
34
36 private $saveParseLogger;
37
39 private $loadBalancer;
40
42 private $roleRegistry;
43
45 private $contentRenderer;
46
48 private $dbDomain;
49
56 public function __construct(
57 ILoadBalancer $loadBalancer,
58 SlotRoleRegistry $roleRegistry,
59 ContentRenderer $contentRenderer,
60 $dbDomain = false
61 ) {
62 $this->loadBalancer = $loadBalancer;
63 $this->roleRegistry = $roleRegistry;
64 $this->contentRenderer = $contentRenderer;
65 $this->dbDomain = $dbDomain;
66 $this->saveParseLogger = new NullLogger();
67 }
68
70 public function setLogger( LoggerInterface $saveParseLogger ): void {
71 $this->saveParseLogger = $saveParseLogger;
72 }
73
101 public function getRenderedRevision(
102 RevisionRecord $rev,
103 ?ParserOptions $options = null,
104 ?Authority $forPerformer = null,
105 array $hints = []
106 ) {
107 if ( $rev->getWikiId() !== $this->dbDomain ) {
108 throw new InvalidArgumentException(
109 "Mismatching wiki ID rev={$rev->getWikiId()}, this={$this->dbDomain}"
110 );
111 }
112
113 $audience = $hints['audience']
115
116 if ( !$rev->audienceCan( RevisionRecord::DELETED_TEXT, $audience, $forPerformer ) ) {
117 // Returning null here is awkward, but consistent with the signature of
118 // RevisionRecord::getContent().
119 return null;
120 }
121
122 if ( !$options ) {
123 $options = $forPerformer ?
124 ParserOptions::newFromUser( $forPerformer->getUser() ) :
125 ParserOptions::newFromAnon();
126 }
127
128 if ( isset( $hints['causeAction'] ) ) {
129 $options->setRenderReason( $hints['causeAction'] );
130 }
131
132 $usePrimary = $hints['use-master'] ?? false;
133
134 $dbIndex = $usePrimary
135 ? DB_PRIMARY // use latest values
136 : DB_REPLICA; // T154554
137
138 $options->setSpeculativeRevIdCallback( function () use ( $dbIndex ) {
139 return $this->getSpeculativeRevId( $dbIndex );
140 } );
141 $options->setSpeculativePageIdCallback( function () use ( $dbIndex ) {
142 return $this->getSpeculativePageId( $dbIndex );
143 } );
144
145 if ( !$rev->getId() && $rev->getTimestamp() ) {
146 // This is an unsaved revision with an already determined timestamp.
147 // Make the "current" time used during parsing match that of the revision.
148 // Any REVISION* parser variables will match up if the revision is saved.
149 $options->setTimestamp( $rev->getTimestamp() );
150 }
151
152 $previousOutput = $hints['previous-output'] ?? null;
153 $renderedRevision = new RenderedRevision(
154 $rev,
155 $options,
156 $this->contentRenderer,
157 function ( RenderedRevision $rrev, array $hints ) use ( $options, $previousOutput ) {
158 $h = [ 'previous-output' => $previousOutput ] + $hints;
159 return $this->combineSlotOutput( $rrev, $options, $h );
160 },
161 $audience,
162 $forPerformer
163 );
164
165 $renderedRevision->setSaveParseLogger( $this->saveParseLogger );
166
167 if ( isset( $hints['known-revision-output'] ) ) {
168 $renderedRevision->setRevisionParserOutput( $hints['known-revision-output'] );
169 }
170
171 return $renderedRevision;
172 }
173
174 private function getSpeculativeRevId( int $dbIndex ): int {
175 // Use a separate primary DB connection in order to see the latest data, by avoiding
176 // stale data from REPEATABLE-READ snapshots.
177 $flags = ILoadBalancer::CONN_TRX_AUTOCOMMIT;
178
179 $db = $this->loadBalancer->getConnection( $dbIndex, [], $this->dbDomain, $flags );
180
181 return 1 + (int)$db->newSelectQueryBuilder()
182 ->select( 'MAX(rev_id)' )
183 ->from( 'revision' )
184 ->caller( __METHOD__ )->fetchField();
185 }
186
187 private function getSpeculativePageId( int $dbIndex ): int {
188 // Use a separate primary DB connection in order to see the latest data, by avoiding
189 // stale data from REPEATABLE-READ snapshots.
190 $flags = ILoadBalancer::CONN_TRX_AUTOCOMMIT;
191
192 $db = $this->loadBalancer->getConnection( $dbIndex, [], $this->dbDomain, $flags );
193
194 return 1 + (int)$db->newSelectQueryBuilder()
195 ->select( 'MAX(page_id)' )
196 ->from( 'page' )
197 ->caller( __METHOD__ )->fetchField();
198 }
199
214 private function combineSlotOutput( RenderedRevision $rrev, ParserOptions $options, array $hints = [] ) {
215 $revision = $rrev->getRevision();
216 $slots = $revision->getSlots()->getSlots();
217
218 $withHtml = $hints['generate-html'] ?? true;
219 $previousOutputs = $this->splitSlotOutput( $rrev, $options, $hints['previous-output'] ?? null );
220
221 // short circuit if there is only the main slot
222 // T351026 hack: if use-parsoid is set, only return main slot output for now
223 // T351113 will remove this hack.
224 if ( array_keys( $slots ) === [ SlotRecord::MAIN ] || $options->getUseParsoid() ) {
225 $h = [ 'previous-output' => $previousOutputs[SlotRecord::MAIN] ] + $hints;
226 return $rrev->getSlotParserOutput( SlotRecord::MAIN, $h );
227 }
228
229 // move main slot to front
230 if ( isset( $slots[SlotRecord::MAIN] ) ) {
231 $slots = [ SlotRecord::MAIN => $slots[SlotRecord::MAIN] ] + $slots;
232 }
233
234 $combinedOutput = new ParserOutput( null );
235 $slotOutput = [];
236
237 $options = $rrev->getOptions();
238 $options->registerWatcher( $combinedOutput->recordOption( ... ) );
239
240 foreach ( $slots as $role => $slot ) {
241 $h = [ 'previous-output' => $previousOutputs[$role] ] + $hints;
242 $out = $rrev->getSlotParserOutput( $role, $h );
243 $slotOutput[$role] = $out;
244
245 // XXX: should the SlotRoleHandler be able to intervene here?
246 // XXX: this should probably just use ParserOutput::collectMetadata
247 $combinedOutput->mergeInternalMetaDataFrom( $out );
248 $combinedOutput->mergeTrackingMetaDataFrom( $out );
249 }
250
251 if ( $withHtml ) {
252 $html = '';
253 $first = true;
255 foreach ( $slotOutput as $role => $out ) {
256 $roleHandler = $this->roleRegistry->getRoleHandler( $role );
257
258 // TODO: put more fancy layout logic here, see T200915.
259 $layout = $roleHandler->getOutputLayoutHints();
260 $display = $layout['display'] ?? 'section';
261
262 if ( $display === 'none' ) {
263 continue;
264 }
265
266 if ( $first ) {
267 // skip header for the first slot
268 $first = false;
269 } else {
270 // NOTE: this placeholder is hydrated by ParserOutput::getText().
271 $headText = Html::element( 'mw:slotheader', [], $role );
272 $html .= Html::rawElement( 'h1', [ 'class' => 'mw-slot-header' ], $headText );
273 $combinedOutput->setOutputFlag( ParserOutputFlags::HAS_SLOT_HEADERS );
274 }
275
276 // XXX: do we want to put a wrapper div around the output?
277 // Do we want to let $roleHandler do that?
278 $html .= $out->getContentHolderText();
279 $combinedOutput->mergeHtmlMetaDataFrom( $out );
280 }
281
282 $combinedOutput->setContentHolderText( $html );
283 }
284
285 $options->registerWatcher( null );
286 return $combinedOutput;
287 }
288
306 private function splitSlotOutput( RenderedRevision $rrev, ParserOptions $options, ?ParserOutput $previousOutput ) {
307 // If there is no previous parse, then there is nothing to split.
308 $revision = $rrev->getRevision();
309 $revslots = $revision->getSlots();
310 if ( $previousOutput === null ) {
311 return array_fill_keys( $revslots->getSlotRoles(), null );
312 }
313
314 // short circuit if there is only the main slot
315 // T351026 hack: if use-parsoid is set, only return main slot output for now
316 // T351113 will remove this hack.
317 if ( $revslots->getSlotRoles() === [ SlotRecord::MAIN ] || $options->getUseParsoid() ) {
318 return [ SlotRecord::MAIN => $previousOutput ];
319 }
320
321 // @todo Currently slot combination is not reversible
322 return array_fill_keys( $revslots->getSlotRoles(), null );
323 }
324}
const DB_REPLICA
Definition defines.php:26
const DB_PRIMARY
Definition defines.php:28
This class is a collection of static functions that serve two purposes:
Definition Html.php:44
Set options of the Parser.
ParserOutput is a rendering of a Content object or a message.
RenderedRevision represents the rendered representation of a revision.
setSaveParseLogger(LoggerInterface $saveParseLogger)
Page revision base class.
getWikiId()
Get the ID of the wiki this revision belongs to.
getTimestamp()
MCR migration note: this replaced Revision::getTimestamp.
audienceCan( $field, $audience, ?Authority $performer=null)
Check that the given audience has access to the given field.
getId( $wikiId=self::LOCAL)
Get revision ID.
The RevisionRenderer service provides access to rendered output for revisions.
setLogger(LoggerInterface $saveParseLogger)
getRenderedRevision(RevisionRecord $rev, ?ParserOptions $options=null, ?Authority $forPerformer=null, array $hints=[])
__construct(ILoadBalancer $loadBalancer, SlotRoleRegistry $roleRegistry, ContentRenderer $contentRenderer, $dbDomain=false)
A registry service for SlotRoleHandlers, used to define which slot roles are available on which page.
This interface represents the authority associated with the current execution context,...
Definition Authority.php:23
This class is a delegate to ILBFactory for a given database cluster.
getConnection( $i, $groups=[], string|false $domain=false, $flags=0)
Get a lazy-connecting database handle for a specific or virtual (DB_PRIMARY/DB_REPLICA) server index.