MediaWiki  master
ParserOutputAccess.php
Go to the documentation of this file.
1 <?php
2 
22 namespace MediaWiki\Page;
23 
25 use InvalidArgumentException;
26 use MediaWiki\Logger\Spi as LoggerSpi;
31 use ParserCache;
32 use ParserOptions;
33 use ParserOutput;
37 use Status;
38 use TitleFormatter;
40 
51 
55  public const OPT_NO_CHECK_CACHE = 1;
56 
58  public const OPT_FORCE_PARSE = self::OPT_NO_CHECK_CACHE;
59 
64  private const OPT_NO_UPDATE_CACHE = 2;
65 
71  public const OPT_NO_AUDIENCE_CHECK = 4;
72 
77  public const OPT_NO_CACHE = self::OPT_NO_UPDATE_CACHE | self::OPT_NO_CHECK_CACHE;
78 
80  private const CACHE_NONE = 'none';
81 
83  private const CACHE_PRIMARY = 'primary';
84 
86  private const CACHE_SECONDARY = 'secondary';
87 
89  private $primaryCache;
90 
94  private $secondaryCache;
95 
97  private $revisionLookup;
98 
101 
104 
106  private $lbFactory;
107 
109  private $loggerSpi;
110 
113 
116 
128  public function __construct(
130  RevisionOutputCache $secondaryCache,
131  RevisionLookup $revisionLookup,
132  RevisionRenderer $revisionRenderer,
134  ILBFactory $lbFactory,
135  LoggerSpi $loggerSpi,
138  ) {
139  $this->primaryCache = $primaryCache;
140  $this->secondaryCache = $secondaryCache;
141  $this->revisionLookup = $revisionLookup;
142  $this->revisionRenderer = $revisionRenderer;
143  $this->statsDataFactory = $statsDataFactory;
144  $this->lbFactory = $lbFactory;
145  $this->loggerSpi = $loggerSpi;
146  $this->wikiPageFactory = $wikiPageFactory;
147  $this->titleFormatter = $titleFormatter;
148  }
149 
159  private function shouldUseCache(
160  PageRecord $page,
161  ParserOptions $parserOptions,
162  ?RevisionRecord $rev
163  ) {
164  if ( $rev && !$rev->getId() ) {
165  // The revision isn't from the database, so the output can't safely be cached.
166  return self::CACHE_NONE;
167  }
168 
169  // NOTE: Keep in sync with ParserWikiPage::shouldCheckParserCache().
170  // NOTE: when we allow caching of old revisions in the future,
171  // we must not allow caching of deleted revisions.
172 
173  $wikiPage = $this->wikiPageFactory->newFromTitle( $page );
174  if ( !$page->exists() || !$wikiPage->getContentHandler()->isParserCacheSupported() ) {
175  return self::CACHE_NONE;
176  }
177 
178  if ( !$rev || $rev->getId() === $page->getLatest( PageRecord::LOCAL ) ) {
179  // current revision
180  return self::CACHE_PRIMARY;
181  }
182 
183  if ( !$rev->audienceCan( RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_PUBLIC ) ) {
184  // deleted/suppressed revision
185  return self::CACHE_NONE;
186  }
187 
188  return self::CACHE_SECONDARY;
189  }
190 
201  public function getCachedParserOutput(
202  PageRecord $page,
203  ParserOptions $parserOptions,
204  ?RevisionRecord $revision = null,
205  int $options = 0
206  ): ?ParserOutput {
207  $useCache = $this->shouldUseCache( $page, $parserOptions, $revision );
208 
209  if ( $useCache === self::CACHE_PRIMARY ) {
210  $output = $this->primaryCache->get( $page, $parserOptions );
211  } elseif ( $useCache === self::CACHE_SECONDARY && $revision ) {
212  $output = $this->secondaryCache->get( $revision, $parserOptions );
213  } else {
214  $output = null;
215  }
216 
217  $hitOrMiss = $output ? 'hit' : 'miss';
218  $this->statsDataFactory->increment( "ParserOutputAccess.Cache.$useCache.$hitOrMiss" );
219 
220  return $output ?: null; // convert false to null
221  }
222 
245  public function getParserOutput(
246  PageRecord $page,
247  ParserOptions $parserOptions,
248  ?RevisionRecord $revision = null,
249  int $options = 0
250  ): Status {
251  $error = $this->checkPreconditions( $page, $revision, $options );
252  if ( $error ) {
253  $this->statsDataFactory->increment( "ParserOutputAccess.Case.error" );
254  return $error;
255  }
256 
257  $currentOrOld = ( $revision && $revision->getId() !== $page->getLatest() ) ? 'old' : 'current';
258  $this->statsDataFactory->increment( "ParserOutputAccess.Case.$currentOrOld" );
259 
260  if ( !( $options & self::OPT_NO_CHECK_CACHE ) ) {
261  $output = $this->getCachedParserOutput( $page, $parserOptions, $revision );
262  if ( $output ) {
263  return Status::newGood( $output );
264  }
265  }
266 
267  if ( !$revision ) {
268  $revision = $page->getLatest() ?
269  $this->revisionLookup->getRevisionById( $page->getLatest() ) : null;
270 
271  if ( !$revision ) {
272  $this->statsDataFactory->increment( "ParserOutputAccess.Status.norev" );
273  return Status::newFatal(
274  'missing-revision',
275  $page->getLatest()
276  );
277  }
278  }
279 
280  $work = $this->newPoolWorkArticleView( $page, $parserOptions, $revision, $options );
281  $work->execute();
282  $output = $work->getParserOutput();
283 
284  $status = Status::newGood();
285  if ( $work->getError() ) {
286  $status->merge( $work->getError() );
287  }
288 
289  if ( !$output && $status->isOK() ) {
290  // TODO: PoolWorkArticle should properly report errors (T267610)
291  $status->fatal( 'pool-errorunknown' );
292  }
293 
294  if ( $output && $status->isOK() ) {
295  $status->setResult( true, $output );
296  }
297 
298  if ( $status->isOK() && $work->getIsDirty() ) {
299  $staleReason = $work->getIsFastStale()
300  ? 'view-pool-contention' : 'view-pool-overload';
301 
302  $status->warning( 'view-pool-dirty-output' );
303  $status->warning( $staleReason );
304  }
305 
306  if ( $status->isGood() ) {
307  $statusMsg = 'good';
308  } elseif ( $status->isOK() ) {
309  $statusMsg = 'ok';
310  } else {
311  $statusMsg = 'error';
312  }
313 
314  $this->statsDataFactory->increment( "ParserOutputAccess.Status.$statusMsg" );
315  return $status;
316  }
317 
325  private function checkPreconditions(
326  PageRecord $page,
327  ?RevisionRecord $revision = null,
328  int $options = 0
329  ): ?Status {
330  if ( !$page->exists() ) {
331  return Status::newFatal( 'nopagetext' );
332  }
333 
334  if ( !( $options & self::OPT_NO_UPDATE_CACHE ) && $revision && !$revision->getId() ) {
335  throw new InvalidArgumentException(
336  'The revision does not have a known ID. Use NO_CACHE.'
337  );
338  }
339 
340  if ( $revision && $revision->getPageId() !== $page->getId() ) {
341  throw new InvalidArgumentException(
342  'The revision does not belong to the given page.'
343  );
344  }
345 
346  if ( $revision && !( $options & self::OPT_NO_AUDIENCE_CHECK ) ) {
347  // NOTE: If per-user checks are desired, the caller should perform them and
348  // then set OPT_NO_AUDIENCE_CHECK if they passed.
349  if ( !$revision->audienceCan( RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_PUBLIC ) ) {
350  return Status::newFatal(
351  'missing-revision-permission',
352  $revision->getId(),
353  $revision->getTimestamp(),
354  $this->titleFormatter->getPrefixedDBkey( $page )
355  );
356  }
357  }
358 
359  return null;
360  }
361 
370  private function newPoolWorkArticleView(
371  PageRecord $page,
372  ParserOptions $parserOptions,
373  RevisionRecord $revision,
374  int $options
376  if ( $options & self::OPT_NO_UPDATE_CACHE ) {
377  $useCache = self::CACHE_NONE;
378  } else {
379  $useCache = $this->shouldUseCache( $page, $parserOptions, $revision );
380  }
381 
382  switch ( $useCache ) {
383  case self::CACHE_PRIMARY:
384  $this->statsDataFactory->increment( 'ParserOutputAccess.PoolWork.Current' );
385  $parserCacheMetadata = $this->primaryCache->getMetadata( $page );
386  $cacheKey = $this->primaryCache->makeParserOutputKey( $page, $parserOptions,
387  $parserCacheMetadata ? $parserCacheMetadata->getUsedOptions() : null
388  );
389 
390  $workKey = $cacheKey . ':revid:' . $revision->getId();
391 
392  return new PoolWorkArticleViewCurrent(
393  $workKey,
394  $page,
395  $revision,
396  $parserOptions,
397  $this->revisionRenderer,
398  $this->primaryCache,
399  $this->lbFactory,
400  $this->loggerSpi,
401  $this->wikiPageFactory
402  );
403 
404  case $useCache == self::CACHE_SECONDARY:
405  $this->statsDataFactory->increment( 'ParserOutputAccess.PoolWork.Old' );
406  $workKey = $this->secondaryCache->makeParserOutputKey( $revision, $parserOptions );
407  return new PoolWorkArticleViewOld(
408  $workKey,
409  $this->secondaryCache,
410  $revision,
411  $parserOptions,
412  $this->revisionRenderer,
413  $this->loggerSpi
414  );
415 
416  default:
417  $this->statsDataFactory->increment( 'ParserOutputAccess.PoolWork.Uncached' );
418  $workKey = $this->secondaryCache->makeParserOutputKey( $revision, $parserOptions );
419  return new PoolWorkArticleView(
420  $workKey,
421  $revision,
422  $parserOptions,
423  $this->revisionRenderer,
424  $this->loggerSpi
425  );
426  }
427 
428  // unreachable
429  }
430 
431 }
ParserOptions
Set options of the Parser.
Definition: ParserOptions.php:45
Page\ParserOutputAccess\$secondaryCache
RevisionOutputCache $secondaryCache
Definition: ParserOutputAccess.php:94
Page\PageRecord
Data record representing a page that is (or used to be, or could be) an editable page on a wiki.
Definition: PageRecord.php:25
PoolWorkArticleViewCurrent
PoolWorkArticleView for the current revision of a page, using ParserCache.
Definition: PoolWorkArticleViewCurrent.php:33
MediaWiki\Revision\RevisionRecord
Page revision base class.
Definition: RevisionRecord.php:47
ParserOutput
Definition: ParserOutput.php:31
StatusValue\newFatal
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:70
Page\ParserOutputAccess\getCachedParserOutput
getCachedParserOutput(PageRecord $page, ParserOptions $parserOptions, ?RevisionRecord $revision=null, int $options=0)
Returns the rendered output for the given page if it is present in the cache.
Definition: ParserOutputAccess.php:201
Page\PageRecord\getLatest
getLatest( $wikiId=self::LOCAL)
The ID of the page's latest revision.
Page\ParserOutputAccess\newPoolWorkArticleView
newPoolWorkArticleView(PageRecord $page, ParserOptions $parserOptions, RevisionRecord $revision, int $options)
Definition: ParserOutputAccess.php:370
Page\ParserOutputAccess
Service for getting rendered output of a given page.
Definition: ParserOutputAccess.php:50
PoolWorkArticleView
PoolCounter protected work wrapping RenderedRevision->getRevisionParserOutput.
Definition: PoolWorkArticleView.php:34
Page\ParserOutputAccess\$loggerSpi
LoggerSpi $loggerSpi
Definition: ParserOutputAccess.php:109
Page\ParserOutputAccess\$lbFactory
ILBFactory $lbFactory
Definition: ParserOutputAccess.php:106
PoolWorkArticleViewOld
PoolWorkArticleView for an old revision of a page, using a simple cache.
Definition: PoolWorkArticleViewOld.php:30
Page\ParserOutputAccess\$statsDataFactory
IBufferingStatsdDataFactory $statsDataFactory
Definition: ParserOutputAccess.php:103
Page\ParserOutputAccess\$wikiPageFactory
WikiPageFactory $wikiPageFactory
Definition: ParserOutputAccess.php:112
MediaWiki\Revision\RevisionLookup
Service for looking up page revisions.
Definition: RevisionLookup.php:38
Page\ParserOutputAccess\$titleFormatter
TitleFormatter $titleFormatter
Definition: ParserOutputAccess.php:115
MediaWiki\Revision\RevisionRecord\DELETED_TEXT
const DELETED_TEXT
Definition: RevisionRecord.php:53
Status
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:44
Page\ParserOutputAccess\__construct
__construct(ParserCache $primaryCache, RevisionOutputCache $secondaryCache, RevisionLookup $revisionLookup, RevisionRenderer $revisionRenderer, IBufferingStatsdDataFactory $statsDataFactory, ILBFactory $lbFactory, LoggerSpi $loggerSpi, WikiPageFactory $wikiPageFactory, TitleFormatter $titleFormatter)
Definition: ParserOutputAccess.php:128
Page\ProperPageIdentity\getId
getId( $wikiId=self::LOCAL)
Returns the page ID.
Page\ParserOutputAccess\$primaryCache
ParserCache $primaryCache
Definition: ParserOutputAccess.php:85
Page\ParserOutputAccess\$revisionLookup
RevisionLookup $revisionLookup
Definition: ParserOutputAccess.php:97
Page\ParserOutputAccess\getParserOutput
getParserOutput(PageRecord $page, ParserOptions $parserOptions, ?RevisionRecord $revision=null, int $options=0)
Returns the rendered output for the given page.
Definition: ParserOutputAccess.php:245
Page\WikiPageFactory
Definition: WikiPageFactory.php:20
Page\ParserOutputAccess\shouldUseCache
shouldUseCache(PageRecord $page, ParserOptions $parserOptions, ?RevisionRecord $rev)
Use a cache?
Definition: ParserOutputAccess.php:159
Page\ParserOutputAccess\$revisionRenderer
RevisionRenderer $revisionRenderer
Definition: ParserOutputAccess.php:100
MediaWiki\Revision\RevisionRenderer
The RevisionRenderer service provides access to rendered output for revisions.
Definition: RevisionRenderer.php:45
StatusValue\newGood
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:82
IBufferingStatsdDataFactory
MediaWiki adaptation of StatsdDataFactory that provides buffering functionality.
Definition: IBufferingStatsdDataFactory.php:13
Parser\RevisionOutputCache
Cache for ParserOutput objects.
Definition: RevisionOutputCache.php:44
TitleFormatter
A title formatter service for MediaWiki.
Definition: TitleFormatter.php:35
Page\PageIdentity\exists
exists()
Checks if the page currently exists.
ParserCache
Cache for ParserOutput objects corresponding to the latest page revisions.
Definition: ParserCache.php:63
MediaWiki\Page
Definition: ContentModelChangeFactory.php:23
MediaWiki\Logger\Spi
Service provider interface for \Psr\Log\LoggerInterface implementation libraries.
Definition: Spi.php:38
MediaWiki\Revision\RevisionRecord\FOR_PUBLIC
const FOR_PUBLIC
Definition: RevisionRecord.php:62
Page\ParserOutputAccess\checkPreconditions
checkPreconditions(PageRecord $page, ?RevisionRecord $revision=null, int $options=0)
Definition: ParserOutputAccess.php:325
Wikimedia\Rdbms\ILBFactory
An interface for generating database load balancers.
Definition: ILBFactory.php:33
CACHE_NONE
const CACHE_NONE
Definition: Defines.php:86