MediaWiki REL1_39
RenderedRevision.php
Go to the documentation of this file.
1<?php
23namespace MediaWiki\Revision;
24
25use Content;
26use InvalidArgumentException;
27use LogicException;
33use ParserOutput;
34use Psr\Log\LoggerInterface;
35use Psr\Log\NullLogger;
36use Wikimedia\Assert\Assert;
37
46
48 private $revision;
49
53 private $options;
54
58 private $audience = RevisionRecord::FOR_PUBLIC;
59
63 private $performer = null;
64
69 private $revisionOutput = null;
70
75 private $slotsOutput = [];
76
81 private $combineOutput;
82
86 private $saveParseLogger;
87
91 private $contentRenderer;
92
109 public function __construct(
110 RevisionRecord $revision,
111 ParserOptions $options,
112 ContentRenderer $contentRenderer,
113 callable $combineOutput,
114 $audience = RevisionRecord::FOR_PUBLIC,
115 Authority $performer = null
116 ) {
117 $this->options = $options;
118
119 $this->setRevisionInternal( $revision );
120
121 $this->contentRenderer = $contentRenderer;
122 $this->combineOutput = $combineOutput;
123 $this->saveParseLogger = new NullLogger();
124
125 if ( $audience === RevisionRecord::FOR_THIS_USER && !$performer ) {
126 throw new InvalidArgumentException(
127 'User must be specified when setting audience to FOR_THIS_USER'
128 );
129 }
130
131 $this->audience = $audience;
132 $this->performer = $performer;
133 }
134
138 public function setSaveParseLogger( LoggerInterface $saveParseLogger ) {
139 $this->saveParseLogger = $saveParseLogger;
140 }
141
145 public function isContentDeleted() {
146 return $this->revision->isDeleted( RevisionRecord::DELETED_TEXT );
147 }
148
152 public function getRevision() {
153 return $this->revision;
154 }
155
159 public function getOptions() {
160 return $this->options;
161 }
162
173 public function setRevisionParserOutput( ParserOutput $output ) {
174 $this->revisionOutput = $output;
175
176 // If there is only one slot, we assume that the combined output is identical
177 // with the main slot's output. This is intended to prevent a redundant re-parse of
178 // the content in case getSlotParserOutput( SlotRecord::MAIN ) is called, for instance
179 // from ContentHandler::getSecondaryDataUpdates.
180 if ( $this->revision->getSlotRoles() === [ SlotRecord::MAIN ] ) {
181 $this->slotsOutput[ SlotRecord::MAIN ] = $output;
182 }
183 }
184
193 public function getRevisionParserOutput( array $hints = [] ) {
194 $withHtml = $hints['generate-html'] ?? true;
195
196 if ( !$this->revisionOutput
197 || ( $withHtml && !$this->revisionOutput->hasText() )
198 ) {
199 $output = call_user_func( $this->combineOutput, $this, $hints );
200
201 Assert::postcondition(
202 $output instanceof ParserOutput,
203 'Callback did not return a ParserOutput object!'
204 );
205
206 $this->revisionOutput = $output;
207 }
208
209 return $this->revisionOutput;
210 }
211
223 public function getSlotParserOutput( $role, array $hints = [] ) {
224 $withHtml = $hints['generate-html'] ?? true;
225
226 if ( !isset( $this->slotsOutput[ $role ] )
227 || ( $withHtml && !$this->slotsOutput[ $role ]->hasText() )
228 ) {
229 $content = $this->revision->getContent( $role, $this->audience, $this->performer );
230
231 if ( !$content ) {
232 throw new SuppressedDataException(
233 'Access to the content has been suppressed for this audience'
234 );
235 } else {
236 // XXX: allow SlotRoleHandler to control the ParserOutput?
237 $output = $this->getSlotParserOutputUncached( $content, $withHtml );
238
239 if ( $withHtml && !$output->hasText() ) {
240 throw new LogicException(
241 'HTML generation was requested, but '
242 . get_class( $content )
243 . ' that passed to '
244 . 'ContentRenderer::getParserOutput() returns a ParserOutput with no text set.'
245 );
246 }
247
248 // Detach watcher, to ensure option use is not recorded in the wrong ParserOutput.
249 $this->options->registerWatcher( null );
250 }
251
252 $this->slotsOutput[ $role ] = $output;
253 }
254
255 return $this->slotsOutput[$role];
256 }
257
264 private function getSlotParserOutputUncached( Content $content, $withHtml ) {
265 $parserOutput = $this->contentRenderer->getParserOutput(
266 $content,
267 $this->revision->getPage(),
268 $this->revision->getId(),
269 $this->options,
270 $withHtml
271 );
272 // Save the rev_id and timestamp so that we don't have to load the revision row on view
273 $parserOutput->setCacheRevisionId( $this->revision->getId() );
274 $parserOutput->setTimestamp( $this->revision->getTimestamp() );
275 return $parserOutput;
276 }
277
287 public function updateRevision( RevisionRecord $rev ) {
288 if ( $rev->getId() === $this->revision->getId() ) {
289 return;
290 }
291
292 if ( $this->revision->getId() ) {
293 throw new LogicException( 'RenderedRevision already has a revision with ID '
294 . $this->revision->getId() . ', can\'t update to revision with ID ' . $rev->getId() );
295 }
296
297 if ( !$this->revision->getSlots()->hasSameContent( $rev->getSlots() ) ) {
298 throw new LogicException( 'Cannot update to a revision with different content!' );
299 }
300
301 $this->setRevisionInternal( $rev );
302
303 $this->pruneRevisionSensitiveOutput(
304 $this->revision->getPageId(),
305 $this->revision->getId(),
306 $this->revision->getTimestamp()
307 );
308 }
309
323 private function pruneRevisionSensitiveOutput(
324 $actualPageId,
325 $actualRevId,
326 $actualRevTimestamp
327 ) {
328 if ( $this->revisionOutput ) {
329 if ( $this->outputVariesOnRevisionMetaData(
330 $this->revisionOutput,
331 $actualPageId,
332 $actualRevId,
333 $actualRevTimestamp
334 ) ) {
335 $this->revisionOutput = null;
336 }
337 } else {
338 $this->saveParseLogger->debug( __METHOD__ . ": no prepared revision output" );
339 }
340
341 foreach ( $this->slotsOutput as $role => $output ) {
342 if ( $this->outputVariesOnRevisionMetaData(
343 $output,
344 $actualPageId,
345 $actualRevId,
346 $actualRevTimestamp
347 ) ) {
348 unset( $this->slotsOutput[$role] );
349 }
350 }
351 }
352
356 private function setRevisionInternal( RevisionRecord $revision ) {
357 $this->revision = $revision;
358
359 // Force the parser to use $this->revision to resolve magic words like {{REVISIONUSER}}
360 // if the revision is either known to be complete, or it doesn't have a revision ID set.
361 // If it's incomplete and we have a revision ID, the parser can do better by loading
362 // the revision from the database if needed to handle a magic word.
363 //
364 // The following considerations inform the logic described above:
365 //
366 // 1) If we have a saved revision already loaded, we want the parser to use it, instead of
367 // loading it again.
368 //
369 // 2) If the revision is a fake that wraps some kind of synthetic content, such as an
370 // error message from Article, it should be used directly and things like {{REVISIONUSER}}
371 // should not expected to work, since there may not even be an actual revision to
372 // refer to.
373 //
374 // 3) If the revision is a fake constructed around a page, a Content object, and
375 // a revision ID, to provide backwards compatibility to code that has access to those
376 // but not to a complete RevisionRecord for rendering, then we want the Parser to
377 // load the actual revision from the database when it encounters a magic word like
378 // {{REVISIONUSER}}, but we don't want to load that revision ahead of time just in case.
379 //
380 // 4) Previewing an edit to a template should use the submitted unsaved
381 // MutableRevisionRecord for self-transclusions in the template's documentation (see T7278).
382 // That revision would be complete except for the ID field.
383 //
384 // 5) Pre-save transform would provide a RevisionRecord that has all meta-data but is
385 // incomplete due to not yet having content set. However, since it doesn't have a revision
386 // ID either, the below code would still force it to be used, allowing
387 // {{subst::REVISIONUSER}} to function as expected.
388
389 if ( $this->revision->isReadyForInsertion() || !$this->revision->getId() ) {
390 $oldCallback = $this->options->getCurrentRevisionRecordCallback();
391 $this->options->setCurrentRevisionRecordCallback(
392 function ( PageReference $parserPage, $parser = null ) use ( $oldCallback ) {
393 if ( $this->revision->getPage()->isSamePageAs( $parserPage ) ) {
394 return $this->revision;
395 } else {
396 return call_user_func( $oldCallback, $parserPage, $parser );
397 }
398 }
399 );
400 }
401 }
402
416 private function outputVariesOnRevisionMetaData(
417 ParserOutput $parserOutput,
418 $actualPageId,
419 $actualRevId,
420 $actualRevTimestamp
421 ) {
422 $logger = $this->saveParseLogger;
423 $varyMsg = __METHOD__ . ": cannot use prepared output for '{title}'";
424 $context = [ 'title' => (string)$this->revision->getPage() ];
425
426 if ( $parserOutput->getOutputFlag( ParserOutputFlags::VARY_REVISION ) ) {
427 // If {{PAGEID}} resolved to 0, then that word need to resolve to the actual page ID
428 $logger->info( "$varyMsg (vary-revision)", $context );
429 return true;
430 } elseif (
431 $parserOutput->getOutputFlag( ParserOutputFlags::VARY_REVISION_ID )
432 && $actualRevId !== false
433 && ( $actualRevId === true || $parserOutput->getSpeculativeRevIdUsed() !== $actualRevId )
434 ) {
435 $logger->info( "$varyMsg (vary-revision-id and wrong ID)", $context );
436 return true;
437 } elseif (
438 $parserOutput->getOutputFlag( ParserOutputFlags::VARY_REVISION_TIMESTAMP )
439 && $actualRevTimestamp !== false
440 && ( $actualRevTimestamp === true ||
441 $parserOutput->getRevisionTimestampUsed() !== $actualRevTimestamp )
442 ) {
443 $logger->info( "$varyMsg (vary-revision-timestamp and wrong timestamp)", $context );
444 return true;
445 } elseif (
446 $parserOutput->getOutputFlag( ParserOutputFlags::VARY_PAGE_ID )
447 && $actualPageId !== false
448 && ( $actualPageId === true || $parserOutput->getSpeculativePageIdUsed() !== $actualPageId )
449 ) {
450 $logger->info( "$varyMsg (vary-page-id and wrong ID)", $context );
451 return true;
452 } elseif ( $parserOutput->getOutputFlag( ParserOutputFlags::VARY_REVISION_EXISTS ) ) {
453 // If {{REVISIONID}} resolved to '', it now needs to resolve to '-'.
454 // Note that edit stashing always uses '-', which can be used for both
455 // edit filter checks and canonical parser cache.
456 $logger->info( "$varyMsg (vary-revision-exists)", $context );
457 return true;
458 } elseif (
459 $parserOutput->getOutputFlag( ParserOutputFlags::VARY_REVISION_SHA1 ) &&
460 $parserOutput->getRevisionUsedSha1Base36() !== $this->revision->getSha1()
461 ) {
462 // If a self-transclusion used the proposed page text, it must match the final
463 // page content after PST transformations and automatically merged edit conflicts
464 $logger->info( "$varyMsg (vary-revision-sha1 with wrong SHA-1)", $context );
465 return true;
466 }
467
468 // NOTE: In the original fix for T135261, the output was discarded if ParserOutputFlags::VARY_USER was
469 // set for a null-edit. The reason was that the original rendering in that case was
470 // targeting the user making the null-edit, not the user who made the original edit,
471 // causing {{REVISIONUSER}} to return the wrong name.
472 // This case is now expected to be handled by the code in RevisionRenderer that
473 // constructs the ParserOptions: For a null-edit, setCurrentRevisionRecordCallback is
474 // called with the old, existing revision.
475 $logger->debug( __METHOD__ . ": reusing prepared output for '{title}'", $context );
476 return false;
477 }
478}
setCacheRevisionId( $id)
RenderedRevision represents the rendered representation of a revision.
__construct(RevisionRecord $revision, ParserOptions $options, ContentRenderer $contentRenderer, callable $combineOutput, $audience=RevisionRecord::FOR_PUBLIC, Authority $performer=null)
setRevisionParserOutput(ParserOutput $output)
Sets a ParserOutput to be returned by getRevisionParserOutput().
updateRevision(RevisionRecord $rev)
Updates the RevisionRecord after the revision has been saved.
setSaveParseLogger(LoggerInterface $saveParseLogger)
getSlotParserOutput( $role, array $hints=[])
Page revision base class.
getSlots()
Returns the slots defined for this revision.
getId( $wikiId=self::LOCAL)
Get revision ID.
Exception raised in response to an audience check when attempting to access suppressed information wi...
Set options of the Parser.
getOutputFlag(string $name)
Provides a uniform interface to various boolean flags stored in the ParserOutput.
setTimestamp( $timestamp)
Base interface for content objects.
Definition Content.php:35
Interface for objects (potentially) representing a page that can be viewable and linked to on a wiki.
This interface represents the authority associated the current execution context, such as a web reque...
Definition Authority.php:37
A lazy provider of ParserOutput objects for a revision's individual slots.
$content
Definition router.php:76