MediaWiki master
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;
39use ParserCache;
41use Wikimedia\Assert\Assert;
42use Wikimedia\Parsoid\Parsoid;
45
56
58 public const PARSOID_PCACHE_NAME = 'parsoid-' . ParserCacheFactory::DEFAULT_NAME;
59
61 public const PARSOID_RCACHE_NAME = 'parsoid-' . ParserCacheFactory::DEFAULT_RCACHE_NAME;
62
66 public const OPT_NO_CHECK_CACHE = 1;
67
69 public const OPT_FORCE_PARSE = self::OPT_NO_CHECK_CACHE;
70
74 public const OPT_NO_UPDATE_CACHE = 2;
75
81 public const OPT_NO_AUDIENCE_CHECK = 4;
82
87 public const OPT_NO_CACHE = self::OPT_NO_UPDATE_CACHE | self::OPT_NO_CHECK_CACHE;
88
93 public const OPT_LINKS_UPDATE = 8;
94
107 public const OPT_FOR_ARTICLE_VIEW = 16;
108
113 public const OPT_IGNORE_PROFILE_VERSION = 128;
114
116 private const CACHE_NONE = 'none';
117
119 private const CACHE_PRIMARY = 'primary';
120
122 private const CACHE_SECONDARY = 'secondary';
123
124 private ParserCacheFactory $parserCacheFactory;
125
131 private MapCacheLRU $localCache;
132
134 private $revisionLookup;
135
137 private $revisionRenderer;
138
140 private $statsDataFactory;
141
143 private $lbFactory;
144 private ChronologyProtector $chronologyProtector;
145
147 private $loggerSpi;
148
150 private $wikiPageFactory;
151
153 private $titleFormatter;
154
166 public function __construct(
167 ParserCacheFactory $parserCacheFactory,
168 RevisionLookup $revisionLookup,
169 RevisionRenderer $revisionRenderer,
170 IBufferingStatsdDataFactory $statsDataFactory,
171 ILBFactory $lbFactory,
172 ChronologyProtector $chronologyProtector,
173 LoggerSpi $loggerSpi,
174 WikiPageFactory $wikiPageFactory,
175 TitleFormatter $titleFormatter
176 ) {
177 $this->parserCacheFactory = $parserCacheFactory;
178 $this->revisionLookup = $revisionLookup;
179 $this->revisionRenderer = $revisionRenderer;
180 $this->statsDataFactory = $statsDataFactory;
181 $this->lbFactory = $lbFactory;
182 $this->chronologyProtector = $chronologyProtector;
183 $this->loggerSpi = $loggerSpi;
184 $this->wikiPageFactory = $wikiPageFactory;
185 $this->titleFormatter = $titleFormatter;
186
187 $this->localCache = new MapCacheLRU( 10 );
188 }
189
198 private function shouldUseCache(
199 PageRecord $page,
200 ?RevisionRecord $rev
201 ) {
202 if ( $rev && !$rev->getId() ) {
203 // The revision isn't from the database, so the output can't safely be cached.
204 return self::CACHE_NONE;
205 }
206
207 // NOTE: Keep in sync with ParserWikiPage::shouldCheckParserCache().
208 // NOTE: when we allow caching of old revisions in the future,
209 // we must not allow caching of deleted revisions.
210
211 $wikiPage = $this->wikiPageFactory->newFromTitle( $page );
212 if ( !$page->exists() || !$wikiPage->getContentHandler()->isParserCacheSupported() ) {
213 return self::CACHE_NONE;
214 }
215
216 $isOld = $rev && $rev->getId() !== $page->getLatest();
217 if ( !$isOld ) {
218 return self::CACHE_PRIMARY;
219 }
220
221 if ( !$rev->audienceCan( RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_PUBLIC ) ) {
222 // deleted/suppressed revision
223 return self::CACHE_NONE;
224 }
225
226 return self::CACHE_SECONDARY;
227 }
228
239 public function getCachedParserOutput(
240 PageRecord $page,
241 ParserOptions $parserOptions,
242 ?RevisionRecord $revision = null,
243 int $options = 0
244 ): ?ParserOutput {
245 $isOld = $revision && $revision->getId() !== $page->getLatest();
246 $useCache = $this->shouldUseCache( $page, $revision );
247 $primaryCache = $this->getPrimaryCache( $parserOptions );
248 $classCacheKey = $primaryCache->makeParserOutputKey( $page, $parserOptions );
249
250 if ( $useCache === self::CACHE_PRIMARY ) {
251 if ( $this->localCache->hasField( $classCacheKey, $page->getLatest() ) && !$isOld ) {
252 return $this->localCache->getField( $classCacheKey, $page->getLatest() );
253 }
254 $output = $primaryCache->get( $page, $parserOptions );
255 } elseif ( $useCache === self::CACHE_SECONDARY && $revision ) {
256 $secondaryCache = $this->getSecondaryCache( $parserOptions );
257 $output = $secondaryCache->get( $revision, $parserOptions );
258 } else {
259 $output = null;
260 }
261
262 $notHitReason = 'miss';
263 if (
264 $output && !( $options & self::OPT_IGNORE_PROFILE_VERSION ) &&
265 $parserOptions->getUseParsoid()
266 ) {
267 $pageBundleData = $output->getExtensionData(
268 PageBundleParserOutputConverter::PARSOID_PAGE_BUNDLE_KEY
269 );
270 // T333606: Force a reparse if the version coming from cache is not the default
271 $cachedVersion = $pageBundleData['version'] ?? null;
272 if (
273 $cachedVersion !== null && // T325137: BadContentModel, no sense in reparsing
274 $cachedVersion !== Parsoid::defaultHTMLVersion()
275 ) {
276 $notHitReason = 'obsolete';
277 $output = null;
278 }
279 }
280
281 if ( $output && !$isOld ) {
282 $this->localCache->setField( $classCacheKey, $page->getLatest(), $output );
283 }
284
285 if ( $output ) {
286 $this->statsDataFactory->increment( "ParserOutputAccess.Cache.$useCache.hit" );
287 } else {
288 $this->statsDataFactory->increment( "ParserOutputAccess.Cache.$useCache.$notHitReason" );
289 }
290
291 return $output ?: null; // convert false to null
292 }
293
316 public function getParserOutput(
317 PageRecord $page,
318 ParserOptions $parserOptions,
319 ?RevisionRecord $revision = null,
320 int $options = 0
321 ): Status {
322 $error = $this->checkPreconditions( $page, $revision, $options );
323 if ( $error ) {
324 $this->statsDataFactory->increment( "ParserOutputAccess.Case.error" );
325 return $error;
326 }
327
328 $isOld = $revision && $revision->getId() !== $page->getLatest();
329 if ( $isOld ) {
330 $this->statsDataFactory->increment( 'ParserOutputAccess.Case.old' );
331 } else {
332 $this->statsDataFactory->increment( 'ParserOutputAccess.Case.current' );
333 }
334
335 if ( !( $options & self::OPT_NO_CHECK_CACHE ) ) {
336 $output = $this->getCachedParserOutput( $page, $parserOptions, $revision );
337 if ( $output ) {
338 return Status::newGood( $output );
339 }
340 }
341
342 if ( !$revision ) {
343 $revId = $page->getLatest();
344 $revision = $revId ? $this->revisionLookup->getRevisionById( $revId ) : null;
345
346 if ( !$revision ) {
347 $this->statsDataFactory->increment( "ParserOutputAccess.Status.norev" );
348 return Status::newFatal( 'missing-revision', $revId );
349 }
350 }
351
352 if ( $options & self::OPT_FOR_ARTICLE_VIEW ) {
353 $work = $this->newPoolWorkArticleView( $page, $parserOptions, $revision, $options );
355 $status = $work->execute();
356 } else {
357 $status = $this->renderRevision( $page, $parserOptions, $revision, $options );
358 }
359
360 $output = $status->getValue();
361 Assert::postcondition( $output || !$status->isOK(), 'Inconsistent status' );
362
363 if ( $output && !$isOld ) {
364 $primaryCache = $this->getPrimaryCache( $parserOptions );
365 $classCacheKey = $primaryCache->makeParserOutputKey( $page, $parserOptions );
366 $this->localCache->setField( $classCacheKey, $page->getLatest(), $output );
367 }
368
369 if ( $status->isGood() ) {
370 $this->statsDataFactory->increment( 'ParserOutputAccess.Status.good' );
371 } elseif ( $status->isOK() ) {
372 $this->statsDataFactory->increment( 'ParserOutputAccess.Status.ok' );
373 } else {
374 $this->statsDataFactory->increment( 'ParserOutputAccess.Status.error' );
375 }
376
377 return $status;
378 }
379
391 private function renderRevision(
392 PageRecord $page,
393 ParserOptions $parserOptions,
394 RevisionRecord $revision,
395 int $options
396 ): Status {
397 $this->statsDataFactory->increment( 'ParserOutputAccess.PoolWork.None' );
398
399 $renderedRev = $this->revisionRenderer->getRenderedRevision(
400 $revision,
401 $parserOptions,
402 null,
403 [ 'audience' => RevisionRecord::RAW ]
404 );
405
406 $output = $renderedRev->getRevisionParserOutput();
407
408 if ( !( $options & self::OPT_NO_UPDATE_CACHE ) && $output->isCacheable() ) {
409 $useCache = $this->shouldUseCache( $page, $revision );
410
411 if ( $useCache === self::CACHE_PRIMARY ) {
412 $primaryCache = $this->getPrimaryCache( $parserOptions );
413 $primaryCache->save( $output, $page, $parserOptions );
414 } elseif ( $useCache === self::CACHE_SECONDARY ) {
415 $secondaryCache = $this->getSecondaryCache( $parserOptions );
416 $secondaryCache->save( $output, $revision, $parserOptions );
417 }
418 }
419
420 if ( $options & self::OPT_LINKS_UPDATE ) {
421 $this->wikiPageFactory->newFromTitle( $page )
422 ->triggerOpportunisticLinksUpdate( $output );
423 }
424
425 return Status::newGood( $output );
426 }
427
435 private function checkPreconditions(
436 PageRecord $page,
437 ?RevisionRecord $revision = null,
438 int $options = 0
439 ): ?Status {
440 if ( !$page->exists() ) {
441 return Status::newFatal( 'nopagetext' );
442 }
443
444 if ( !( $options & self::OPT_NO_UPDATE_CACHE ) && $revision && !$revision->getId() ) {
445 throw new InvalidArgumentException(
446 'The revision does not have a known ID. Use OPT_NO_CACHE.'
447 );
448 }
449
450 if ( $revision && $revision->getPageId() !== $page->getId() ) {
451 throw new InvalidArgumentException(
452 'The revision does not belong to the given page.'
453 );
454 }
455
456 if ( $revision && !( $options & self::OPT_NO_AUDIENCE_CHECK ) ) {
457 // NOTE: If per-user checks are desired, the caller should perform them and
458 // then set OPT_NO_AUDIENCE_CHECK if they passed.
459 if ( !$revision->audienceCan( RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_PUBLIC ) ) {
460 return Status::newFatal(
461 'missing-revision-permission',
462 $revision->getId(),
463 $revision->getTimestamp(),
464 $this->titleFormatter->getPrefixedDBkey( $page )
465 );
466 }
467 }
468
469 return null;
470 }
471
480 protected function newPoolWorkArticleView(
481 PageRecord $page,
482 ParserOptions $parserOptions,
483 RevisionRecord $revision,
484 int $options
485 ): PoolCounterWork {
486 $useCache = $this->shouldUseCache( $page, $revision );
487
488 switch ( $useCache ) {
489 case self::CACHE_PRIMARY:
490 $this->statsDataFactory->increment( 'ParserOutputAccess.PoolWork.Current' );
491 $primaryCache = $this->getPrimaryCache( $parserOptions );
492 $parserCacheMetadata = $primaryCache->getMetadata( $page );
493 $cacheKey = $primaryCache->makeParserOutputKey( $page, $parserOptions,
494 $parserCacheMetadata ? $parserCacheMetadata->getUsedOptions() : null
495 );
496
497 $workKey = $cacheKey . ':revid:' . $revision->getId();
498
500 $workKey,
501 $page,
502 $revision,
503 $parserOptions,
504 $this->revisionRenderer,
505 $primaryCache,
506 $this->lbFactory,
507 $this->chronologyProtector,
508 $this->loggerSpi,
509 $this->wikiPageFactory,
510 !( $options & self::OPT_NO_UPDATE_CACHE ),
511 (bool)( $options & self::OPT_LINKS_UPDATE )
512 );
513
514 case self::CACHE_SECONDARY:
515 $this->statsDataFactory->increment( 'ParserOutputAccess.PoolWork.Old' );
516 $secondaryCache = $this->getSecondaryCache( $parserOptions );
517 $workKey = $secondaryCache->makeParserOutputKey( $revision, $parserOptions );
518 return new PoolWorkArticleViewOld(
519 $workKey,
520 $secondaryCache,
521 $revision,
522 $parserOptions,
523 $this->revisionRenderer,
524 $this->loggerSpi
525 );
526
527 default:
528 $this->statsDataFactory->increment( 'ParserOutputAccess.PoolWork.Uncached' );
529 $secondaryCache = $this->getSecondaryCache( $parserOptions );
530 $workKey = $secondaryCache->makeParserOutputKeyOptionalRevId( $revision, $parserOptions );
531 return new PoolWorkArticleView(
532 $workKey,
533 $revision,
534 $parserOptions,
535 $this->revisionRenderer,
536 $this->loggerSpi
537 );
538 }
539
540 // unreachable
541 }
542
543 private function getPrimaryCache( ParserOptions $pOpts ): ParserCache {
544 if ( $pOpts->getUseParsoid() ) {
545 return $this->parserCacheFactory->getParserCache(
546 self::PARSOID_PCACHE_NAME
547 );
548 }
549
550 return $this->parserCacheFactory->getParserCache(
551 ParserCacheFactory::DEFAULT_NAME
552 );
553 }
554
555 private function getSecondaryCache( ParserOptions $pOpts ): RevisionOutputCache {
556 if ( $pOpts->getUseParsoid() ) {
557 return $this->parserCacheFactory->getRevisionOutputCache(
558 self::PARSOID_RCACHE_NAME
559 );
560 }
561
562 return $this->parserCacheFactory->getRevisionOutputCache(
563 ParserCacheFactory::DEFAULT_RCACHE_NAME
564 );
565 }
566
567}
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.
ParserOutput is a rendering of a Content object or a message.
Provides methods for conversion between PageBundle and ParserOutput TODO: Convert to a trait once we ...
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.
getUseParsoid()
Parsoid-format HTML output, or legacy wikitext parser HTML?
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.