MediaWiki REL1_34
RenderedRevision.php
Go to the documentation of this file.
1<?php
23namespace MediaWiki\Revision;
24
25use InvalidArgumentException;
26use LogicException;
28use ParserOutput;
29use Psr\Log\LoggerInterface;
30use Psr\Log\NullLogger;
31use Revision;
32use Title;
33use User;
34use Content;
35use Wikimedia\Assert\Assert;
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
86
91
108 public function __construct(
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
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(),
266 $this->options,
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 ) {
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 ) {
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}
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:4113
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:51
Base interface for content objects.
Definition Content.php:34
A lazy provider of ParserOutput objects for a revision's individual slots.
$context
Definition load.php:45
$content
Definition router.php:78