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 
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->makeParserOutputKeyOptionalRevId( $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:88
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.
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.
Cache for ParserOutput objects corresponding to the latest page revisions.
Definition: ParserCache.php:64
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:73
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:85
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:46
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:24
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.
Definition: ILBFactory.php:46