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 
49 
53  public const OPT_NO_CHECK_CACHE = 1;
54 
56  public const OPT_FORCE_PARSE = self::OPT_NO_CHECK_CACHE;
57 
62  private const OPT_NO_UPDATE_CACHE = 2;
63 
69  public const OPT_NO_AUDIENCE_CHECK = 4;
70 
75  public const OPT_NO_CACHE = self::OPT_NO_UPDATE_CACHE | self::OPT_NO_CHECK_CACHE;
76 
78  private const CACHE_NONE = 'none';
79 
81  private const CACHE_PRIMARY = 'primary';
82 
84  private const CACHE_SECONDARY = 'secondary';
85 
87  private $primaryCache;
88 
92  private $secondaryCache;
93 
95  private $revisionLookup;
96 
99 
102 
104  private $lbFactory;
105 
107  private $loggerSpi;
108 
111 
114 
126  public function __construct(
128  RevisionOutputCache $secondaryCache,
129  RevisionLookup $revisionLookup,
130  RevisionRenderer $revisionRenderer,
132  ILBFactory $lbFactory,
133  LoggerSpi $loggerSpi,
136  ) {
137  $this->primaryCache = $primaryCache;
138  $this->secondaryCache = $secondaryCache;
139  $this->revisionLookup = $revisionLookup;
140  $this->revisionRenderer = $revisionRenderer;
141  $this->statsDataFactory = $statsDataFactory;
142  $this->lbFactory = $lbFactory;
143  $this->loggerSpi = $loggerSpi;
144  $this->wikiPageFactory = $wikiPageFactory;
145  $this->titleFormatter = $titleFormatter;
146  }
147 
157  private function shouldUseCache(
158  PageRecord $page,
159  ParserOptions $parserOptions,
160  ?RevisionRecord $rev
161  ) {
162  if ( $rev && !$rev->getId() ) {
163  // The revision isn't from the database, so the output can't safely be cached.
164  return self::CACHE_NONE;
165  }
166 
167  // NOTE: Keep in sync with ParserWikiPage::shouldCheckParserCache().
168  // NOTE: when we allow caching of old revisions in the future,
169  // we must not allow caching of deleted revisions.
170 
171  $wikiPage = $this->wikiPageFactory->newFromTitle( $page );
172  if ( !$page->exists() || !$wikiPage->getContentHandler()->isParserCacheSupported() ) {
173  return self::CACHE_NONE;
174  }
175 
176  if ( !$rev || $rev->getId() === $page->getLatest( PageRecord::LOCAL ) ) {
177  // current revision
178  return self::CACHE_PRIMARY;
179  }
180 
181  if ( !$rev->audienceCan( RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_PUBLIC ) ) {
182  // deleted/suppressed revision
183  return self::CACHE_NONE;
184  }
185 
186  return self::CACHE_SECONDARY;
187  }
188 
199  public function getCachedParserOutput(
200  PageRecord $page,
201  ParserOptions $parserOptions,
202  ?RevisionRecord $revision = null,
203  int $options = 0
204  ): ?ParserOutput {
205  $useCache = $this->shouldUseCache( $page, $parserOptions, $revision );
206 
207  if ( $useCache === self::CACHE_PRIMARY ) {
208  $output = $this->primaryCache->get( $page, $parserOptions );
209  } elseif ( $useCache === self::CACHE_SECONDARY && $revision ) {
210  $output = $this->secondaryCache->get( $revision, $parserOptions );
211  } else {
212  $output = null;
213  }
214 
215  $hitOrMiss = $output ? 'hit' : 'miss';
216  $this->statsDataFactory->increment( "ParserOutputAccess.Cache.$useCache.$hitOrMiss" );
217 
218  return $output ?: null; // convert false to null
219  }
220 
243  public function getParserOutput(
244  PageRecord $page,
245  ParserOptions $parserOptions,
246  ?RevisionRecord $revision = null,
247  int $options = 0
248  ): Status {
249  $error = $this->checkPreconditions( $page, $revision, $options );
250  if ( $error ) {
251  $this->statsDataFactory->increment( "ParserOutputAccess.Case.error" );
252  return $error;
253  }
254 
255  $currentOrOld = ( $revision && $revision->getId() !== $page->getLatest() ) ? 'old' : 'current';
256  $this->statsDataFactory->increment( "ParserOutputAccess.Case.$currentOrOld" );
257 
258  if ( !( $options & self::OPT_NO_CHECK_CACHE ) ) {
259  $output = $this->getCachedParserOutput( $page, $parserOptions, $revision );
260  if ( $output ) {
261  return Status::newGood( $output );
262  }
263  }
264 
265  if ( !$revision ) {
266  $revision = $page->getLatest() ?
267  $this->revisionLookup->getRevisionById( $page->getLatest() ) : null;
268 
269  if ( !$revision ) {
270  $this->statsDataFactory->increment( "ParserOutputAccess.Status.norev" );
271  return Status::newFatal(
272  'missing-revision',
273  $page->getLatest()
274  );
275  }
276  }
277 
278  $work = $this->newPoolWorkArticleView( $page, $parserOptions, $revision, $options );
279  $work->execute();
280  $output = $work->getParserOutput();
281 
282  $status = Status::newGood();
283  if ( $work->getError() ) {
284  $status->merge( $work->getError() );
285  }
286 
287  if ( !$output && $status->isOK() ) {
288  // TODO: PoolWorkArticle should properly report errors (T267610)
289  $status->fatal( 'pool-errorunknown' );
290  }
291 
292  if ( $output && $status->isOK() ) {
293  $status->setResult( true, $output );
294  }
295 
296  if ( $status->isOK() && $work->getIsDirty() ) {
297  $staleReason = $work->getIsFastStale()
298  ? 'view-pool-contention' : 'view-pool-overload';
299 
300  $status->warning( 'view-pool-dirty-output' );
301  $status->warning( $staleReason );
302  }
303 
304  if ( $status->isGood() ) {
305  $statusMsg = 'good';
306  } elseif ( $status->isOK() ) {
307  $statusMsg = 'ok';
308  } else {
309  $statusMsg = 'error';
310  }
311 
312  $this->statsDataFactory->increment( "ParserOutputAccess.Status.$statusMsg" );
313  return $status;
314  }
315 
323  private function checkPreconditions(
324  PageRecord $page,
325  ?RevisionRecord $revision = null,
326  int $options = 0
327  ): ?Status {
328  if ( !$page->exists() ) {
329  return Status::newFatal( 'nopagetext' );
330  }
331 
332  if ( !( $options & self::OPT_NO_UPDATE_CACHE ) && $revision && !$revision->getId() ) {
333  throw new InvalidArgumentException(
334  'The revision does not have a known ID. Use NO_CACHE.'
335  );
336  }
337 
338  if ( $revision && $revision->getPageId() !== $page->getId() ) {
339  throw new InvalidArgumentException(
340  'The revision does not belong to the given page.'
341  );
342  }
343 
344  if ( $revision && !( $options & self::OPT_NO_AUDIENCE_CHECK ) ) {
345  // NOTE: If per-user checks are desired, the caller should perform them and
346  // then set OPT_NO_AUDIENCE_CHECK if they passed.
347  if ( !$revision->audienceCan( RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_PUBLIC ) ) {
348  return Status::newFatal(
349  'missing-revision-permission',
350  $revision->getId(),
351  $revision->getTimestamp(),
352  $this->titleFormatter->getPrefixedDBkey( $page )
353  );
354  }
355  }
356 
357  return null;
358  }
359 
368  private function newPoolWorkArticleView(
369  PageRecord $page,
370  ParserOptions $parserOptions,
371  RevisionRecord $revision,
372  int $options
374  if ( $options & self::OPT_NO_UPDATE_CACHE ) {
375  $useCache = self::CACHE_NONE;
376  } else {
377  $useCache = $this->shouldUseCache( $page, $parserOptions, $revision );
378  }
379 
380  switch ( $useCache ) {
381  case self::CACHE_PRIMARY:
382  $this->statsDataFactory->increment( 'ParserOutputAccess.PoolWork.Current' );
383  $parserCacheMetadata = $this->primaryCache->getMetadata( $page );
384  $cacheKey = $this->primaryCache->makeParserOutputKey( $page, $parserOptions,
385  $parserCacheMetadata ? $parserCacheMetadata->getUsedOptions() : null
386  );
387 
388  $workKey = $cacheKey . ':revid:' . $revision->getId();
389 
390  return new PoolWorkArticleViewCurrent(
391  $workKey,
392  $page,
393  $revision,
394  $parserOptions,
395  $this->revisionRenderer,
396  $this->primaryCache,
397  $this->lbFactory,
398  $this->loggerSpi,
399  $this->wikiPageFactory
400  );
401 
402  case $useCache == self::CACHE_SECONDARY:
403  $this->statsDataFactory->increment( 'ParserOutputAccess.PoolWork.Old' );
404  $workKey = $this->secondaryCache->makeParserOutputKey( $revision, $parserOptions );
405  return new PoolWorkArticleViewOld(
406  $workKey,
407  $this->secondaryCache,
408  $revision,
409  $parserOptions,
410  $this->revisionRenderer,
411  $this->loggerSpi
412  );
413 
414  default:
415  $this->statsDataFactory->increment( 'ParserOutputAccess.PoolWork.Uncached' );
416  $workKey = $this->secondaryCache->makeParserOutputKey( $revision, $parserOptions );
417  return new PoolWorkArticleView(
418  $workKey,
419  $revision,
420  $parserOptions,
421  $this->revisionRenderer,
422  $this->loggerSpi
423  );
424  }
425 
426  // unreachable
427  }
428 
429 }
ParserOptions
Set options of the Parser.
Definition: ParserOptions.php:45
Page\ParserOutputAccess\$secondaryCache
RevisionOutputCache $secondaryCache
Definition: ParserOutputAccess.php:92
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:35
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:199
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:368
Page\ParserOutputAccess
Service for getting rendered output of a given page.
Definition: ParserOutputAccess.php:48
PoolWorkArticleView
PoolCounter protected work wrapping RenderedRevision->getRevisionParserOutput.
Definition: PoolWorkArticleView.php:34
Page\ParserOutputAccess\$loggerSpi
LoggerSpi $loggerSpi
Definition: ParserOutputAccess.php:107
Page\ParserOutputAccess\$lbFactory
ILBFactory $lbFactory
Definition: ParserOutputAccess.php:104
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:101
Page\ParserOutputAccess\$wikiPageFactory
WikiPageFactory $wikiPageFactory
Definition: ParserOutputAccess.php:110
MediaWiki\Revision\RevisionLookup
Service for looking up page revisions.
Definition: RevisionLookup.php:38
Page\ParserOutputAccess\$titleFormatter
TitleFormatter $titleFormatter
Definition: ParserOutputAccess.php:113
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:126
Page\ProperPageIdentity\getId
getId( $wikiId=self::LOCAL)
Returns the page ID.
Page\ParserOutputAccess\$primaryCache
ParserCache $primaryCache
Definition: ParserOutputAccess.php:83
Page\ParserOutputAccess\$revisionLookup
RevisionLookup $revisionLookup
Definition: ParserOutputAccess.php:95
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:243
Page\WikiPageFactory
Definition: WikiPageFactory.php:19
Page\ParserOutputAccess\shouldUseCache
shouldUseCache(PageRecord $page, ParserOptions $parserOptions, ?RevisionRecord $rev)
Use a cache?
Definition: ParserOutputAccess.php:157
Page\ParserOutputAccess\$revisionRenderer
RevisionRenderer $revisionRenderer
Definition: ParserOutputAccess.php:98
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:323
Wikimedia\Rdbms\ILBFactory
An interface for generating database load balancers.
Definition: ILBFactory.php:33
CACHE_NONE
const CACHE_NONE
Definition: Defines.php:86