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;
32 use ParserOptions;
33 use ParserOutput;
34 use Psr\Log\LoggerInterface;
35 use Psr\Log\NullLogger;
36 use Wikimedia\Assert\Assert;
37 
46 
48  private $revision;
49 
53  private $options;
54 
59 
63  private $performer = null;
64 
69  private $revisionOutput = null;
70 
75  private $slotsOutput = [];
76 
81  private $combineOutput;
82 
87 
92 
109  public function __construct(
113  callable $combineOutput,
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 
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 
304  $this->revision->getPageId(),
305  $this->revision->getId(),
306  $this->revision->getTimestamp()
307  );
308  }
309 
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 
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 
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 }
RenderedRevision represents the rendered representation of a revision.
__construct(RevisionRecord $revision, ParserOptions $options, ContentRenderer $contentRenderer, callable $combineOutput, $audience=RevisionRecord::FOR_PUBLIC, Authority $performer=null)
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.
updateRevision(RevisionRecord $rev)
Updates the RevisionRecord after the revision has been saved.
ParserOutput[] $slotsOutput
The ParserOutput for each slot, initialized lazily by getSlotParserOutput().
ContentRenderer $contentRenderer
Service to render content.
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.
outputVariesOnRevisionMetaData(ParserOutput $parserOutput, $actualPageId, $actualRevId, $actualRevTimestamp)
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...
IContextSource $context
Definition: MediaWiki.php:42
Set options of the Parser.
getRevisionTimestampUsed()
getRevisionUsedSha1Base36()
getSpeculativePageIdUsed()
getOutputFlag(string $name)
Provides a uniform interface to various boolean flags stored in the ParserOutput.
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