MediaWiki  master
ParserOutputAccess.php
Go to the documentation of this file.
1 <?php
20 namespace MediaWiki\Page;
21 
23 use InvalidArgumentException;
24 use MediaWiki\Logger\Spi as LoggerSpi;
32 use ParserCache;
33 use ParserOptions;
34 use ParserOutput;
35 use PoolCounterWork;
39 use Wikimedia\Assert\Assert;
42 
53 
57  public const OPT_NO_CHECK_CACHE = 1;
58 
60  public const OPT_FORCE_PARSE = self::OPT_NO_CHECK_CACHE;
61 
65  public const OPT_NO_UPDATE_CACHE = 2;
66 
72  public const OPT_NO_AUDIENCE_CHECK = 4;
73 
78  public const OPT_NO_CACHE = self::OPT_NO_UPDATE_CACHE | self::OPT_NO_CHECK_CACHE;
79 
84  public const OPT_LINKS_UPDATE = 8;
85 
87  private const CACHE_NONE = 'none';
88 
90  private const CACHE_PRIMARY = 'primary';
91 
93  private const CACHE_SECONDARY = 'secondary';
94 
95  private ParserCacheFactory $parserCacheFactory;
96 
102  private $localCache = [];
103 
105  private $revisionLookup;
106 
108  private $revisionRenderer;
109 
111  private $statsDataFactory;
112 
114  private $lbFactory;
115  private ChronologyProtector $chronologyProtector;
116 
118  private $loggerSpi;
119 
121  private $wikiPageFactory;
122 
124  private $titleFormatter;
125 
137  public function __construct(
138  ParserCacheFactory $parserCacheFactory,
139  RevisionLookup $revisionLookup,
140  RevisionRenderer $revisionRenderer,
141  IBufferingStatsdDataFactory $statsDataFactory,
142  ILBFactory $lbFactory,
143  ChronologyProtector $chronologyProtector,
144  LoggerSpi $loggerSpi,
145  WikiPageFactory $wikiPageFactory,
146  TitleFormatter $titleFormatter
147  ) {
148  $this->parserCacheFactory = $parserCacheFactory;
149  $this->revisionLookup = $revisionLookup;
150  $this->revisionRenderer = $revisionRenderer;
151  $this->statsDataFactory = $statsDataFactory;
152  $this->lbFactory = $lbFactory;
153  $this->chronologyProtector = $chronologyProtector;
154  $this->loggerSpi = $loggerSpi;
155  $this->wikiPageFactory = $wikiPageFactory;
156  $this->titleFormatter = $titleFormatter;
157  }
158 
167  private function shouldUseCache(
168  PageRecord $page,
169  ?RevisionRecord $rev
170  ) {
171  if ( $rev && !$rev->getId() ) {
172  // The revision isn't from the database, so the output can't safely be cached.
173  return self::CACHE_NONE;
174  }
175 
176  // NOTE: Keep in sync with ParserWikiPage::shouldCheckParserCache().
177  // NOTE: when we allow caching of old revisions in the future,
178  // we must not allow caching of deleted revisions.
179 
180  $wikiPage = $this->wikiPageFactory->newFromTitle( $page );
181  if ( !$page->exists() || !$wikiPage->getContentHandler()->isParserCacheSupported() ) {
182  return self::CACHE_NONE;
183  }
184 
185  $isOld = $rev && $rev->getId() !== $page->getLatest();
186  if ( !$isOld ) {
187  return self::CACHE_PRIMARY;
188  }
189 
191  // deleted/suppressed revision
192  return self::CACHE_NONE;
193  }
194 
195  return self::CACHE_SECONDARY;
196  }
197 
208  public function getCachedParserOutput(
209  PageRecord $page,
210  ParserOptions $parserOptions,
211  ?RevisionRecord $revision = null,
212  int $options = 0
213  ): ?ParserOutput {
214  $isOld = $revision && $revision->getId() !== $page->getLatest();
215  $useCache = $this->shouldUseCache( $page, $revision );
216  $primaryCache = $this->getPrimaryCache( $parserOptions );
217  $classCacheKey = $primaryCache->makeParserOutputKey( $page, $parserOptions );
218 
219  if ( $useCache === self::CACHE_PRIMARY ) {
220  if ( isset( $this->localCache[$classCacheKey] ) && !$isOld ) {
221  return $this->localCache[$classCacheKey];
222  }
223  $output = $primaryCache->get( $page, $parserOptions );
224  } elseif ( $useCache === self::CACHE_SECONDARY && $revision ) {
225  $secondaryCache = $this->getSecondaryCache( $parserOptions );
226  $output = $secondaryCache->get( $revision, $parserOptions );
227  } else {
228  $output = null;
229  }
230 
231  if ( $output && !$isOld ) {
232  $this->localCache[$classCacheKey] = $output;
233  }
234 
235  if ( $output ) {
236  $this->statsDataFactory->increment( "ParserOutputAccess.Cache.$useCache.hit" );
237  } else {
238  $this->statsDataFactory->increment( "ParserOutputAccess.Cache.$useCache.miss" );
239  }
240 
241  return $output ?: null; // convert false to null
242  }
243 
266  public function getParserOutput(
267  PageRecord $page,
268  ParserOptions $parserOptions,
269  ?RevisionRecord $revision = null,
270  int $options = 0
271  ): Status {
272  $error = $this->checkPreconditions( $page, $revision, $options );
273  if ( $error ) {
274  $this->statsDataFactory->increment( "ParserOutputAccess.Case.error" );
275  return $error;
276  }
277 
278  $isOld = $revision && $revision->getId() !== $page->getLatest();
279  if ( $isOld ) {
280  $this->statsDataFactory->increment( 'ParserOutputAccess.Case.old' );
281  } else {
282  $this->statsDataFactory->increment( 'ParserOutputAccess.Case.current' );
283  }
284 
285  if ( !( $options & self::OPT_NO_CHECK_CACHE ) ) {
286  $output = $this->getCachedParserOutput( $page, $parserOptions, $revision );
287  if ( $output ) {
288  return Status::newGood( $output );
289  }
290  }
291 
292  if ( !$revision ) {
293  $revId = $page->getLatest();
294  $revision = $revId ? $this->revisionLookup->getRevisionById( $revId ) : null;
295 
296  if ( !$revision ) {
297  $this->statsDataFactory->increment( "ParserOutputAccess.Status.norev" );
298  return Status::newFatal( 'missing-revision', $revId );
299  }
300  }
301 
302  $work = $this->newPoolWorkArticleView( $page, $parserOptions, $revision, $options );
304  $status = $work->execute();
305  $output = $status->getValue();
306  Assert::postcondition( $output || !$status->isOK(), 'Worker returned invalid status' );
307 
308  if ( $output && !$isOld ) {
309  $primaryCache = $this->getPrimaryCache( $parserOptions );
310  $classCacheKey = $primaryCache->makeParserOutputKey( $page, $parserOptions );
311  $this->localCache[$classCacheKey] = $output;
312  }
313 
314  if ( $status->isGood() ) {
315  $this->statsDataFactory->increment( 'ParserOutputAccess.Status.good' );
316  } elseif ( $status->isOK() ) {
317  $this->statsDataFactory->increment( 'ParserOutputAccess.Status.ok' );
318  } else {
319  $this->statsDataFactory->increment( 'ParserOutputAccess.Status.error' );
320  }
321 
322  return $status;
323  }
324 
332  private function checkPreconditions(
333  PageRecord $page,
334  ?RevisionRecord $revision = null,
335  int $options = 0
336  ): ?Status {
337  if ( !$page->exists() ) {
338  return Status::newFatal( 'nopagetext' );
339  }
340 
341  if ( !( $options & self::OPT_NO_UPDATE_CACHE ) && $revision && !$revision->getId() ) {
342  throw new InvalidArgumentException(
343  'The revision does not have a known ID. Use OPT_NO_CACHE.'
344  );
345  }
346 
347  if ( $revision && $revision->getPageId() !== $page->getId() ) {
348  throw new InvalidArgumentException(
349  'The revision does not belong to the given page.'
350  );
351  }
352 
353  if ( $revision && !( $options & self::OPT_NO_AUDIENCE_CHECK ) ) {
354  // NOTE: If per-user checks are desired, the caller should perform them and
355  // then set OPT_NO_AUDIENCE_CHECK if they passed.
356  if ( !$revision->audienceCan( RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_PUBLIC ) ) {
357  return Status::newFatal(
358  'missing-revision-permission',
359  $revision->getId(),
360  $revision->getTimestamp(),
361  $this->titleFormatter->getPrefixedDBkey( $page )
362  );
363  }
364  }
365 
366  return null;
367  }
368 
377  private function newPoolWorkArticleView(
378  PageRecord $page,
379  ParserOptions $parserOptions,
380  RevisionRecord $revision,
381  int $options
382  ): PoolCounterWork {
383  $useCache = $this->shouldUseCache( $page, $revision );
384 
385  switch ( $useCache ) {
386  case self::CACHE_PRIMARY:
387  $this->statsDataFactory->increment( 'ParserOutputAccess.PoolWork.Current' );
388  $primaryCache = $this->getPrimaryCache( $parserOptions );
389  $parserCacheMetadata = $primaryCache->getMetadata( $page );
390  $cacheKey = $primaryCache->makeParserOutputKey( $page, $parserOptions,
391  $parserCacheMetadata ? $parserCacheMetadata->getUsedOptions() : null
392  );
393 
394  $workKey = $cacheKey . ':revid:' . $revision->getId();
395 
396  return new PoolWorkArticleViewCurrent(
397  $workKey,
398  $page,
399  $revision,
400  $parserOptions,
401  $this->revisionRenderer,
402  $primaryCache,
403  $this->lbFactory,
404  $this->chronologyProtector,
405  $this->loggerSpi,
406  $this->wikiPageFactory,
407  !( $options & self::OPT_NO_UPDATE_CACHE ),
408  (bool)( $options & self::OPT_LINKS_UPDATE )
409  );
410 
411  case self::CACHE_SECONDARY:
412  $this->statsDataFactory->increment( 'ParserOutputAccess.PoolWork.Old' );
413  $secondaryCache = $this->getSecondaryCache( $parserOptions );
414  $workKey = $secondaryCache->makeParserOutputKey( $revision, $parserOptions );
415  return new PoolWorkArticleViewOld(
416  $workKey,
417  $secondaryCache,
418  $revision,
419  $parserOptions,
420  $this->revisionRenderer,
421  $this->loggerSpi
422  );
423 
424  default:
425  $this->statsDataFactory->increment( 'ParserOutputAccess.PoolWork.Uncached' );
426  $secondaryCache = $this->getSecondaryCache( $parserOptions );
427  $workKey = $secondaryCache->makeParserOutputKeyOptionalRevId( $revision, $parserOptions );
428  return new PoolWorkArticleView(
429  $workKey,
430  $revision,
431  $parserOptions,
432  $this->revisionRenderer,
433  $this->loggerSpi
434  );
435  }
436 
437  // unreachable
438  }
439 
440  private function getPrimaryCache( ParserOptions $pOpts ): ParserCache {
441  if ( $pOpts->getUseParsoid() ) {
442  // T331148: This is different from
443  // ParsoidOutputAccess::PARSOID_PARSER_CACHE_NAME; will be
444  // renamed once the contents cached on the read-views and
445  // the REST path are identical.
446  return $this->parserCacheFactory->getParserCache(
447  'parsoid-' . ParserCacheFactory::DEFAULT_NAME
448  );
449  }
450 
451  return $this->parserCacheFactory->getParserCache(
452  ParserCacheFactory::DEFAULT_NAME
453  );
454  }
455 
456  private function getSecondaryCache( ParserOptions $pOpts ): RevisionOutputCache {
457  if ( $pOpts->getUseParsoid() ) {
458  return $this->parserCacheFactory->getRevisionOutputCache(
459  'parsoid-' . ParserCacheFactory::DEFAULT_RCACHE_NAME
460  );
461  }
462 
463  return $this->parserCacheFactory->getRevisionOutputCache(
464  ParserCacheFactory::DEFAULT_RCACHE_NAME
465  );
466  }
467 
468 }
const CACHE_NONE
Definition: Defines.php:86
if(!defined('MW_SETUP_CALLBACK'))
Definition: WebStart.php:88
Service for getting rendered output of a given page.
__construct(ParserCacheFactory $parserCacheFactory, RevisionLookup $revisionLookup, RevisionRenderer $revisionRenderer, IBufferingStatsdDataFactory $statsDataFactory, ILBFactory $lbFactory, ChronologyProtector $chronologyProtector, LoggerSpi $loggerSpi, WikiPageFactory $wikiPageFactory, TitleFormatter $titleFormatter)
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.
getParserOutput(PageRecord $page, ParserOptions $parserOptions, ?RevisionRecord $revision=null, int $options=0)
Returns the rendered output for the given page.
Service for creating WikiPage objects.
Cache for ParserOutput objects.
Page revision base class.
audienceCan( $field, $audience, Authority $performer=null)
Check that the given audience has access to the given field.
getId( $wikiId=self::LOCAL)
Get revision ID.
The RevisionRenderer service provides access to rendered output for revisions.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:58
Cache for ParserOutput objects corresponding to the latest page revisions.
Definition: ParserCache.php:64
Set options of the Parser.
Class for dealing with PoolCounters using class members.
PoolWorkArticleView for the current revision of a page, using ParserCache.
PoolWorkArticleView for an old revision of a page, using a simple cache.
PoolCounter protected work wrapping RenderedRevision->getRevisionParserOutput.
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:73
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:85
Provide a given client with protection against visible database lag.
MediaWiki adaptation of StatsdDataFactory that provides buffering functionality.
Service provider interface for \Psr\Log\LoggerInterface implementation libraries.
Definition: Spi.php:38
exists()
Checks if the page currently exists.
Data record representing a page that is (or used to be, or could be) an editable page on a wiki.
Definition: PageRecord.php:26
getLatest( $wikiId=self::LOCAL)
The ID of the page's latest revision.
getId( $wikiId=self::LOCAL)
Returns the page ID.
Service for looking up page revisions.
A title formatter service for MediaWiki.
Manager of ILoadBalancer objects and, indirectly, IDatabase connections.
Definition: ILBFactory.php:46