MediaWiki master
RevisionRenderer.php
Go to the documentation of this file.
1<?php
23namespace MediaWiki\Revision;
24
25use InvalidArgumentException;
31use Psr\Log\LoggerInterface;
32use Psr\Log\NullLogger;
34
46
48 private $saveParseLogger;
49
51 private $loadBalancer;
52
54 private $roleRegistry;
55
57 private $contentRenderer;
58
60 private $dbDomain;
61
68 public function __construct(
69 ILoadBalancer $loadBalancer,
70 SlotRoleRegistry $roleRegistry,
71 ContentRenderer $contentRenderer,
72 $dbDomain = false
73 ) {
74 $this->loadBalancer = $loadBalancer;
75 $this->roleRegistry = $roleRegistry;
76 $this->contentRenderer = $contentRenderer;
77 $this->dbDomain = $dbDomain;
78 $this->saveParseLogger = new NullLogger();
79 }
80
84 public function setLogger( LoggerInterface $saveParseLogger ) {
85 $this->saveParseLogger = $saveParseLogger;
86 }
87
88 // phpcs:disable Generic.Files.LineLength.TooLong
115 // phpcs:enable Generic.Files.LineLength.TooLong
116 public function getRenderedRevision(
117 RevisionRecord $rev,
118 ParserOptions $options = null,
119 Authority $forPerformer = null,
120 array $hints = []
121 ) {
122 if ( $rev->getWikiId() !== $this->dbDomain ) {
123 throw new InvalidArgumentException( 'Mismatching wiki ID ' . $rev->getWikiId() );
124 }
125
126 $audience = $hints['audience']
128
129 if ( !$rev->audienceCan( RevisionRecord::DELETED_TEXT, $audience, $forPerformer ) ) {
130 // Returning null here is awkward, but consistent with the signature of
131 // RevisionRecord::getContent().
132 return null;
133 }
134
135 if ( !$options ) {
136 $options = $forPerformer ?
137 ParserOptions::newFromUser( $forPerformer->getUser() ) :
138 ParserOptions::newFromAnon();
139 }
140
141 if ( isset( $hints['causeAction'] ) ) {
142 $options->setRenderReason( $hints['causeAction'] );
143 }
144
145 $usePrimary = $hints['use-master'] ?? false;
146
147 $dbIndex = $usePrimary
148 ? DB_PRIMARY // use latest values
149 : DB_REPLICA; // T154554
150
151 $options->setSpeculativeRevIdCallback( function () use ( $dbIndex ) {
152 return $this->getSpeculativeRevId( $dbIndex );
153 } );
154 $options->setSpeculativePageIdCallback( function () use ( $dbIndex ) {
155 return $this->getSpeculativePageId( $dbIndex );
156 } );
157
158 if ( !$rev->getId() && $rev->getTimestamp() ) {
159 // This is an unsaved revision with an already determined timestamp.
160 // Make the "current" time used during parsing match that of the revision.
161 // Any REVISION* parser variables will match up if the revision is saved.
162 $options->setTimestamp( $rev->getTimestamp() );
163 }
164
165 $previousOutput = $hints['previous-output'] ?? null;
166 $renderedRevision = new RenderedRevision(
167 $rev,
168 $options,
169 $this->contentRenderer,
170 function ( RenderedRevision $rrev, array $hints ) use ( $options, $previousOutput ) {
171 $h = [ 'previous-output' => $previousOutput ] + $hints;
172 return $this->combineSlotOutput( $rrev, $options, $h );
173 },
174 $audience,
175 $forPerformer
176 );
177
178 $renderedRevision->setSaveParseLogger( $this->saveParseLogger );
179
180 if ( isset( $hints['known-revision-output'] ) ) {
181 $renderedRevision->setRevisionParserOutput( $hints['known-revision-output'] );
182 }
183
184 return $renderedRevision;
185 }
186
187 private function getSpeculativeRevId( $dbIndex ) {
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(rev_id)' )
196 ->from( 'revision' )
197 ->caller( __METHOD__ )->fetchField();
198 }
199
200 private function getSpeculativePageId( $dbIndex ) {
201 // Use a separate primary DB connection in order to see the latest data, by avoiding
202 // stale data from REPEATABLE-READ snapshots.
203 $flags = ILoadBalancer::CONN_TRX_AUTOCOMMIT;
204
205 $db = $this->loadBalancer->getConnection( $dbIndex, [], $this->dbDomain, $flags );
206
207 return 1 + (int)$db->newSelectQueryBuilder()
208 ->select( 'MAX(page_id)' )
209 ->from( 'page' )
210 ->caller( __METHOD__ )->fetchField();
211 }
212
227 private function combineSlotOutput( RenderedRevision $rrev, ParserOptions $options, array $hints = [] ) {
228 $revision = $rrev->getRevision();
229 $slots = $revision->getSlots()->getSlots();
230
231 $withHtml = $hints['generate-html'] ?? true;
232 $previousOutputs = $this->splitSlotOutput( $rrev, $options, $hints['previous-output'] ?? null );
233
234 // short circuit if there is only the main slot
235 // T351026 hack: if use-parsoid is set, only return main slot output for now
236 // T351113 will remove this hack.
237 if ( array_keys( $slots ) === [ SlotRecord::MAIN ] || $options->getUseParsoid() ) {
238 $h = [ 'previous-output' => $previousOutputs[SlotRecord::MAIN] ] + $hints;
239 return $rrev->getSlotParserOutput( SlotRecord::MAIN, $h );
240 }
241
242 // move main slot to front
243 if ( isset( $slots[SlotRecord::MAIN] ) ) {
244 $slots = [ SlotRecord::MAIN => $slots[SlotRecord::MAIN] ] + $slots;
245 }
246
247 $combinedOutput = new ParserOutput( null );
248 $slotOutput = [];
249
250 $options = $rrev->getOptions();
251 $options->registerWatcher( [ $combinedOutput, 'recordOption' ] );
252
253 foreach ( $slots as $role => $slot ) {
254 $h = [ 'previous-output' => $previousOutputs[$role] ] + $hints;
255 $out = $rrev->getSlotParserOutput( $role, $h );
256 $slotOutput[$role] = $out;
257
258 // XXX: should the SlotRoleHandler be able to intervene here?
259 $combinedOutput->mergeInternalMetaDataFrom( $out );
260 $combinedOutput->mergeTrackingMetaDataFrom( $out );
261 }
262
263 if ( $withHtml ) {
264 $html = '';
265 $first = true;
267 foreach ( $slotOutput as $role => $out ) {
268 $roleHandler = $this->roleRegistry->getRoleHandler( $role );
269
270 // TODO: put more fancy layout logic here, see T200915.
271 $layout = $roleHandler->getOutputLayoutHints();
272 $display = $layout['display'] ?? 'section';
273
274 if ( $display === 'none' ) {
275 continue;
276 }
277
278 if ( $first ) {
279 // skip header for the first slot
280 $first = false;
281 } else {
282 // NOTE: this placeholder is hydrated by ParserOutput::getText().
283 $headText = Html::element( 'mw:slotheader', [], $role );
284 $html .= Html::rawElement( 'h1', [ 'class' => 'mw-slot-header' ], $headText );
285 }
286
287 // XXX: do we want to put a wrapper div around the output?
288 // Do we want to let $roleHandler do that?
289 $html .= $out->getRawText();
290 $combinedOutput->mergeHtmlMetaDataFrom( $out );
291 }
292
293 $combinedOutput->setRawText( $html );
294 }
295
296 $options->registerWatcher( null );
297 return $combinedOutput;
298 }
299
317 private function splitSlotOutput( RenderedRevision $rrev, ParserOptions $options, ?ParserOutput $previousOutput ) {
318 // If there is no previous parse, then there is nothing to split.
319 $revision = $rrev->getRevision();
320 $revslots = $revision->getSlots();
321 if ( $previousOutput === null ) {
322 return array_fill_keys( $revslots->getSlotRoles(), null );
323 }
324
325 // short circuit if there is only the main slot
326 // T351026 hack: if use-parsoid is set, only return main slot output for now
327 // T351113 will remove this hack.
328 if ( $revslots->getSlotRoles() === [ SlotRecord::MAIN ] || $options->getUseParsoid() ) {
329 return [ SlotRecord::MAIN => $previousOutput ];
330 }
331
332 // @todo Currently slot combination is not reversible
333 return array_fill_keys( $revslots->getSlotRoles(), null );
334 }
335}
This class is a collection of static functions that serve two purposes:
Definition Html.php:56
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.
audienceCan( $field, $audience, Authority $performer=null)
Check that the given audience has access to the given field.
getWikiId()
Get the ID of the wiki this revision belongs to.
getTimestamp()
MCR migration note: this replaced Revision::getTimestamp.
getId( $wikiId=self::LOCAL)
Get revision ID.
The RevisionRenderer service provides access to rendered output for revisions.
setLogger(LoggerInterface $saveParseLogger)
__construct(ILoadBalancer $loadBalancer, SlotRoleRegistry $roleRegistry, ContentRenderer $contentRenderer, $dbDomain=false)
getRenderedRevision(RevisionRecord $rev, ParserOptions $options=null, Authority $forPerformer=null, array $hints=[])
A registry service for SlotRoleHandlers, used to define which slot roles are available on which page.
Set options of the Parser.
setRenderReason(string $renderReason)
Sets reason for rendering the content.
setTimestamp( $x)
Timestamp used for {{CURRENTDAY}} etc.
getUseParsoid()
Parsoid-format HTML output, or legacy wikitext parser HTML?
registerWatcher( $callback)
Registers a callback for tracking which ParserOptions which are used.
setSpeculativeRevIdCallback( $x)
Callback to generate a guess for {{REVISIONID}}.
setSpeculativePageIdCallback( $x)
Callback to generate a guess for {{PAGEID}}.
This interface represents the authority associated with the current execution context,...
Definition Authority.php:37
This class is a delegate to ILBFactory for a given database cluster.
element(SerializerNode $parent, SerializerNode $node, $contents)
const DB_REPLICA
Definition defines.php:26
const DB_PRIMARY
Definition defines.php:28