MediaWiki  master
RenderedRevision.php
Go to the documentation of this file.
1 <?php
23 namespace MediaWiki\Revision;
24 
25 use Content;
26 use InvalidArgumentException;
27 use LogicException;
29 use ParserOptions;
30 use ParserOutput;
31 use Psr\Log\LoggerInterface;
32 use Psr\Log\NullLogger;
33 use Title;
34 use 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 
84  private $combineOutput;
85 
90 
107  public function __construct(
108  Title $title,
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 
207  return $this->revisionOutput;
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 ) {
325  if ( $this->outputVariesOnRevisionMetaData(
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 ) {
338  if ( $this->outputVariesOnRevisionMetaData(
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 }
MediaWiki\Revision\RenderedRevision\getOptions
getOptions()
Definition: RenderedRevision.php:157
ParserOptions
Set options of the Parser.
Definition: ParserOptions.php:45
MediaWiki\Revision\RevisionRecord
Page revision base class.
Definition: RevisionRecord.php:47
ParserOutput
Definition: ParserOutput.php:31
MediaWiki\Revision\RenderedRevision\$options
ParserOptions $options
Definition: RenderedRevision.php:56
MediaWiki\Revision\RenderedRevision\$performer
Authority null $performer
The user to use for audience checks during content access.
Definition: RenderedRevision.php:66
MediaWiki\Revision\SuppressedDataException
Exception raised in response to an audience check when attempting to access suppressed information wi...
Definition: SuppressedDataException.php:33
MediaWiki\Revision\RenderedRevision\$revision
RevisionRecord $revision
Definition: RenderedRevision.php:51
MediaWiki\Revision\RenderedRevision\updateRevision
updateRevision(RevisionRecord $rev)
Updates the RevisionRecord after the revision has been saved.
Definition: RenderedRevision.php:283
MediaWiki\Revision\RenderedRevision\__construct
__construct(Title $title, RevisionRecord $revision, ParserOptions $options, callable $combineOutput, $audience=RevisionRecord::FOR_PUBLIC, Authority $performer=null)
Definition: RenderedRevision.php:107
MediaWiki\Revision\RenderedRevision\$audience
int $audience
Audience to check when accessing content.
Definition: RenderedRevision.php:61
MediaWiki\Revision\RenderedRevision\$saveParseLogger
LoggerInterface $saveParseLogger
For profiling ParserOutput re-use.
Definition: RenderedRevision.php:89
MediaWiki\Revision\SlotRenderingProvider
A lazy provider of ParserOutput objects for a revision's individual slots.
Definition: SlotRenderingProvider.php:12
MediaWiki\Revision
Definition: ContributionsLookup.php:3
MediaWiki\Revision\RevisionRecord\DELETED_TEXT
const DELETED_TEXT
Definition: RevisionRecord.php:53
MediaWiki\Revision\RenderedRevision\setRevisionInternal
setRevisionInternal(RevisionRecord $revision)
Definition: RenderedRevision.php:352
MediaWiki\Revision\SlotRecord\MAIN
const MAIN
Definition: SlotRecord.php:43
MediaWiki\Revision\RenderedRevision\pruneRevisionSensitiveOutput
pruneRevisionSensitiveOutput( $actualPageId, $actualRevId, $actualRevTimestamp)
Prune any output that depends on the revision ID.
Definition: RenderedRevision.php:319
MediaWiki\Revision\RenderedRevision\$combineOutput
callable $combineOutput
Callback for combining slot output into revision output.
Definition: RenderedRevision.php:84
ParserOutput\getFlag
getFlag( $flag)
Definition: ParserOutput.php:1076
MediaWiki\Revision\RenderedRevision\getSlotParserOutputUncached
getSlotParserOutputUncached(Content $content, $withHtml)
Definition: RenderedRevision.php:261
MediaWiki\Revision\RenderedRevision\$slotsOutput
ParserOutput[] $slotsOutput
The ParserOutput for each slot, initialized lazily by getSlotParserOutput().
Definition: RenderedRevision.php:78
ParserOutput\getSpeculativePageIdUsed
getSpeculativePageIdUsed()
Definition: ParserOutput.php:523
MediaWiki\Revision\RenderedRevision\getRevision
getRevision()
Definition: RenderedRevision.php:150
MediaWiki\Revision\RenderedRevision\setRevisionParserOutput
setRevisionParserOutput(ParserOutput $output)
Sets a ParserOutput to be returned by getRevisionParserOutput().
Definition: RenderedRevision.php:171
MediaWiki\Revision\RevisionRecord\getSlots
getSlots()
Returns the slots defined for this revision.
Definition: RevisionRecord.php:222
MediaWiki\Permissions\Authority
This interface represents the authority associated the current execution context, such as a web reque...
Definition: Authority.php:37
$content
$content
Definition: router.php:76
MediaWiki\Revision\RenderedRevision\isContentDeleted
isContentDeleted()
Definition: RenderedRevision.php:143
MediaWiki\Revision\RenderedRevision\$revisionOutput
ParserOutput null $revisionOutput
The combined ParserOutput for the revision, initialized lazily by getRevisionParserOutput().
Definition: RenderedRevision.php:72
MediaWiki\Revision\RenderedRevision\getRevisionParserOutput
getRevisionParserOutput(array $hints=[])
Definition: RenderedRevision.php:191
MediaWiki\Revision\RenderedRevision\$title
Title $title
Definition: RenderedRevision.php:48
MediaWiki\Revision\RenderedRevision\getSlotParserOutput
getSlotParserOutput( $role, array $hints=[])
Definition: RenderedRevision.php:221
ParserOutput\getRevisionUsedSha1Base36
getRevisionUsedSha1Base36()
Definition: ParserOutput.php:566
MediaWiki\Revision\RenderedRevision\setSaveParseLogger
setSaveParseLogger(LoggerInterface $saveParseLogger)
Definition: RenderedRevision.php:136
Content
Base interface for content objects.
Definition: Content.php:35
Title\equals
equals(object $other)
Compares with another Title.
Definition: Title.php:3490
ParserOutput\getRevisionTimestampUsed
getRevisionTimestampUsed()
Definition: ParserOutput.php:539
Title
Represents a title within MediaWiki.
Definition: Title.php:48
MediaWiki\Revision\RevisionRecord\getId
getId( $wikiId=self::LOCAL)
Get revision ID.
Definition: RevisionRecord.php:279
MediaWiki\Revision\RevisionRecord\FOR_THIS_USER
const FOR_THIS_USER
Definition: RevisionRecord.php:63
MediaWiki\Revision\RenderedRevision
RenderedRevision represents the rendered representation of a revision.
Definition: RenderedRevision.php:43
MediaWiki\$context
IContextSource $context
Definition: MediaWiki.php:40
MediaWiki\Revision\RenderedRevision\outputVariesOnRevisionMetaData
outputVariesOnRevisionMetaData(ParserOutput $out, $actualPageId, $actualRevId, $actualRevTimestamp)
Definition: RenderedRevision.php:413
MediaWiki\Revision\RevisionRecord\FOR_PUBLIC
const FOR_PUBLIC
Definition: RevisionRecord.php:62
ParserOutput\getSpeculativeRevIdUsed
getSpeculativeRevIdUsed()
Definition: ParserOutput.php:507