MediaWiki REL1_37
RenderedRevision.php
Go to the documentation of this file.
1<?php
23namespace MediaWiki\Revision;
24
25use Content;
26use InvalidArgumentException;
27use LogicException;
30use ParserOutput;
31use Psr\Log\LoggerInterface;
32use Psr\Log\NullLogger;
33use Title;
34use Wikimedia\Assert\Assert;
35
44
48 private $title;
49
51 private $revision;
52
56 private $options;
57
62
66 private $performer = null;
67
72 private $revisionOutput = null;
73
78 private $slotsOutput = [];
79
85
90
107 public function __construct(
111 callable $combineOutput,
113 Authority $performer = null
114 ) {
115 $this->title = $title;
116 $this->options = $options;
117
118 $this->setRevisionInternal( $revision );
119
120 $this->combineOutput = $combineOutput;
121 $this->saveParseLogger = new NullLogger();
122
124 throw new InvalidArgumentException(
125 'User must be specified when setting audience to FOR_THIS_USER'
126 );
127 }
128
129 $this->audience = $audience;
130 $this->performer = $performer;
131 }
132
136 public function setSaveParseLogger( LoggerInterface $saveParseLogger ) {
137 $this->saveParseLogger = $saveParseLogger;
138 }
139
143 public function isContentDeleted() {
144 return $this->revision->isDeleted( RevisionRecord::DELETED_TEXT );
145 }
146
150 public function getRevision() {
151 return $this->revision;
152 }
153
157 public function getOptions() {
158 return $this->options;
159 }
160
171 public function setRevisionParserOutput( ParserOutput $output ) {
172 $this->revisionOutput = $output;
173
174 // If there is only one slot, we assume that the combined output is identical
175 // with the main slot's output. This is intended to prevent a redundant re-parse of
176 // the content in case getSlotParserOutput( SlotRecord::MAIN ) is called, for instance
177 // from ContentHandler::getSecondaryDataUpdates.
178 if ( $this->revision->getSlotRoles() === [ SlotRecord::MAIN ] ) {
179 $this->slotsOutput[ SlotRecord::MAIN ] = $output;
180 }
181 }
182
191 public function getRevisionParserOutput( array $hints = [] ) {
192 $withHtml = $hints['generate-html'] ?? true;
193
194 if ( !$this->revisionOutput
195 || ( $withHtml && !$this->revisionOutput->hasText() )
196 ) {
197 $output = call_user_func( $this->combineOutput, $this, $hints );
198
199 Assert::postcondition(
200 $output instanceof ParserOutput,
201 'Callback did not return a ParserOutput object!'
202 );
203
204 $this->revisionOutput = $output;
205 }
206
208 }
209
221 public function getSlotParserOutput( $role, array $hints = [] ) {
222 $withHtml = $hints['generate-html'] ?? true;
223
224 if ( !isset( $this->slotsOutput[ $role ] )
225 || ( $withHtml && !$this->slotsOutput[ $role ]->hasText() )
226 ) {
227 $content = $this->revision->getContent( $role, $this->audience, $this->performer );
228
229 if ( !$content ) {
230 throw new SuppressedDataException(
231 'Access to the content has been suppressed for this audience'
232 );
233 } else {
234 // XXX: allow SlotRoleHandler to control the ParserOutput?
235 $output = $this->getSlotParserOutputUncached( $content, $withHtml );
236
237 if ( $withHtml && !$output->hasText() ) {
238 throw new LogicException(
239 'HTML generation was requested, but '
240 . get_class( $content )
241 . '::getParserOutput() returns a ParserOutput with no text set.'
242 );
243 }
244
245 // Detach watcher, to ensure option use is not recorded in the wrong ParserOutput.
246 $this->options->registerWatcher( null );
247 }
248
249 $this->slotsOutput[ $role ] = $output;
250 }
251
252 return $this->slotsOutput[$role];
253 }
254
261 private function getSlotParserOutputUncached( Content $content, $withHtml ) {
262 $parserOutput = $content->getParserOutput(
263 $this->title,
264 $this->revision->getId(),
265 $this->options,
266 $withHtml
267 );
268 // Save the rev_id and timestamp so that we don't have to load the revision row on view
269 $parserOutput->setCacheRevisionId( $this->revision->getId() );
270 $parserOutput->setTimestamp( $this->revision->getTimestamp() );
271 return $parserOutput;
272 }
273
283 public function updateRevision( RevisionRecord $rev ) {
284 if ( $rev->getId() === $this->revision->getId() ) {
285 return;
286 }
287
288 if ( $this->revision->getId() ) {
289 throw new LogicException( 'RenderedRevision already has a revision with ID '
290 . $this->revision->getId() . ', can\'t update to revision with ID ' . $rev->getId() );
291 }
292
293 if ( !$this->revision->getSlots()->hasSameContent( $rev->getSlots() ) ) {
294 throw new LogicException( 'Cannot update to a revision with different content!' );
295 }
296
297 $this->setRevisionInternal( $rev );
298
300 $this->revision->getPageId(),
301 $this->revision->getId(),
302 $this->revision->getTimestamp()
303 );
304 }
305
320 $actualPageId,
321 $actualRevId,
322 $actualRevTimestamp
323 ) {
324 if ( $this->revisionOutput ) {
326 $this->revisionOutput,
327 $actualPageId,
328 $actualRevId,
329 $actualRevTimestamp
330 ) ) {
331 $this->revisionOutput = null;
332 }
333 } else {
334 $this->saveParseLogger->debug( __METHOD__ . ": no prepared revision output" );
335 }
336
337 foreach ( $this->slotsOutput as $role => $output ) {
339 $output,
340 $actualPageId,
341 $actualRevId,
342 $actualRevTimestamp
343 ) ) {
344 unset( $this->slotsOutput[$role] );
345 }
346 }
347 }
348
353 $this->revision = $revision;
354
355 // Force the parser to use $this->revision to resolve magic words like {{REVISIONUSER}}
356 // if the revision is either known to be complete, or it doesn't have a revision ID set.
357 // If it's incomplete and we have a revision ID, the parser can do better by loading
358 // the revision from the database if needed to handle a magic word.
359 //
360 // The following considerations inform the logic described above:
361 //
362 // 1) If we have a saved revision already loaded, we want the parser to use it, instead of
363 // loading it again.
364 //
365 // 2) If the revision is a fake that wraps some kind of synthetic content, such as an
366 // error message from Article, it should be used directly and things like {{REVISIONUSER}}
367 // should not expected to work, since there may not even be an actual revision to
368 // refer to.
369 //
370 // 3) If the revision is a fake constructed around a Title, a Content object, and
371 // a revision ID, to provide backwards compatibility to code that has access to those
372 // but not to a complete RevisionRecord for rendering, then we want the Parser to
373 // load the actual revision from the database when it encounters a magic word like
374 // {{REVISIONUSER}}, but we don't want to load that revision ahead of time just in case.
375 //
376 // 4) Previewing an edit to a template should use the submitted unsaved
377 // MutableRevisionRecord for self-transclusions in the template's documentation (see T7278).
378 // That revision would be complete except for the ID field.
379 //
380 // 5) Pre-save transform would provide a RevisionRecord that has all meta-data but is
381 // incomplete due to not yet having content set. However, since it doesn't have a revision
382 // ID either, the below code would still force it to be used, allowing
383 // {{subst::REVISIONUSER}} to function as expected.
384
385 if ( $this->revision->isReadyForInsertion() || !$this->revision->getId() ) {
387 $oldCallback = $this->options->getCurrentRevisionRecordCallback();
388 $this->options->setCurrentRevisionRecordCallback(
389 function ( Title $parserTitle, $parser = null ) use ( $title, $oldCallback ) {
390 if ( $title->equals( $parserTitle ) ) {
391 return $this->revision;
392 } else {
393 return call_user_func( $oldCallback, $parserTitle, $parser );
394 }
395 }
396 );
397 }
398 }
399
414 ParserOutput $out,
415 $actualPageId,
416 $actualRevId,
417 $actualRevTimestamp
418 ) {
419 $logger = $this->saveParseLogger;
420 $varyMsg = __METHOD__ . ": cannot use prepared output for '{title}'";
421 $context = [ 'title' => $this->title->getPrefixedText() ];
422
423 if ( $out->getFlag( 'vary-revision' ) ) {
424 // If {{PAGEID}} resolved to 0, then that word need to resolve to the actual page ID
425 $logger->info( "$varyMsg (vary-revision)", $context );
426 return true;
427 } elseif (
428 $out->getFlag( 'vary-revision-id' )
429 && $actualRevId !== false
430 && ( $actualRevId === true || $out->getSpeculativeRevIdUsed() !== $actualRevId )
431 ) {
432 $logger->info( "$varyMsg (vary-revision-id and wrong ID)", $context );
433 return true;
434 } elseif (
435 $out->getFlag( 'vary-revision-timestamp' )
436 && $actualRevTimestamp !== false
437 && ( $actualRevTimestamp === true ||
438 $out->getRevisionTimestampUsed() !== $actualRevTimestamp )
439 ) {
440 $logger->info( "$varyMsg (vary-revision-timestamp and wrong timestamp)", $context );
441 return true;
442 } elseif (
443 $out->getFlag( 'vary-page-id' )
444 && $actualPageId !== false
445 && ( $actualPageId === true || $out->getSpeculativePageIdUsed() !== $actualPageId )
446 ) {
447 $logger->info( "$varyMsg (vary-page-id and wrong ID)", $context );
448 return true;
449 } elseif ( $out->getFlag( 'vary-revision-exists' ) ) {
450 // If {{REVISIONID}} resolved to '', it now needs to resolve to '-'.
451 // Note that edit stashing always uses '-', which can be used for both
452 // edit filter checks and canonical parser cache.
453 $logger->info( "$varyMsg (vary-revision-exists)", $context );
454 return true;
455 } elseif (
456 $out->getFlag( 'vary-revision-sha1' ) &&
457 $out->getRevisionUsedSha1Base36() !== $this->revision->getSha1()
458 ) {
459 // If a self-transclusion used the proposed page text, it must match the final
460 // page content after PST transformations and automatically merged edit conflicts
461 $logger->info( "$varyMsg (vary-revision-sha1 with wrong SHA-1)", $context );
462 return true;
463 }
464
465 // NOTE: In the original fix for T135261, the output was discarded if 'vary-user' was
466 // set for a null-edit. The reason was that the original rendering in that case was
467 // targeting the user making the null-edit, not the user who made the original edit,
468 // causing {{REVISIONUSER}} to return the wrong name.
469 // This case is now expected to be handled by the code in RevisionRenderer that
470 // constructs the ParserOptions: For a null-edit, setCurrentRevisionRecordCallback is
471 // called with the old, existing revision.
472 $logger->debug( __METHOD__ . ": reusing prepared output for '{title}'", $context );
473 return false;
474 }
475}
RenderedRevision represents the rendered representation of a revision.
LoggerInterface $saveParseLogger
For profiling ParserOutput re-use.
setRevisionParserOutput(ParserOutput $output)
Sets a ParserOutput to be returned by getRevisionParserOutput().
ParserOutput null $revisionOutput
The combined ParserOutput for the revision, initialized lazily by getRevisionParserOutput().
pruneRevisionSensitiveOutput( $actualPageId, $actualRevId, $actualRevTimestamp)
Prune any output that depends on the revision ID.
outputVariesOnRevisionMetaData(ParserOutput $out, $actualPageId, $actualRevId, $actualRevTimestamp)
updateRevision(RevisionRecord $rev)
Updates the RevisionRecord after the revision has been saved.
ParserOutput[] $slotsOutput
The ParserOutput for each slot, initialized lazily by getSlotParserOutput().
__construct(Title $title, RevisionRecord $revision, ParserOptions $options, callable $combineOutput, $audience=RevisionRecord::FOR_PUBLIC, Authority $performer=null)
setRevisionInternal(RevisionRecord $revision)
setSaveParseLogger(LoggerInterface $saveParseLogger)
int $audience
Audience to check when accessing content.
callable $combineOutput
Callback for combining slot output into revision output.
getSlotParserOutputUncached(Content $content, $withHtml)
getSlotParserOutput( $role, array $hints=[])
Authority null $performer
The user to use for audience checks during content access.
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.
Represents a title within MediaWiki.
Definition Title.php:48
equals(object $other)
Compares with another Title.
Definition Title.php:3499
Base interface for content objects.
Definition Content.php:35
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