MediaWiki REL1_35
RenderedRevision.php
Go to the documentation of this file.
1<?php
23namespace MediaWiki\Revision;
24
25use Content;
26use InvalidArgumentException;
27use LogicException;
29use ParserOutput;
30use Psr\Log\LoggerInterface;
31use Psr\Log\NullLogger;
32use Title;
33use User;
34use 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
85
90
107 public function __construct(
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
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 ) {
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 ) {
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}
RenderedRevision represents the rendered representation of a revision.
LoggerInterface $saveParseLogger
For profiling ParserOutput re-use.
User null $forUser
The user to use for audience checks during content access.
__construct(Title $title, RevisionRecord $revision, ParserOptions $options, callable $combineOutput, $audience=RevisionRecord::FOR_PUBLIC, User $forUser=null)
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().
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=[])
Page revision base class.
getSlots()
Returns the slots defined for this revision.
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:42
equals(LinkTarget $title)
Compare with another title.
Definition Title.php:3983
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:60
Base interface for content objects.
Definition Content.php:35
A lazy provider of ParserOutput objects for a revision's individual slots.
$content
Definition router.php:76