MediaWiki 1.42.0
ParserOutputAccess.php
Go to the documentation of this file.
1<?php
20namespace MediaWiki\Page;
21
23use InvalidArgumentException;
24use MapCacheLRU;
25use MediaWiki\Logger\Spi as LoggerSpi;
38use ParserCache;
40use Wikimedia\Assert\Assert;
43
54
56 public const PARSOID_PCACHE_NAME = 'parsoid-' . ParserCacheFactory::DEFAULT_NAME;
57
59 public const PARSOID_RCACHE_NAME = 'parsoid-' . ParserCacheFactory::DEFAULT_RCACHE_NAME;
60
64 public const OPT_NO_CHECK_CACHE = 1;
65
67 public const OPT_FORCE_PARSE = self::OPT_NO_CHECK_CACHE;
68
72 public const OPT_NO_UPDATE_CACHE = 2;
73
79 public const OPT_NO_AUDIENCE_CHECK = 4;
80
85 public const OPT_NO_CACHE = self::OPT_NO_UPDATE_CACHE | self::OPT_NO_CHECK_CACHE;
86
91 public const OPT_LINKS_UPDATE = 8;
92
105 public const OPT_FOR_ARTICLE_VIEW = 16;
106
108 private const CACHE_NONE = 'none';
109
111 private const CACHE_PRIMARY = 'primary';
112
114 private const CACHE_SECONDARY = 'secondary';
115
116 private ParserCacheFactory $parserCacheFactory;
117
123 private MapCacheLRU $localCache;
124
126 private $revisionLookup;
127
129 private $revisionRenderer;
130
132 private $statsDataFactory;
133
135 private $lbFactory;
136 private ChronologyProtector $chronologyProtector;
137
139 private $loggerSpi;
140
142 private $wikiPageFactory;
143
145 private $titleFormatter;
146
158 public function __construct(
159 ParserCacheFactory $parserCacheFactory,
160 RevisionLookup $revisionLookup,
161 RevisionRenderer $revisionRenderer,
162 IBufferingStatsdDataFactory $statsDataFactory,
163 ILBFactory $lbFactory,
164 ChronologyProtector $chronologyProtector,
165 LoggerSpi $loggerSpi,
166 WikiPageFactory $wikiPageFactory,
167 TitleFormatter $titleFormatter
168 ) {
169 $this->parserCacheFactory = $parserCacheFactory;
170 $this->revisionLookup = $revisionLookup;
171 $this->revisionRenderer = $revisionRenderer;
172 $this->statsDataFactory = $statsDataFactory;
173 $this->lbFactory = $lbFactory;
174 $this->chronologyProtector = $chronologyProtector;
175 $this->loggerSpi = $loggerSpi;
176 $this->wikiPageFactory = $wikiPageFactory;
177 $this->titleFormatter = $titleFormatter;
178
179 $this->localCache = new MapCacheLRU( 10 );
180 }
181
190 private function shouldUseCache(
191 PageRecord $page,
192 ?RevisionRecord $rev
193 ) {
194 if ( $rev && !$rev->getId() ) {
195 // The revision isn't from the database, so the output can't safely be cached.
196 return self::CACHE_NONE;
197 }
198
199 // NOTE: Keep in sync with ParserWikiPage::shouldCheckParserCache().
200 // NOTE: when we allow caching of old revisions in the future,
201 // we must not allow caching of deleted revisions.
202
203 $wikiPage = $this->wikiPageFactory->newFromTitle( $page );
204 if ( !$page->exists() || !$wikiPage->getContentHandler()->isParserCacheSupported() ) {
205 return self::CACHE_NONE;
206 }
207
208 $isOld = $rev && $rev->getId() !== $page->getLatest();
209 if ( !$isOld ) {
210 return self::CACHE_PRIMARY;
211 }
212
213 if ( !$rev->audienceCan( RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_PUBLIC ) ) {
214 // deleted/suppressed revision
215 return self::CACHE_NONE;
216 }
217
218 return self::CACHE_SECONDARY;
219 }
220
231 public function getCachedParserOutput(
232 PageRecord $page,
233 ParserOptions $parserOptions,
234 ?RevisionRecord $revision = null,
235 int $options = 0
236 ): ?ParserOutput {
237 $isOld = $revision && $revision->getId() !== $page->getLatest();
238 $useCache = $this->shouldUseCache( $page, $revision );
239 $primaryCache = $this->getPrimaryCache( $parserOptions );
240 $classCacheKey = $primaryCache->makeParserOutputKey( $page, $parserOptions );
241
242 if ( $useCache === self::CACHE_PRIMARY ) {
243 if ( $this->localCache->hasField( $classCacheKey, $page->getLatest() ) && !$isOld ) {
244 return $this->localCache->getField( $classCacheKey, $page->getLatest() );
245 }
246 $output = $primaryCache->get( $page, $parserOptions );
247 } elseif ( $useCache === self::CACHE_SECONDARY && $revision ) {
248 $secondaryCache = $this->getSecondaryCache( $parserOptions );
249 $output = $secondaryCache->get( $revision, $parserOptions );
250 } else {
251 $output = null;
252 }
253
254 if ( $output && !$isOld ) {
255 $this->localCache->setField( $classCacheKey, $page->getLatest(), $output );
256 }
257
258 if ( $output ) {
259 $this->statsDataFactory->increment( "ParserOutputAccess.Cache.$useCache.hit" );
260 } else {
261 $this->statsDataFactory->increment( "ParserOutputAccess.Cache.$useCache.miss" );
262 }
263
264 return $output ?: null; // convert false to null
265 }
266
289 public function getParserOutput(
290 PageRecord $page,
291 ParserOptions $parserOptions,
292 ?RevisionRecord $revision = null,
293 int $options = 0
294 ): Status {
295 $error = $this->checkPreconditions( $page, $revision, $options );
296 if ( $error ) {
297 $this->statsDataFactory->increment( "ParserOutputAccess.Case.error" );
298 return $error;
299 }
300
301 $isOld = $revision && $revision->getId() !== $page->getLatest();
302 if ( $isOld ) {
303 $this->statsDataFactory->increment( 'ParserOutputAccess.Case.old' );
304 } else {
305 $this->statsDataFactory->increment( 'ParserOutputAccess.Case.current' );
306 }
307
308 if ( !( $options & self::OPT_NO_CHECK_CACHE ) ) {
309 $output = $this->getCachedParserOutput( $page, $parserOptions, $revision );
310 if ( $output ) {
311 return Status::newGood( $output );
312 }
313 }
314
315 if ( !$revision ) {
316 $revId = $page->getLatest();
317 $revision = $revId ? $this->revisionLookup->getRevisionById( $revId ) : null;
318
319 if ( !$revision ) {
320 $this->statsDataFactory->increment( "ParserOutputAccess.Status.norev" );
321 return Status::newFatal( 'missing-revision', $revId );
322 }
323 }
324
325 if ( $options & self::OPT_FOR_ARTICLE_VIEW ) {
326 $work = $this->newPoolWorkArticleView( $page, $parserOptions, $revision, $options );
328 $status = $work->execute();
329 } else {
330 $status = $this->renderRevision( $page, $parserOptions, $revision, $options );
331 }
332
333 $output = $status->getValue();
334 Assert::postcondition( $output || !$status->isOK(), 'Inconsistent status' );
335
336 if ( $output && !$isOld ) {
337 $primaryCache = $this->getPrimaryCache( $parserOptions );
338 $classCacheKey = $primaryCache->makeParserOutputKey( $page, $parserOptions );
339 $this->localCache->setField( $classCacheKey, $page->getLatest(), $output );
340 }
341
342 if ( $status->isGood() ) {
343 $this->statsDataFactory->increment( 'ParserOutputAccess.Status.good' );
344 } elseif ( $status->isOK() ) {
345 $this->statsDataFactory->increment( 'ParserOutputAccess.Status.ok' );
346 } else {
347 $this->statsDataFactory->increment( 'ParserOutputAccess.Status.error' );
348 }
349
350 return $status;
351 }
352
364 private function renderRevision(
365 PageRecord $page,
366 ParserOptions $parserOptions,
367 RevisionRecord $revision,
368 int $options
369 ): Status {
370 $this->statsDataFactory->increment( 'ParserOutputAccess.PoolWork.None' );
371
372 $renderedRev = $this->revisionRenderer->getRenderedRevision(
373 $revision,
374 $parserOptions,
375 null,
376 [ 'audience' => RevisionRecord::RAW ]
377 );
378
379 $output = $renderedRev->getRevisionParserOutput();
380
381 if ( !( $options & self::OPT_NO_UPDATE_CACHE ) && $output->isCacheable() ) {
382 $useCache = $this->shouldUseCache( $page, $revision );
383
384 if ( $useCache === self::CACHE_PRIMARY ) {
385 $primaryCache = $this->getPrimaryCache( $parserOptions );
386 $primaryCache->save( $output, $page, $parserOptions );
387 } elseif ( $useCache === self::CACHE_SECONDARY ) {
388 $secondaryCache = $this->getSecondaryCache( $parserOptions );
389 $secondaryCache->save( $output, $revision, $parserOptions );
390 }
391 }
392
393 if ( $options & self::OPT_LINKS_UPDATE ) {
394 $this->wikiPageFactory->newFromTitle( $page )
395 ->triggerOpportunisticLinksUpdate( $output );
396 }
397
398 return Status::newGood( $output );
399 }
400
408 private function checkPreconditions(
409 PageRecord $page,
410 ?RevisionRecord $revision = null,
411 int $options = 0
412 ): ?Status {
413 if ( !$page->exists() ) {
414 return Status::newFatal( 'nopagetext' );
415 }
416
417 if ( !( $options & self::OPT_NO_UPDATE_CACHE ) && $revision && !$revision->getId() ) {
418 throw new InvalidArgumentException(
419 'The revision does not have a known ID. Use OPT_NO_CACHE.'
420 );
421 }
422
423 if ( $revision && $revision->getPageId() !== $page->getId() ) {
424 throw new InvalidArgumentException(
425 'The revision does not belong to the given page.'
426 );
427 }
428
429 if ( $revision && !( $options & self::OPT_NO_AUDIENCE_CHECK ) ) {
430 // NOTE: If per-user checks are desired, the caller should perform them and
431 // then set OPT_NO_AUDIENCE_CHECK if they passed.
432 if ( !$revision->audienceCan( RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_PUBLIC ) ) {
433 return Status::newFatal(
434 'missing-revision-permission',
435 $revision->getId(),
436 $revision->getTimestamp(),
437 $this->titleFormatter->getPrefixedDBkey( $page )
438 );
439 }
440 }
441
442 return null;
443 }
444
453 protected function newPoolWorkArticleView(
454 PageRecord $page,
455 ParserOptions $parserOptions,
456 RevisionRecord $revision,
457 int $options
458 ): PoolCounterWork {
459 $useCache = $this->shouldUseCache( $page, $revision );
460
461 switch ( $useCache ) {
462 case self::CACHE_PRIMARY:
463 $this->statsDataFactory->increment( 'ParserOutputAccess.PoolWork.Current' );
464 $primaryCache = $this->getPrimaryCache( $parserOptions );
465 $parserCacheMetadata = $primaryCache->getMetadata( $page );
466 $cacheKey = $primaryCache->makeParserOutputKey( $page, $parserOptions,
467 $parserCacheMetadata ? $parserCacheMetadata->getUsedOptions() : null
468 );
469
470 $workKey = $cacheKey . ':revid:' . $revision->getId();
471
473 $workKey,
474 $page,
475 $revision,
476 $parserOptions,
477 $this->revisionRenderer,
478 $primaryCache,
479 $this->lbFactory,
480 $this->chronologyProtector,
481 $this->loggerSpi,
482 $this->wikiPageFactory,
483 !( $options & self::OPT_NO_UPDATE_CACHE ),
484 (bool)( $options & self::OPT_LINKS_UPDATE )
485 );
486
487 case self::CACHE_SECONDARY:
488 $this->statsDataFactory->increment( 'ParserOutputAccess.PoolWork.Old' );
489 $secondaryCache = $this->getSecondaryCache( $parserOptions );
490 $workKey = $secondaryCache->makeParserOutputKey( $revision, $parserOptions );
491 return new PoolWorkArticleViewOld(
492 $workKey,
493 $secondaryCache,
494 $revision,
495 $parserOptions,
496 $this->revisionRenderer,
497 $this->loggerSpi
498 );
499
500 default:
501 $this->statsDataFactory->increment( 'ParserOutputAccess.PoolWork.Uncached' );
502 $secondaryCache = $this->getSecondaryCache( $parserOptions );
503 $workKey = $secondaryCache->makeParserOutputKeyOptionalRevId( $revision, $parserOptions );
504 return new PoolWorkArticleView(
505 $workKey,
506 $revision,
507 $parserOptions,
508 $this->revisionRenderer,
509 $this->loggerSpi
510 );
511 }
512
513 // unreachable
514 }
515
516 private function getPrimaryCache( ParserOptions $pOpts ): ParserCache {
517 if ( $pOpts->getUseParsoid() ) {
518 return $this->parserCacheFactory->getParserCache(
519 self::PARSOID_PCACHE_NAME
520 );
521 }
522
523 return $this->parserCacheFactory->getParserCache(
524 ParserCacheFactory::DEFAULT_NAME
525 );
526 }
527
528 private function getSecondaryCache( ParserOptions $pOpts ): RevisionOutputCache {
529 if ( $pOpts->getUseParsoid() ) {
530 return $this->parserCacheFactory->getRevisionOutputCache(
531 self::PARSOID_RCACHE_NAME
532 );
533 }
534
535 return $this->parserCacheFactory->getRevisionOutputCache(
536 ParserCacheFactory::DEFAULT_RCACHE_NAME
537 );
538 }
539
540}
const CACHE_NONE
Definition Defines.php:86
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:81
Store key-value entries in a size-limited in-memory LRU cache.
Service for getting rendered output of a given page.
const OPT_FOR_ARTICLE_VIEW
Apply page view semantics.
__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.
newPoolWorkArticleView(PageRecord $page, ParserOptions $parserOptions, RevisionRecord $revision, int $options)
getParserOutput(PageRecord $page, ParserOptions $parserOptions, ?RevisionRecord $revision=null, int $options=0)
Returns the rendered output for the given page.
Service for creating WikiPage objects.
Rendered output of a wiki page, as parsed from wikitext.
Cache for ParserOutput objects.
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.
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:54
Cache for ParserOutput objects corresponding to the latest page revisions.
Set options of the Parser.
Provide a given client with protection against visible database lag.
MediaWiki adaptation of StatsdDataFactory that provides buffering functionality.
Service provider interface to create \Psr\Log\LoggerInterface objects.
Definition Spi.php:64
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.
getLatest( $wikiId=self::LOCAL)
The ID of the page's latest revision.
Service for looking up page revisions.
A title formatter service for MediaWiki.
Manager of ILoadBalancer objects and, indirectly, IDatabase connections.