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
129 private MapCacheLRU $localCache;
130
131 private ParserCacheFactory $parserCacheFactory;
132 private RevisionLookup $revisionLookup;
133 private RevisionRenderer $revisionRenderer;
134 private IBufferingStatsdDataFactory $statsDataFactory;
135 private ILBFactory $lbFactory;
136 private ChronologyProtector $chronologyProtector;
137 private LoggerSpi $loggerSpi;
138 private WikiPageFactory $wikiPageFactory;
139 private TitleFormatter $titleFormatter;
140
141 public function __construct(
142 ParserCacheFactory $parserCacheFactory,
143 RevisionLookup $revisionLookup,
144 RevisionRenderer $revisionRenderer,
145 IBufferingStatsdDataFactory $statsDataFactory,
146 ILBFactory $lbFactory,
147 ChronologyProtector $chronologyProtector,
148 LoggerSpi $loggerSpi,
149 WikiPageFactory $wikiPageFactory,
150 TitleFormatter $titleFormatter
151 ) {
152 $this->parserCacheFactory = $parserCacheFactory;
153 $this->revisionLookup = $revisionLookup;
154 $this->revisionRenderer = $revisionRenderer;
155 $this->statsDataFactory = $statsDataFactory;
156 $this->lbFactory = $lbFactory;
157 $this->chronologyProtector = $chronologyProtector;
158 $this->loggerSpi = $loggerSpi;
159 $this->wikiPageFactory = $wikiPageFactory;
160 $this->titleFormatter = $titleFormatter;
161
162 $this->localCache = new MapCacheLRU( 10 );
163 }
164
173 private function shouldUseCache(
174 PageRecord $page,
175 ?RevisionRecord $rev
176 ) {
177 if ( $rev && !$rev->getId() ) {
178 // The revision isn't from the database, so the output can't safely be cached.
179 return self::CACHE_NONE;
180 }
181
182 // NOTE: Keep in sync with ParserWikiPage::shouldCheckParserCache().
183 // NOTE: when we allow caching of old revisions in the future,
184 // we must not allow caching of deleted revisions.
185
186 $wikiPage = $this->wikiPageFactory->newFromTitle( $page );
187 if ( !$page->exists() || !$wikiPage->getContentHandler()->isParserCacheSupported() ) {
188 return self::CACHE_NONE;
189 }
190
191 $isOld = $rev && $rev->getId() !== $page->getLatest();
192 if ( !$isOld ) {
193 return self::CACHE_PRIMARY;
194 }
195
196 if ( !$rev->audienceCan( RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_PUBLIC ) ) {
197 // deleted/suppressed revision
198 return self::CACHE_NONE;
199 }
200
201 return self::CACHE_SECONDARY;
202 }
203
214 public function getCachedParserOutput(
215 PageRecord $page,
216 ParserOptions $parserOptions,
217 ?RevisionRecord $revision = null,
218 int $options = 0
219 ): ?ParserOutput {
220 $isOld = $revision && $revision->getId() !== $page->getLatest();
221 $useCache = $this->shouldUseCache( $page, $revision );
222 $primaryCache = $this->getPrimaryCache( $parserOptions );
223 $classCacheKey = $primaryCache->makeParserOutputKey( $page, $parserOptions );
224
225 if ( $useCache === self::CACHE_PRIMARY ) {
226 if ( $this->localCache->hasField( $classCacheKey, $page->getLatest() ) && !$isOld ) {
227 return $this->localCache->getField( $classCacheKey, $page->getLatest() );
228 }
229 $output = $primaryCache->get( $page, $parserOptions );
230 } elseif ( $useCache === self::CACHE_SECONDARY && $revision ) {
231 $secondaryCache = $this->getSecondaryCache( $parserOptions );
232 $output = $secondaryCache->get( $revision, $parserOptions );
233 } else {
234 $output = null;
235 }
236
237 $notHitReason = 'miss';
238 if (
239 $output && !( $options & self::OPT_IGNORE_PROFILE_VERSION ) &&
240 $parserOptions->getUseParsoid()
241 ) {
242 $pageBundleData = $output->getExtensionData(
243 PageBundleParserOutputConverter::PARSOID_PAGE_BUNDLE_KEY
244 );
245 // T333606: Force a reparse if the version coming from cache is not the default
246 $cachedVersion = $pageBundleData['version'] ?? null;
247 if (
248 $cachedVersion !== null && // T325137: BadContentModel, no sense in reparsing
249 $cachedVersion !== Parsoid::defaultHTMLVersion()
250 ) {
251 $notHitReason = 'obsolete';
252 $output = null;
253 }
254 }
255
256 if ( $output && !$isOld ) {
257 $this->localCache->setField( $classCacheKey, $page->getLatest(), $output );
258 }
259
260 if ( $output ) {
261 $this->statsDataFactory->increment( "ParserOutputAccess.Cache.$useCache.hit" );
262 } else {
263 $this->statsDataFactory->increment( "ParserOutputAccess.Cache.$useCache.$notHitReason" );
264 }
265
266 return $output ?: null; // convert false to null
267 }
268
291 public function getParserOutput(
292 PageRecord $page,
293 ParserOptions $parserOptions,
294 ?RevisionRecord $revision = null,
295 int $options = 0
296 ): Status {
297 $error = $this->checkPreconditions( $page, $revision, $options );
298 if ( $error ) {
299 $this->statsDataFactory->increment( "ParserOutputAccess.Case.error" );
300 return $error;
301 }
302
303 $isOld = $revision && $revision->getId() !== $page->getLatest();
304 if ( $isOld ) {
305 $this->statsDataFactory->increment( 'ParserOutputAccess.Case.old' );
306 } else {
307 $this->statsDataFactory->increment( 'ParserOutputAccess.Case.current' );
308 }
309
310 if ( !( $options & self::OPT_NO_CHECK_CACHE ) ) {
311 $output = $this->getCachedParserOutput( $page, $parserOptions, $revision );
312 if ( $output ) {
313 return Status::newGood( $output );
314 }
315 }
316
317 if ( !$revision ) {
318 $revId = $page->getLatest();
319 $revision = $revId ? $this->revisionLookup->getRevisionById( $revId ) : null;
320
321 if ( !$revision ) {
322 $this->statsDataFactory->increment( "ParserOutputAccess.Status.norev" );
323 return Status::newFatal( 'missing-revision', $revId );
324 }
325 }
326
327 if ( $options & self::OPT_FOR_ARTICLE_VIEW ) {
328 $work = $this->newPoolWorkArticleView( $page, $parserOptions, $revision, $options );
330 $status = $work->execute();
331 } else {
332 $status = $this->renderRevision( $page, $parserOptions, $revision, $options );
333 }
334
335 $output = $status->getValue();
336 Assert::postcondition( $output || !$status->isOK(), 'Inconsistent status' );
337
338 if ( $output && !$isOld ) {
339 $primaryCache = $this->getPrimaryCache( $parserOptions );
340 $classCacheKey = $primaryCache->makeParserOutputKey( $page, $parserOptions );
341 $this->localCache->setField( $classCacheKey, $page->getLatest(), $output );
342 }
343
344 if ( $status->isGood() ) {
345 $this->statsDataFactory->increment( 'ParserOutputAccess.Status.good' );
346 } elseif ( $status->isOK() ) {
347 $this->statsDataFactory->increment( 'ParserOutputAccess.Status.ok' );
348 } else {
349 $this->statsDataFactory->increment( 'ParserOutputAccess.Status.error' );
350 }
351
352 return $status;
353 }
354
366 private function renderRevision(
367 PageRecord $page,
368 ParserOptions $parserOptions,
369 RevisionRecord $revision,
370 int $options
371 ): Status {
372 $this->statsDataFactory->increment( 'ParserOutputAccess.PoolWork.None' );
373
374 $renderedRev = $this->revisionRenderer->getRenderedRevision(
375 $revision,
376 $parserOptions,
377 null,
378 [ 'audience' => RevisionRecord::RAW ]
379 );
380
381 $output = $renderedRev->getRevisionParserOutput();
382
383 if ( !( $options & self::OPT_NO_UPDATE_CACHE ) && $output->isCacheable() ) {
384 $useCache = $this->shouldUseCache( $page, $revision );
385
386 if ( $useCache === self::CACHE_PRIMARY ) {
387 $primaryCache = $this->getPrimaryCache( $parserOptions );
388 $primaryCache->save( $output, $page, $parserOptions );
389 } elseif ( $useCache === self::CACHE_SECONDARY ) {
390 $secondaryCache = $this->getSecondaryCache( $parserOptions );
391 $secondaryCache->save( $output, $revision, $parserOptions );
392 }
393 }
394
395 if ( $options & self::OPT_LINKS_UPDATE ) {
396 $this->wikiPageFactory->newFromTitle( $page )
397 ->triggerOpportunisticLinksUpdate( $output );
398 }
399
400 return Status::newGood( $output );
401 }
402
410 private function checkPreconditions(
411 PageRecord $page,
412 ?RevisionRecord $revision = null,
413 int $options = 0
414 ): ?Status {
415 if ( !$page->exists() ) {
416 return Status::newFatal( 'nopagetext' );
417 }
418
419 if ( !( $options & self::OPT_NO_UPDATE_CACHE ) && $revision && !$revision->getId() ) {
420 throw new InvalidArgumentException(
421 'The revision does not have a known ID. Use OPT_NO_CACHE.'
422 );
423 }
424
425 if ( $revision && $revision->getPageId() !== $page->getId() ) {
426 throw new InvalidArgumentException(
427 'The revision does not belong to the given page.'
428 );
429 }
430
431 if ( $revision && !( $options & self::OPT_NO_AUDIENCE_CHECK ) ) {
432 // NOTE: If per-user checks are desired, the caller should perform them and
433 // then set OPT_NO_AUDIENCE_CHECK if they passed.
434 if ( !$revision->audienceCan( RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_PUBLIC ) ) {
435 return Status::newFatal(
436 'missing-revision-permission',
437 $revision->getId(),
438 $revision->getTimestamp(),
439 $this->titleFormatter->getPrefixedDBkey( $page )
440 );
441 }
442 }
443
444 return null;
445 }
446
455 protected function newPoolWorkArticleView(
456 PageRecord $page,
457 ParserOptions $parserOptions,
458 RevisionRecord $revision,
459 int $options
460 ): PoolCounterWork {
461 $useCache = $this->shouldUseCache( $page, $revision );
462
463 switch ( $useCache ) {
464 case self::CACHE_PRIMARY:
465 $this->statsDataFactory->increment( 'ParserOutputAccess.PoolWork.Current' );
466 $primaryCache = $this->getPrimaryCache( $parserOptions );
467 $parserCacheMetadata = $primaryCache->getMetadata( $page );
468 $cacheKey = $primaryCache->makeParserOutputKey( $page, $parserOptions,
469 $parserCacheMetadata ? $parserCacheMetadata->getUsedOptions() : null
470 );
471
472 $workKey = $cacheKey . ':revid:' . $revision->getId();
473
475 $workKey,
476 $page,
477 $revision,
478 $parserOptions,
479 $this->revisionRenderer,
480 $primaryCache,
481 $this->lbFactory,
482 $this->chronologyProtector,
483 $this->loggerSpi,
484 $this->wikiPageFactory,
485 !( $options & self::OPT_NO_UPDATE_CACHE ),
486 (bool)( $options & self::OPT_LINKS_UPDATE )
487 );
488
489 case self::CACHE_SECONDARY:
490 $this->statsDataFactory->increment( 'ParserOutputAccess.PoolWork.Old' );
491 $secondaryCache = $this->getSecondaryCache( $parserOptions );
492 $workKey = $secondaryCache->makeParserOutputKey( $revision, $parserOptions );
493 return new PoolWorkArticleViewOld(
494 $workKey,
495 $secondaryCache,
496 $revision,
497 $parserOptions,
498 $this->revisionRenderer,
499 $this->loggerSpi
500 );
501
502 default:
503 $this->statsDataFactory->increment( 'ParserOutputAccess.PoolWork.Uncached' );
504 $secondaryCache = $this->getSecondaryCache( $parserOptions );
505 $workKey = $secondaryCache->makeParserOutputKeyOptionalRevId( $revision, $parserOptions );
506 return new PoolWorkArticleView(
507 $workKey,
508 $revision,
509 $parserOptions,
510 $this->revisionRenderer,
511 $this->loggerSpi
512 );
513 }
514
515 // unreachable
516 }
517
518 private function getPrimaryCache( ParserOptions $pOpts ): ParserCache {
519 if ( $pOpts->getUseParsoid() ) {
520 return $this->parserCacheFactory->getParserCache(
521 self::PARSOID_PCACHE_NAME
522 );
523 }
524
525 return $this->parserCacheFactory->getParserCache(
526 ParserCacheFactory::DEFAULT_NAME
527 );
528 }
529
530 private function getSecondaryCache( ParserOptions $pOpts ): RevisionOutputCache {
531 if ( $pOpts->getUseParsoid() ) {
532 return $this->parserCacheFactory->getRevisionOutputCache(
533 self::PARSOID_RCACHE_NAME
534 );
535 }
536
537 return $this->parserCacheFactory->getRevisionOutputCache(
538 ParserCacheFactory::DEFAULT_RCACHE_NAME
539 );
540 }
541
542}
const CACHE_NONE
Definition Defines.php:87
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.