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
81 public function setLogger( LoggerInterface $saveParseLogger ) {
82 $this->saveParseLogger = $saveParseLogger;
83 }
84
85 // phpcs:disable Generic.Files.LineLength.TooLong
112 // phpcs:enable Generic.Files.LineLength.TooLong
113 public function getRenderedRevision(
114 RevisionRecord $rev,
115 ?ParserOptions $options = null,
116 ?Authority $forPerformer = null,
117 array $hints = []
118 ) {
119 if ( $rev->getWikiId() !== $this->dbDomain ) {
120 throw new InvalidArgumentException( 'Mismatching wiki ID ' . $rev->getWikiId() );
121 }
122
123 $audience = $hints['audience']
125
126 if ( !$rev->audienceCan( RevisionRecord::DELETED_TEXT, $audience, $forPerformer ) ) {
127 // Returning null here is awkward, but consistent with the signature of
128 // RevisionRecord::getContent().
129 return null;
130 }
131
132 if ( !$options ) {
133 $options = $forPerformer ?
134 ParserOptions::newFromUser( $forPerformer->getUser() ) :
135 ParserOptions::newFromAnon();
136 }
137
138 if ( isset( $hints['causeAction'] ) ) {
139 $options->setRenderReason( $hints['causeAction'] );
140 }
141
142 $usePrimary = $hints['use-master'] ?? false;
143
144 $dbIndex = $usePrimary
145 ? DB_PRIMARY // use latest values
146 : DB_REPLICA; // T154554
147
148 $options->setSpeculativeRevIdCallback( function () use ( $dbIndex ) {
149 return $this->getSpeculativeRevId( $dbIndex );
150 } );
151 $options->setSpeculativePageIdCallback( function () use ( $dbIndex ) {
152 return $this->getSpeculativePageId( $dbIndex );
153 } );
154
155 if ( !$rev->getId() && $rev->getTimestamp() ) {
156 // This is an unsaved revision with an already determined timestamp.
157 // Make the "current" time used during parsing match that of the revision.
158 // Any REVISION* parser variables will match up if the revision is saved.
159 $options->setTimestamp( $rev->getTimestamp() );
160 }
161
162 $previousOutput = $hints['previous-output'] ?? null;
163 $renderedRevision = new RenderedRevision(
164 $rev,
165 $options,
166 $this->contentRenderer,
167 function ( RenderedRevision $rrev, array $hints ) use ( $options, $previousOutput ) {
168 $h = [ 'previous-output' => $previousOutput ] + $hints;
169 return $this->combineSlotOutput( $rrev, $options, $h );
170 },
171 $audience,
172 $forPerformer
173 );
174
175 $renderedRevision->setSaveParseLogger( $this->saveParseLogger );
176
177 if ( isset( $hints['known-revision-output'] ) ) {
178 $renderedRevision->setRevisionParserOutput( $hints['known-revision-output'] );
179 }
180
181 return $renderedRevision;
182 }
183
184 private function getSpeculativeRevId( $dbIndex ) {
185 // Use a separate primary DB connection in order to see the latest data, by avoiding
186 // stale data from REPEATABLE-READ snapshots.
187 $flags = ILoadBalancer::CONN_TRX_AUTOCOMMIT;
188
189 $db = $this->loadBalancer->getConnection( $dbIndex, [], $this->dbDomain, $flags );
190
191 return 1 + (int)$db->newSelectQueryBuilder()
192 ->select( 'MAX(rev_id)' )
193 ->from( 'revision' )
194 ->caller( __METHOD__ )->fetchField();
195 }
196
197 private function getSpeculativePageId( $dbIndex ) {
198 // Use a separate primary DB connection in order to see the latest data, by avoiding
199 // stale data from REPEATABLE-READ snapshots.
200 $flags = ILoadBalancer::CONN_TRX_AUTOCOMMIT;
201
202 $db = $this->loadBalancer->getConnection( $dbIndex, [], $this->dbDomain, $flags );
203
204 return 1 + (int)$db->newSelectQueryBuilder()
205 ->select( 'MAX(page_id)' )
206 ->from( 'page' )
207 ->caller( __METHOD__ )->fetchField();
208 }
209
224 private function combineSlotOutput( RenderedRevision $rrev, ParserOptions $options, array $hints = [] ) {
225 $revision = $rrev->getRevision();
226 $slots = $revision->getSlots()->getSlots();
227
228 $withHtml = $hints['generate-html'] ?? true;
229 $previousOutputs = $this->splitSlotOutput( $rrev, $options, $hints['previous-output'] ?? null );
230
231 // short circuit if there is only the main slot
232 // T351026 hack: if use-parsoid is set, only return main slot output for now
233 // T351113 will remove this hack.
234 if ( array_keys( $slots ) === [ SlotRecord::MAIN ] || $options->getUseParsoid() ) {
235 $h = [ 'previous-output' => $previousOutputs[SlotRecord::MAIN] ] + $hints;
236 return $rrev->getSlotParserOutput( SlotRecord::MAIN, $h );
237 }
238
239 // move main slot to front
240 if ( isset( $slots[SlotRecord::MAIN] ) ) {
241 $slots = [ SlotRecord::MAIN => $slots[SlotRecord::MAIN] ] + $slots;
242 }
243
244 $combinedOutput = new ParserOutput( null );
245 $slotOutput = [];
246
247 $options = $rrev->getOptions();
248 $options->registerWatcher( [ $combinedOutput, 'recordOption' ] );
249
250 foreach ( $slots as $role => $slot ) {
251 $h = [ 'previous-output' => $previousOutputs[$role] ] + $hints;
252 $out = $rrev->getSlotParserOutput( $role, $h );
253 $slotOutput[$role] = $out;
254
255 // XXX: should the SlotRoleHandler be able to intervene here?
256 $combinedOutput->mergeInternalMetaDataFrom( $out );
257 $combinedOutput->mergeTrackingMetaDataFrom( $out );
258 }
259
260 if ( $withHtml ) {
261 $html = '';
262 $first = true;
264 foreach ( $slotOutput as $role => $out ) {
265 $roleHandler = $this->roleRegistry->getRoleHandler( $role );
266
267 // TODO: put more fancy layout logic here, see T200915.
268 $layout = $roleHandler->getOutputLayoutHints();
269 $display = $layout['display'] ?? 'section';
270
271 if ( $display === 'none' ) {
272 continue;
273 }
274
275 if ( $first ) {
276 // skip header for the first slot
277 $first = false;
278 } else {
279 // NOTE: this placeholder is hydrated by ParserOutput::getText().
280 $headText = Html::element( 'mw:slotheader', [], $role );
281 $html .= Html::rawElement( 'h1', [ 'class' => 'mw-slot-header' ], $headText );
282 }
283
284 // XXX: do we want to put a wrapper div around the output?
285 // Do we want to let $roleHandler do that?
286 $html .= $out->getRawText();
287 $combinedOutput->mergeHtmlMetaDataFrom( $out );
288 }
289
290 $combinedOutput->setRawText( $html );
291 }
292
293 $options->registerWatcher( null );
294 return $combinedOutput;
295 }
296
314 private function splitSlotOutput( RenderedRevision $rrev, ParserOptions $options, ?ParserOutput $previousOutput ) {
315 // If there is no previous parse, then there is nothing to split.
316 $revision = $rrev->getRevision();
317 $revslots = $revision->getSlots();
318 if ( $previousOutput === null ) {
319 return array_fill_keys( $revslots->getSlotRoles(), null );
320 }
321
322 // short circuit if there is only the main slot
323 // T351026 hack: if use-parsoid is set, only return main slot output for now
324 // T351113 will remove this hack.
325 if ( $revslots->getSlotRoles() === [ SlotRecord::MAIN ] || $options->getUseParsoid() ) {
326 return [ SlotRecord::MAIN => $previousOutput ];
327 }
328
329 // @todo Currently slot combination is not reversible
330 return array_fill_keys( $revslots->getSlotRoles(), null );
331 }
332}
This class is a collection of static functions that serve two purposes:
Definition Html.php:56
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: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