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