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;
29 use ParserCache;
30 use ParserOptions;
31 use ParserOutput;
32 use PoolCounterWork;
36 use Status;
37 use TitleFormatter;
38 use Wikimedia\Assert\Assert;
40 
51 
55  public const OPT_NO_CHECK_CACHE = 1;
56 
58  public const OPT_FORCE_PARSE = self::OPT_NO_CHECK_CACHE;
59 
63  public const OPT_NO_UPDATE_CACHE = 2;
64 
70  public const OPT_NO_AUDIENCE_CHECK = 4;
71 
76  public const OPT_NO_CACHE = self::OPT_NO_UPDATE_CACHE | self::OPT_NO_CHECK_CACHE;
77 
79  private const CACHE_NONE = 'none';
80 
82  private const CACHE_PRIMARY = 'primary';
83 
85  private const CACHE_SECONDARY = 'secondary';
86 
88  private $primaryCache;
89 
93  private $secondaryCache;
94 
100  private $localCache = [];
101 
103  private $revisionLookup;
104 
106  private $revisionRenderer;
107 
109  private $statsDataFactory;
110 
112  private $lbFactory;
113 
115  private $loggerSpi;
116 
118  private $wikiPageFactory;
119 
121  private $titleFormatter;
122 
134  public function __construct(
135  ParserCache $primaryCache,
136  RevisionOutputCache $secondaryCache,
137  RevisionLookup $revisionLookup,
138  RevisionRenderer $revisionRenderer,
139  IBufferingStatsdDataFactory $statsDataFactory,
140  ILBFactory $lbFactory,
141  LoggerSpi $loggerSpi,
142  WikiPageFactory $wikiPageFactory,
143  TitleFormatter $titleFormatter
144  ) {
145  $this->primaryCache = $primaryCache;
146  $this->secondaryCache = $secondaryCache;
147  $this->revisionLookup = $revisionLookup;
148  $this->revisionRenderer = $revisionRenderer;
149  $this->statsDataFactory = $statsDataFactory;
150  $this->lbFactory = $lbFactory;
151  $this->loggerSpi = $loggerSpi;
152  $this->wikiPageFactory = $wikiPageFactory;
153  $this->titleFormatter = $titleFormatter;
154  }
155 
164  private function shouldUseCache(
165  PageRecord $page,
166  ?RevisionRecord $rev
167  ) {
168  if ( $rev && !$rev->getId() ) {
169  // The revision isn't from the database, so the output can't safely be cached.
170  return self::CACHE_NONE;
171  }
172 
173  // NOTE: Keep in sync with ParserWikiPage::shouldCheckParserCache().
174  // NOTE: when we allow caching of old revisions in the future,
175  // we must not allow caching of deleted revisions.
176 
177  $wikiPage = $this->wikiPageFactory->newFromTitle( $page );
178  if ( !$page->exists() || !$wikiPage->getContentHandler()->isParserCacheSupported() ) {
179  return self::CACHE_NONE;
180  }
181 
182  $isOld = $rev && $rev->getId() !== $page->getLatest();
183  if ( !$isOld ) {
184  return self::CACHE_PRIMARY;
185  }
186 
187  if ( !$rev->audienceCan( RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_PUBLIC ) ) {
188  // deleted/suppressed revision
189  return self::CACHE_NONE;
190  }
191 
192  return self::CACHE_SECONDARY;
193  }
194 
205  public function getCachedParserOutput(
206  PageRecord $page,
207  ParserOptions $parserOptions,
208  ?RevisionRecord $revision = null,
209  int $options = 0
210  ): ?ParserOutput {
211  $isOld = $revision && $revision->getId() !== $page->getLatest();
212  $useCache = $this->shouldUseCache( $page, $revision );
213  $classCacheKey = $this->primaryCache->makeParserOutputKey( $page, $parserOptions );
214 
215  if ( $useCache === self::CACHE_PRIMARY ) {
216  if ( isset( $this->localCache[$classCacheKey] ) && !$isOld ) {
217  return $this->localCache[$classCacheKey];
218  }
219  $output = $this->primaryCache->get( $page, $parserOptions );
220  } elseif ( $useCache === self::CACHE_SECONDARY && $revision ) {
221  $output = $this->secondaryCache->get( $revision, $parserOptions );
222  } else {
223  $output = null;
224  }
225 
226  if ( $output && !$isOld ) {
227  $this->localCache[$classCacheKey] = $output;
228  }
229 
230  if ( $output ) {
231  $this->statsDataFactory->increment( "ParserOutputAccess.Cache.$useCache.hit" );
232  } else {
233  $this->statsDataFactory->increment( "ParserOutputAccess.Cache.$useCache.miss" );
234  }
235 
236  return $output ?: null; // convert false to null
237  }
238 
261  public function getParserOutput(
262  PageRecord $page,
263  ParserOptions $parserOptions,
264  ?RevisionRecord $revision = null,
265  int $options = 0
266  ): Status {
267  $error = $this->checkPreconditions( $page, $revision, $options );
268  if ( $error ) {
269  $this->statsDataFactory->increment( "ParserOutputAccess.Case.error" );
270  return $error;
271  }
272 
273  $isOld = $revision && $revision->getId() !== $page->getLatest();
274  if ( $isOld ) {
275  $this->statsDataFactory->increment( 'ParserOutputAccess.Case.old' );
276  } else {
277  $this->statsDataFactory->increment( 'ParserOutputAccess.Case.current' );
278  }
279 
280  if ( !( $options & self::OPT_NO_CHECK_CACHE ) ) {
281  $output = $this->getCachedParserOutput( $page, $parserOptions, $revision );
282  if ( $output ) {
283  return Status::newGood( $output );
284  }
285  }
286 
287  if ( !$revision ) {
288  $revId = $page->getLatest();
289  $revision = $revId ? $this->revisionLookup->getRevisionById( $revId ) : null;
290 
291  if ( !$revision ) {
292  $this->statsDataFactory->increment( "ParserOutputAccess.Status.norev" );
293  return Status::newFatal( 'missing-revision', $revId );
294  }
295  }
296 
297  $work = $this->newPoolWorkArticleView( $page, $parserOptions, $revision, $options );
299  $status = $work->execute();
300  $output = $status->getValue();
301  Assert::postcondition( $output || !$status->isOK(), 'Worker returned invalid status' );
302 
303  if ( $output && !$isOld ) {
304  $classCacheKey = $this->primaryCache->makeParserOutputKey( $page, $parserOptions );
305  $this->localCache[$classCacheKey] = $output;
306  }
307 
308  if ( $status->isGood() ) {
309  $this->statsDataFactory->increment( 'ParserOutputAccess.Status.good' );
310  } elseif ( $status->isOK() ) {
311  $this->statsDataFactory->increment( 'ParserOutputAccess.Status.ok' );
312  } else {
313  $this->statsDataFactory->increment( 'ParserOutputAccess.Status.error' );
314  }
315 
316  return $status;
317  }
318 
326  private function checkPreconditions(
327  PageRecord $page,
328  ?RevisionRecord $revision = null,
329  int $options = 0
330  ): ?Status {
331  if ( !$page->exists() ) {
332  return Status::newFatal( 'nopagetext' );
333  }
334 
335  if ( !( $options & self::OPT_NO_UPDATE_CACHE ) && $revision && !$revision->getId() ) {
336  throw new InvalidArgumentException(
337  'The revision does not have a known ID. Use NO_CACHE.'
338  );
339  }
340 
341  if ( $revision && $revision->getPageId() !== $page->getId() ) {
342  throw new InvalidArgumentException(
343  'The revision does not belong to the given page.'
344  );
345  }
346 
347  if ( $revision && !( $options & self::OPT_NO_AUDIENCE_CHECK ) ) {
348  // NOTE: If per-user checks are desired, the caller should perform them and
349  // then set OPT_NO_AUDIENCE_CHECK if they passed.
350  if ( !$revision->audienceCan( RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_PUBLIC ) ) {
351  return Status::newFatal(
352  'missing-revision-permission',
353  $revision->getId(),
354  $revision->getTimestamp(),
355  $this->titleFormatter->getPrefixedDBkey( $page )
356  );
357  }
358  }
359 
360  return null;
361  }
362 
371  private function newPoolWorkArticleView(
372  PageRecord $page,
373  ParserOptions $parserOptions,
374  RevisionRecord $revision,
375  int $options
376  ): PoolCounterWork {
377  $useCache = $this->shouldUseCache( $page, $revision );
378 
379  switch ( $useCache ) {
380  case self::CACHE_PRIMARY:
381  $this->statsDataFactory->increment( 'ParserOutputAccess.PoolWork.Current' );
382  $parserCacheMetadata = $this->primaryCache->getMetadata( $page );
383  $cacheKey = $this->primaryCache->makeParserOutputKey( $page, $parserOptions,
384  $parserCacheMetadata ? $parserCacheMetadata->getUsedOptions() : null
385  );
386 
387  $workKey = $cacheKey . ':revid:' . $revision->getId();
388 
389  return new PoolWorkArticleViewCurrent(
390  $workKey,
391  $page,
392  $revision,
393  $parserOptions,
394  $this->revisionRenderer,
395  $this->primaryCache,
396  $this->lbFactory,
397  $this->loggerSpi,
398  $this->wikiPageFactory,
399  !( $options & self::OPT_NO_UPDATE_CACHE )
400  );
401 
402  case self::CACHE_SECONDARY:
403  $this->statsDataFactory->increment( 'ParserOutputAccess.PoolWork.Old' );
404  $workKey = $this->secondaryCache->makeParserOutputKey( $revision, $parserOptions );
405  return new PoolWorkArticleViewOld(
406  $workKey,
407  $this->secondaryCache,
408  $revision,
409  $parserOptions,
410  $this->revisionRenderer,
411  $this->loggerSpi
412  );
413 
414  default:
415  $this->statsDataFactory->increment( 'ParserOutputAccess.PoolWork.Uncached' );
416  $workKey = $this->secondaryCache->makeParserOutputKey( $revision, $parserOptions );
417  return new PoolWorkArticleView(
418  $workKey,
419  $revision,
420  $parserOptions,
421  $this->revisionRenderer,
422  $this->loggerSpi
423  );
424  }
425 
426  // unreachable
427  }
428 
429 }
const CACHE_NONE
Definition: Defines.php:86
if(!defined('MW_SETUP_CALLBACK'))
The persistent session ID (if any) loaded at startup.
Definition: WebStart.php:82
Service for getting rendered output of a given page.
__construct(ParserCache $primaryCache, RevisionOutputCache $secondaryCache, RevisionLookup $revisionLookup, RevisionRenderer $revisionRenderer, IBufferingStatsdDataFactory $statsDataFactory, ILBFactory $lbFactory, 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.
The RevisionRenderer service provides access to rendered output for revisions.
Cache for ParserOutput objects corresponding to the latest page revisions.
Definition: ParserCache.php:63
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:70
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:82
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:44
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:23
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 of IDatabase connections.
Definition: ILBFactory.php:31