MediaWiki 1.41.2
ParserOutputAccess.php
Go to the documentation of this file.
1<?php
20namespace MediaWiki\Page;
21
23use InvalidArgumentException;
24use MediaWiki\Logger\Spi as LoggerSpi;
32use ParserCache;
34use ParserOutput;
39use 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
190 if ( !$rev->audienceCan( RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_PUBLIC ) ) {
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][$page->getLatest()] ) && !$isOld ) {
221 return $this->localCache[$classCacheKey][$page->getLatest()];
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] = [ $page->getLatest() => $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] = [ $page->getLatest() => $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
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.
Set options of the Parser.
Rendered output of a wiki page, as parsed from wikitext.
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.
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.
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.