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;
28 use ParserOptions;
29 use ParserOutput;
30 use Psr\Log\LoggerInterface;
31 use Psr\Log\NullLogger;
32 use Title;
33 use User;
34 use Wikimedia\Assert\Assert;
35 
44 
48  private $title;
49 
51  private $revision;
52 
56  private $options;
57 
62 
66  private $forUser = 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  User $forUser = 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->forUser = $forUser;
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->forUser );
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  return $content->getParserOutput(
263  $this->title,
264  $this->revision->getId(),
265  $this->options,
266  $withHtml
267  );
268  }
269 
279  public function updateRevision( RevisionRecord $rev ) {
280  if ( $rev->getId() === $this->revision->getId() ) {
281  return;
282  }
283 
284  if ( $this->revision->getId() ) {
285  throw new LogicException( 'RenderedRevision already has a revision with ID '
286  . $this->revision->getId() . ', can\'t update to revision with ID ' . $rev->getId() );
287  }
288 
289  if ( !$this->revision->getSlots()->hasSameContent( $rev->getSlots() ) ) {
290  throw new LogicException( 'Cannot update to a revision with different content!' );
291  }
292 
293  $this->setRevisionInternal( $rev );
294 
296  $this->revision->getPageId(),
297  $this->revision->getId(),
298  $this->revision->getTimestamp()
299  );
300  }
301 
316  $actualPageId,
317  $actualRevId,
318  $actualRevTimestamp
319  ) {
320  if ( $this->revisionOutput ) {
321  if ( $this->outputVariesOnRevisionMetaData(
322  $this->revisionOutput,
323  $actualPageId,
324  $actualRevId,
325  $actualRevTimestamp
326  ) ) {
327  $this->revisionOutput = null;
328  }
329  } else {
330  $this->saveParseLogger->debug( __METHOD__ . ": no prepared revision output" );
331  }
332 
333  foreach ( $this->slotsOutput as $role => $output ) {
334  if ( $this->outputVariesOnRevisionMetaData(
335  $output,
336  $actualPageId,
337  $actualRevId,
338  $actualRevTimestamp
339  ) ) {
340  unset( $this->slotsOutput[$role] );
341  }
342  }
343  }
344 
349  $this->revision = $revision;
350 
351  // Force the parser to use $this->revision to resolve magic words like {{REVISIONUSER}}
352  // if the revision is either known to be complete, or it doesn't have a revision ID set.
353  // If it's incomplete and we have a revision ID, the parser can do better by loading
354  // the revision from the database if needed to handle a magic word.
355  //
356  // The following considerations inform the logic described above:
357  //
358  // 1) If we have a saved revision already loaded, we want the parser to use it, instead of
359  // loading it again.
360  //
361  // 2) If the revision is a fake that wraps some kind of synthetic content, such as an
362  // error message from Article, it should be used directly and things like {{REVISIONUSER}}
363  // should not expected to work, since there may not even be an actual revision to
364  // refer to.
365  //
366  // 3) If the revision is a fake constructed around a Title, a Content object, and
367  // a revision ID, to provide backwards compatibility to code that has access to those
368  // but not to a complete RevisionRecord for rendering, then we want the Parser to
369  // load the actual revision from the database when it encounters a magic word like
370  // {{REVISIONUSER}}, but we don't want to load that revision ahead of time just in case.
371  //
372  // 4) Previewing an edit to a template should use the submitted unsaved
373  // MutableRevisionRecord for self-transclusions in the template's documentation (see T7278).
374  // That revision would be complete except for the ID field.
375  //
376  // 5) Pre-save transform would provide a RevisionRecord that has all meta-data but is
377  // incomplete due to not yet having content set. However, since it doesn't have a revision
378  // ID either, the below code would still force it to be used, allowing
379  // {{subst::REVISIONUSER}} to function as expected.
380 
381  if ( $this->revision->isReadyForInsertion() || !$this->revision->getId() ) {
383  $oldCallback = $this->options->getCurrentRevisionRecordCallback();
384  $this->options->setCurrentRevisionRecordCallback(
385  function ( Title $parserTitle, $parser = null ) use ( $title, $oldCallback ) {
386  if ( $title->equals( $parserTitle ) ) {
387  return $this->revision;
388  } else {
389  return call_user_func( $oldCallback, $parserTitle, $parser );
390  }
391  }
392  );
393  }
394  }
395 
410  ParserOutput $out,
411  $actualPageId,
412  $actualRevId,
413  $actualRevTimestamp
414  ) {
415  $logger = $this->saveParseLogger;
416  $varyMsg = __METHOD__ . ": cannot use prepared output for '{title}'";
417  $context = [ 'title' => $this->title->getPrefixedText() ];
418 
419  if ( $out->getFlag( 'vary-revision' ) ) {
420  // If {{PAGEID}} resolved to 0, then that word need to resolve to the actual page ID
421  $logger->info( "$varyMsg (vary-revision)", $context );
422  return true;
423  } elseif (
424  $out->getFlag( 'vary-revision-id' )
425  && $actualRevId !== false
426  && ( $actualRevId === true || $out->getSpeculativeRevIdUsed() !== $actualRevId )
427  ) {
428  $logger->info( "$varyMsg (vary-revision-id and wrong ID)", $context );
429  return true;
430  } elseif (
431  $out->getFlag( 'vary-revision-timestamp' )
432  && $actualRevTimestamp !== false
433  && ( $actualRevTimestamp === true ||
434  $out->getRevisionTimestampUsed() !== $actualRevTimestamp )
435  ) {
436  $logger->info( "$varyMsg (vary-revision-timestamp and wrong timestamp)", $context );
437  return true;
438  } elseif (
439  $out->getFlag( 'vary-page-id' )
440  && $actualPageId !== false
441  && ( $actualPageId === true || $out->getSpeculativePageIdUsed() !== $actualPageId )
442  ) {
443  $logger->info( "$varyMsg (vary-page-id and wrong ID)", $context );
444  return true;
445  } elseif ( $out->getFlag( 'vary-revision-exists' ) ) {
446  // If {{REVISIONID}} resolved to '', it now needs to resolve to '-'.
447  // Note that edit stashing always uses '-', which can be used for both
448  // edit filter checks and canonical parser cache.
449  $logger->info( "$varyMsg (vary-revision-exists)", $context );
450  return true;
451  } elseif (
452  $out->getFlag( 'vary-revision-sha1' ) &&
453  $out->getRevisionUsedSha1Base36() !== $this->revision->getSha1()
454  ) {
455  // If a self-transclusion used the proposed page text, it must match the final
456  // page content after PST transformations and automatically merged edit conflicts
457  $logger->info( "$varyMsg (vary-revision-sha1 with wrong SHA-1)", $context );
458  return true;
459  }
460 
461  // NOTE: In the original fix for T135261, the output was discarded if 'vary-user' was
462  // set for a null-edit. The reason was that the original rendering in that case was
463  // targeting the user making the null-edit, not the user who made the original edit,
464  // causing {{REVISIONUSER}} to return the wrong name.
465  // This case is now expected to be handled by the code in RevisionRenderer that
466  // constructs the ParserOptions: For a null-edit, setCurrentRevisionRecordCallback is
467  // called with the old, existing revision.
468  $logger->debug( __METHOD__ . ": reusing prepared output for '{title}'", $context );
469  return false;
470  }
471 }
ParserOptions
Set options of the Parser.
Definition: ParserOptions.php:44
Revision\RenderedRevision\getRevision
getRevision()
Definition: RenderedRevision.php:150
Revision\RenderedRevision\$revision
RevisionRecord $revision
Definition: RenderedRevision.php:51
Revision\RevisionRecord
Page revision base class.
Definition: RevisionRecord.php:46
ParserOutput
Definition: ParserOutput.php:25
Revision\RenderedRevision\$title
Title $title
Definition: RenderedRevision.php:48
Revision\RenderedRevision\getRevisionParserOutput
getRevisionParserOutput(array $hints=[])
Definition: RenderedRevision.php:191
Revision\SuppressedDataException
Exception raised in response to an audience check when attempting to access suppressed information wi...
Definition: SuppressedDataException.php:33
Revision\RenderedRevision\$options
ParserOptions $options
Definition: RenderedRevision.php:56
Revision\RenderedRevision\getSlotParserOutputUncached
getSlotParserOutputUncached(Content $content, $withHtml)
Definition: RenderedRevision.php:261
Revision\RenderedRevision\setSaveParseLogger
setSaveParseLogger(LoggerInterface $saveParseLogger)
Definition: RenderedRevision.php:136
Revision\RenderedRevision\setRevisionInternal
setRevisionInternal(RevisionRecord $revision)
Definition: RenderedRevision.php:348
Title\equals
equals(LinkTarget $title)
Compare with another title.
Definition: Title.php:3974
Revision\SlotRenderingProvider
A lazy provider of ParserOutput objects for a revision's individual slots.
Definition: SlotRenderingProvider.php:12
MediaWiki\Revision
Definition: ContributionsLookup.php:3
Revision\RenderedRevision\$saveParseLogger
LoggerInterface $saveParseLogger
For profiling ParserOutput re-use.
Definition: RenderedRevision.php:89
ParserOutput\getFlag
getFlag( $flag)
Definition: ParserOutput.php:1053
Revision\RenderedRevision\getSlotParserOutput
getSlotParserOutput( $role, array $hints=[])
Definition: RenderedRevision.php:221
Revision\RenderedRevision\$revisionOutput
ParserOutput null $revisionOutput
The combined ParserOutput for the revision, initialized lazily by getRevisionParserOutput().
Definition: RenderedRevision.php:72
ParserOutput\getSpeculativePageIdUsed
getSpeculativePageIdUsed()
Definition: ParserOutput.php:500
Revision\RevisionRecord\getId
getId()
Get revision ID.
Definition: RevisionRecord.php:279
Revision\RenderedRevision\$combineOutput
callable $combineOutput
Callback for combining slot output into revision output.
Definition: RenderedRevision.php:84
$content
$content
Definition: router.php:76
Revision\RenderedRevision\$audience
int $audience
Audience to check when accessing content.
Definition: RenderedRevision.php:61
ParserOutput\getRevisionUsedSha1Base36
getRevisionUsedSha1Base36()
Definition: ParserOutput.php:543
Revision\SlotRecord\MAIN
const MAIN
Definition: SlotRecord.php:41
Revision\RevisionRecord\getSlots
getSlots()
Returns the slots defined for this revision.
Definition: RevisionRecord.php:233
Content
Base interface for content objects.
Definition: Content.php:35
Revision\RevisionRecord\FOR_PUBLIC
const FOR_PUBLIC
Definition: RevisionRecord.php:58
Revision\RenderedRevision\$forUser
User null $forUser
The user to use for audience checks during content access.
Definition: RenderedRevision.php:66
Revision\RenderedRevision\$slotsOutput
ParserOutput[] $slotsOutput
The ParserOutput for each slot, initialized lazily by getSlotParserOutput().
Definition: RenderedRevision.php:78
ParserOutput\getRevisionTimestampUsed
getRevisionTimestampUsed()
Definition: ParserOutput.php:516
Title
Represents a title within MediaWiki.
Definition: Title.php:42
Revision\RenderedRevision\__construct
__construct(Title $title, RevisionRecord $revision, ParserOptions $options, callable $combineOutput, $audience=RevisionRecord::FOR_PUBLIC, User $forUser=null)
Definition: RenderedRevision.php:107
Revision\RevisionRecord\DELETED_TEXT
const DELETED_TEXT
Definition: RevisionRecord.php:49
Revision\RenderedRevision\pruneRevisionSensitiveOutput
pruneRevisionSensitiveOutput( $actualPageId, $actualRevId, $actualRevTimestamp)
Prune any output that depends on the revision ID.
Definition: RenderedRevision.php:315
Revision\RenderedRevision
RenderedRevision represents the rendered representation of a revision.
Definition: RenderedRevision.php:43
Revision\RenderedRevision\isContentDeleted
isContentDeleted()
Definition: RenderedRevision.php:143
MediaWiki\$context
IContextSource $context
Definition: MediaWiki.php:40
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:56
Revision\RevisionRecord\FOR_THIS_USER
const FOR_THIS_USER
Definition: RevisionRecord.php:59
ParserOutput\getSpeculativeRevIdUsed
getSpeculativeRevIdUsed()
Definition: ParserOutput.php:484
Revision\RenderedRevision\setRevisionParserOutput
setRevisionParserOutput(ParserOutput $output)
Sets a ParserOutput to be returned by getRevisionParserOutput().
Definition: RenderedRevision.php:171
Revision\RenderedRevision\getOptions
getOptions()
Definition: RenderedRevision.php:157
Revision\RenderedRevision\outputVariesOnRevisionMetaData
outputVariesOnRevisionMetaData(ParserOutput $out, $actualPageId, $actualRevId, $actualRevTimestamp)
Definition: RenderedRevision.php:409
Revision\RenderedRevision\updateRevision
updateRevision(RevisionRecord $rev)
Updates the RevisionRecord after the revision has been saved.
Definition: RenderedRevision.php:279