Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
81.95% |
227 / 277 |
|
60.00% |
6 / 10 |
CRAP | |
0.00% |
0 / 1 |
ParserOutputAccess | |
81.95% |
227 / 277 |
|
60.00% |
6 / 10 |
113.78 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
1 | |||
shouldUseCache | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
8 | |||
getCachedParserOutput | |
78.57% |
33 / 42 |
|
0.00% |
0 / 1 |
18.52 | |||
getParserOutput | |
100.00% |
60 / 60 |
|
100.00% |
1 / 1 |
15 | |||
renderRevision | |
66.04% |
35 / 53 |
|
0.00% |
0 / 1 |
26.03 | |||
checkPreconditions | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
10 | |||
newPoolWorkArticleView | |
75.93% |
41 / 54 |
|
0.00% |
0 / 1 |
5.35 | |||
getPrimaryCache | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
getSecondaryCache | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
startOperationSpan | |
23.08% |
3 / 13 |
|
0.00% |
0 / 1 |
7.10 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | namespace MediaWiki\Page; |
21 | |
22 | use InvalidArgumentException; |
23 | use MapCacheLRU; |
24 | use MediaWiki\Logger\Spi as LoggerSpi; |
25 | use MediaWiki\MainConfigNames; |
26 | use MediaWiki\MediaWikiServices; |
27 | use MediaWiki\Parser\ParserCache; |
28 | use MediaWiki\Parser\ParserCacheFactory; |
29 | use MediaWiki\Parser\ParserOptions; |
30 | use MediaWiki\Parser\ParserOutput; |
31 | use MediaWiki\Parser\Parsoid\PageBundleParserOutputConverter; |
32 | use MediaWiki\Parser\RevisionOutputCache; |
33 | use MediaWiki\PoolCounter\PoolCounterWork; |
34 | use MediaWiki\PoolCounter\PoolWorkArticleView; |
35 | use MediaWiki\PoolCounter\PoolWorkArticleViewCurrent; |
36 | use MediaWiki\PoolCounter\PoolWorkArticleViewOld; |
37 | use MediaWiki\Revision\RevisionLookup; |
38 | use MediaWiki\Revision\RevisionRecord; |
39 | use MediaWiki\Revision\RevisionRenderer; |
40 | use MediaWiki\Revision\SlotRecord; |
41 | use MediaWiki\Status\Status; |
42 | use MediaWiki\Title\TitleFormatter; |
43 | use MediaWiki\WikiMap\WikiMap; |
44 | use Wikimedia\Assert\Assert; |
45 | use Wikimedia\Parsoid\Parsoid; |
46 | use Wikimedia\Rdbms\ChronologyProtector; |
47 | use Wikimedia\Rdbms\ILBFactory; |
48 | use Wikimedia\Stats\StatsFactory; |
49 | use Wikimedia\Telemetry\SpanInterface; |
50 | use Wikimedia\Telemetry\TracerInterface; |
51 | |
52 | /** |
53 | * Service for getting rendered output of a given page. |
54 | * |
55 | * This is a high level service, encapsulating concerns like caching |
56 | * and stampede protection via PoolCounter. |
57 | * |
58 | * @since 1.36 |
59 | * @ingroup Page |
60 | */ |
61 | class ParserOutputAccess { |
62 | |
63 | /** @internal */ |
64 | public const PARSOID_PCACHE_NAME = 'parsoid-' . ParserCacheFactory::DEFAULT_NAME; |
65 | |
66 | /** @internal */ |
67 | public const PARSOID_RCACHE_NAME = 'parsoid-' . ParserCacheFactory::DEFAULT_RCACHE_NAME; |
68 | |
69 | /** |
70 | * @var int Do not check the cache before parsing (force parse) |
71 | */ |
72 | public const OPT_NO_CHECK_CACHE = 1; |
73 | |
74 | /** @var int Alias for NO_CHECK_CACHE */ |
75 | public const OPT_FORCE_PARSE = self::OPT_NO_CHECK_CACHE; |
76 | |
77 | /** |
78 | * @var int Do not update the cache after parsing. |
79 | */ |
80 | public const OPT_NO_UPDATE_CACHE = 2; |
81 | |
82 | /** |
83 | * @var int Bypass audience check for deleted/suppressed revisions. |
84 | * The caller is responsible for ensuring that unauthorized access is prevented. |
85 | * If not set, output generation will fail if the revision is not public. |
86 | */ |
87 | public const OPT_NO_AUDIENCE_CHECK = 4; |
88 | |
89 | /** |
90 | * @var int Do not check the cache before parsing, |
91 | * and do not update the cache after parsing (not cacheable). |
92 | */ |
93 | public const OPT_NO_CACHE = self::OPT_NO_UPDATE_CACHE | self::OPT_NO_CHECK_CACHE; |
94 | |
95 | /** |
96 | * @var int Do perform an opportunistic LinksUpdate on cache miss |
97 | * @since 1.41 |
98 | */ |
99 | public const OPT_LINKS_UPDATE = 8; |
100 | |
101 | /** |
102 | * Apply page view semantics. This relaxes some guarantees, specifically: |
103 | * - Use PoolCounter for stampede protection, causing the request to |
104 | * block until another process has finished rendering the content. |
105 | * - Allow stale parser output to be returned to prevent long waits for |
106 | * slow renders. |
107 | * - Allow cacheable placeholder output to be returned when PoolCounter |
108 | * fails to obtain a lock. See the PoolCounterConf setting for details. |
109 | * |
110 | * @see Bug T352837 |
111 | * @since 1.42 |
112 | */ |
113 | public const OPT_FOR_ARTICLE_VIEW = 16; |
114 | |
115 | /** |
116 | * @var int Ignore the profile version of the result from the cache. |
117 | * Otherwise, if it's not Parsoid's default, it will be invalidated. |
118 | */ |
119 | public const OPT_IGNORE_PROFILE_VERSION = 128; |
120 | |
121 | /** @var string Do not read or write any cache */ |
122 | private const CACHE_NONE = 'none'; |
123 | |
124 | /** @var string Use primary cache */ |
125 | private const CACHE_PRIMARY = 'primary'; |
126 | |
127 | /** @var string Use secondary cache */ |
128 | private const CACHE_SECONDARY = 'secondary'; |
129 | |
130 | /** |
131 | * In cases that an extension tries to get the same ParserOutput of |
132 | * the page right after it was parsed (T301310). |
133 | * @var MapCacheLRU<string,ParserOutput> |
134 | */ |
135 | private MapCacheLRU $localCache; |
136 | |
137 | private ParserCacheFactory $parserCacheFactory; |
138 | private RevisionLookup $revisionLookup; |
139 | private RevisionRenderer $revisionRenderer; |
140 | private StatsFactory $statsFactory; |
141 | private ILBFactory $lbFactory; |
142 | private ChronologyProtector $chronologyProtector; |
143 | private LoggerSpi $loggerSpi; |
144 | private WikiPageFactory $wikiPageFactory; |
145 | private TitleFormatter $titleFormatter; |
146 | private TracerInterface $tracer; |
147 | |
148 | public function __construct( |
149 | ParserCacheFactory $parserCacheFactory, |
150 | RevisionLookup $revisionLookup, |
151 | RevisionRenderer $revisionRenderer, |
152 | StatsFactory $statsFactory, |
153 | ILBFactory $lbFactory, |
154 | ChronologyProtector $chronologyProtector, |
155 | LoggerSpi $loggerSpi, |
156 | WikiPageFactory $wikiPageFactory, |
157 | TitleFormatter $titleFormatter, |
158 | TracerInterface $tracer |
159 | ) { |
160 | $this->parserCacheFactory = $parserCacheFactory; |
161 | $this->revisionLookup = $revisionLookup; |
162 | $this->revisionRenderer = $revisionRenderer; |
163 | $this->statsFactory = $statsFactory; |
164 | $this->lbFactory = $lbFactory; |
165 | $this->chronologyProtector = $chronologyProtector; |
166 | $this->loggerSpi = $loggerSpi; |
167 | $this->wikiPageFactory = $wikiPageFactory; |
168 | $this->titleFormatter = $titleFormatter; |
169 | $this->tracer = $tracer; |
170 | |
171 | $this->localCache = new MapCacheLRU( 10 ); |
172 | } |
173 | |
174 | /** |
175 | * Use a cache? |
176 | * |
177 | * @param PageRecord $page |
178 | * @param RevisionRecord|null $rev |
179 | * |
180 | * @return string One of the CACHE_XXX constants. |
181 | */ |
182 | private function shouldUseCache( |
183 | PageRecord $page, |
184 | ?RevisionRecord $rev |
185 | ) { |
186 | if ( $rev && !$rev->getId() ) { |
187 | // The revision isn't from the database, so the output can't safely be cached. |
188 | return self::CACHE_NONE; |
189 | } |
190 | |
191 | // NOTE: Keep in sync with ParserWikiPage::shouldCheckParserCache(). |
192 | // NOTE: when we allow caching of old revisions in the future, |
193 | // we must not allow caching of deleted revisions. |
194 | |
195 | $wikiPage = $this->wikiPageFactory->newFromTitle( $page ); |
196 | if ( !$page->exists() || !$wikiPage->getContentHandler()->isParserCacheSupported() ) { |
197 | return self::CACHE_NONE; |
198 | } |
199 | |
200 | $isOld = $rev && $rev->getId() !== $page->getLatest(); |
201 | if ( !$isOld ) { |
202 | return self::CACHE_PRIMARY; |
203 | } |
204 | |
205 | if ( !$rev->audienceCan( RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_PUBLIC ) ) { |
206 | // deleted/suppressed revision |
207 | return self::CACHE_NONE; |
208 | } |
209 | |
210 | return self::CACHE_SECONDARY; |
211 | } |
212 | |
213 | /** |
214 | * Returns the rendered output for the given page if it is present in the cache. |
215 | * |
216 | * @param PageRecord $page |
217 | * @param ParserOptions $parserOptions |
218 | * @param RevisionRecord|null $revision |
219 | * @param int $options Bitfield using the OPT_XXX constants |
220 | * |
221 | * @return ParserOutput|null |
222 | */ |
223 | public function getCachedParserOutput( |
224 | PageRecord $page, |
225 | ParserOptions $parserOptions, |
226 | ?RevisionRecord $revision = null, |
227 | int $options = 0 |
228 | ): ?ParserOutput { |
229 | $span = $this->startOperationSpan( __FUNCTION__, $page, $revision ); |
230 | $isOld = $revision && $revision->getId() !== $page->getLatest(); |
231 | $useCache = $this->shouldUseCache( $page, $revision ); |
232 | $primaryCache = $this->getPrimaryCache( $parserOptions ); |
233 | $classCacheKey = $primaryCache->makeParserOutputKey( $page, $parserOptions ); |
234 | |
235 | if ( $useCache === self::CACHE_PRIMARY ) { |
236 | if ( $this->localCache->hasField( $classCacheKey, $page->getLatest() ) && !$isOld ) { |
237 | return $this->localCache->getField( $classCacheKey, $page->getLatest() ); |
238 | } |
239 | $output = $primaryCache->get( $page, $parserOptions ); |
240 | } elseif ( $useCache === self::CACHE_SECONDARY && $revision ) { |
241 | $secondaryCache = $this->getSecondaryCache( $parserOptions ); |
242 | $output = $secondaryCache->get( $revision, $parserOptions ); |
243 | } else { |
244 | $output = null; |
245 | } |
246 | |
247 | $notHitReason = 'miss'; |
248 | if ( |
249 | $output && !( $options & self::OPT_IGNORE_PROFILE_VERSION ) && |
250 | $parserOptions->getUseParsoid() |
251 | ) { |
252 | $pageBundleData = $output->getExtensionData( |
253 | PageBundleParserOutputConverter::PARSOID_PAGE_BUNDLE_KEY |
254 | ); |
255 | // T333606: Force a reparse if the version coming from cache is not the default |
256 | $cachedVersion = $pageBundleData['version'] ?? null; |
257 | if ( |
258 | $cachedVersion !== null && // T325137: BadContentModel, no sense in reparsing |
259 | $cachedVersion !== Parsoid::defaultHTMLVersion() |
260 | ) { |
261 | $notHitReason = 'obsolete'; |
262 | $output = null; |
263 | } |
264 | } |
265 | |
266 | if ( $output && !$isOld ) { |
267 | $this->localCache->setField( $classCacheKey, $page->getLatest(), $output ); |
268 | } |
269 | |
270 | if ( $output ) { |
271 | $this->statsFactory |
272 | ->getCounter( 'parseroutputaccess_cache' ) |
273 | ->setLabel( 'cache', $useCache ) |
274 | ->setLabel( 'reason', 'hit' ) |
275 | ->setLabel( 'type', 'hit' ) |
276 | ->copyToStatsdAt( "ParserOutputAccess.Cache.$useCache.hit" ) |
277 | ->increment(); |
278 | } else { |
279 | $this->statsFactory |
280 | ->getCounter( 'parseroutputaccess_cache' ) |
281 | ->setLabel( 'reason', $notHitReason ) |
282 | ->setLabel( 'cache', $useCache ) |
283 | ->setLabel( 'type', 'miss' ) |
284 | ->copyToStatsdAt( "ParserOutputAccess.Cache.$useCache.$notHitReason" ) |
285 | ->increment(); |
286 | } |
287 | |
288 | return $output ?: null; // convert false to null |
289 | } |
290 | |
291 | /** |
292 | * Returns the rendered output for the given page. |
293 | * Caching and concurrency control is applied. |
294 | * |
295 | * @param PageRecord $page |
296 | * @param ParserOptions $parserOptions |
297 | * @param RevisionRecord|null $revision |
298 | * @param int $options Bitfield using the OPT_XXX constants |
299 | * |
300 | * @return Status containing a ParserOutput if no error occurred. |
301 | * Well known errors and warnings include the following messages: |
302 | * - 'view-pool-dirty-output' (warning) The output is dirty (from a stale cache entry). |
303 | * - 'view-pool-contention' (warning) Dirty output was returned immediately instead of |
304 | * waiting to acquire a work lock (when "fast stale" mode is enabled in PoolCounter). |
305 | * - 'view-pool-timeout' (warning) Dirty output was returned after failing to acquire |
306 | * a work lock (got QUEUE_FULL or TIMEOUT from PoolCounter). |
307 | * - 'pool-queuefull' (error) unable to acquire work lock, and no cached content found. |
308 | * - 'pool-timeout' (error) unable to acquire work lock, and no cached content found. |
309 | * - 'pool-servererror' (error) PoolCounterWork failed due to a lock service error. |
310 | * - 'pool-unknownerror' (error) PoolCounterWork failed for an unknown reason. |
311 | * - 'nopagetext' (error) The page does not exist |
312 | */ |
313 | public function getParserOutput( |
314 | PageRecord $page, |
315 | ParserOptions $parserOptions, |
316 | ?RevisionRecord $revision = null, |
317 | int $options = 0 |
318 | ): Status { |
319 | $span = $this->startOperationSpan( __FUNCTION__, $page, $revision ); |
320 | $error = $this->checkPreconditions( $page, $revision, $options ); |
321 | if ( $error ) { |
322 | $this->statsFactory |
323 | ->getCounter( 'parseroutputaccess_case' ) |
324 | ->setLabel( 'case', 'error' ) |
325 | ->copyToStatsdAt( 'ParserOutputAccess.Case.error' ) |
326 | ->increment(); |
327 | return $error; |
328 | } |
329 | |
330 | $isOld = $revision && $revision->getId() !== $page->getLatest(); |
331 | if ( $isOld ) { |
332 | $this->statsFactory |
333 | ->getCounter( 'parseroutputaccess_case' ) |
334 | ->setLabel( 'case', 'old' ) |
335 | ->copyToStatsdAt( 'ParserOutputAccess.Case.old' ) |
336 | ->increment(); |
337 | } else { |
338 | $this->statsFactory |
339 | ->getCounter( 'parseroutputaccess_case' ) |
340 | ->setLabel( 'case', 'current' ) |
341 | ->copyToStatsdAt( 'ParserOutputAccess.Case.current' ) |
342 | ->increment(); |
343 | } |
344 | |
345 | if ( !( $options & self::OPT_NO_CHECK_CACHE ) ) { |
346 | $output = $this->getCachedParserOutput( $page, $parserOptions, $revision ); |
347 | if ( $output ) { |
348 | return Status::newGood( $output ); |
349 | } |
350 | } |
351 | |
352 | if ( !$revision ) { |
353 | $revId = $page->getLatest(); |
354 | $revision = $revId ? $this->revisionLookup->getRevisionById( $revId ) : null; |
355 | |
356 | if ( !$revision ) { |
357 | $this->statsFactory |
358 | ->getCounter( 'parseroutputaccess_status' ) |
359 | ->setLabel( 'status', 'norev' ) |
360 | ->copyToStatsdAt( "ParserOutputAccess.Status.norev" ) |
361 | ->increment(); |
362 | return Status::newFatal( 'missing-revision', $revId ); |
363 | } |
364 | } |
365 | |
366 | if ( $options & self::OPT_FOR_ARTICLE_VIEW ) { |
367 | $work = $this->newPoolWorkArticleView( $page, $parserOptions, $revision, $options ); |
368 | /** @var Status $status */ |
369 | $status = $work->execute(); |
370 | } else { |
371 | // XXX: we could try harder to reuse a cache lookup above to |
372 | // provide the $previous argument here |
373 | $status = $this->renderRevision( $page, $parserOptions, $revision, $options, null ); |
374 | } |
375 | |
376 | $output = $status->getValue(); |
377 | Assert::postcondition( $output || !$status->isOK(), 'Inconsistent status' ); |
378 | |
379 | if ( $output && !$isOld ) { |
380 | $primaryCache = $this->getPrimaryCache( $parserOptions ); |
381 | $classCacheKey = $primaryCache->makeParserOutputKey( $page, $parserOptions ); |
382 | $this->localCache->setField( $classCacheKey, $page->getLatest(), $output ); |
383 | } |
384 | |
385 | if ( $status->isGood() ) { |
386 | $this->statsFactory->getCounter( 'parseroutputaccess_status' ) |
387 | ->setLabel( 'status', 'good' ) |
388 | ->copyToStatsdAt( 'ParserOutputAccess.Status.good' ) |
389 | ->increment(); |
390 | } elseif ( $status->isOK() ) { |
391 | $this->statsFactory->getCounter( 'parseroutputaccess_status' ) |
392 | ->setLabel( 'status', 'ok' ) |
393 | ->copyToStatsdAt( 'ParserOutputAccess.Status.ok' ) |
394 | ->increment(); |
395 | } else { |
396 | $this->statsFactory->getCounter( 'parseroutputaccess_status' ) |
397 | ->setLabel( 'status', 'error' ) |
398 | ->copyToStatsdAt( 'ParserOutputAccess.Status.error' ) |
399 | ->increment(); |
400 | } |
401 | |
402 | return $status; |
403 | } |
404 | |
405 | /** |
406 | * Render the given revision. |
407 | * |
408 | * This method will update the parser cache if appropriate, and will |
409 | * trigger a links update if OPT_LINKS_UPDATE is set. |
410 | * |
411 | * This method does not perform access checks, and will not load content |
412 | * from caches. The caller is assumed to have taken care of that. |
413 | * |
414 | * Where possible, pass in a $previousOutput, which will prevent an |
415 | * unnecessary double-lookup in the cache. |
416 | * |
417 | * @see PoolWorkArticleView::renderRevision |
418 | */ |
419 | private function renderRevision( |
420 | PageRecord $page, |
421 | ParserOptions $parserOptions, |
422 | RevisionRecord $revision, |
423 | int $options, |
424 | ?ParserOutput $previousOutput = null |
425 | ): Status { |
426 | $span = $this->startOperationSpan( __FUNCTION__, $page, $revision ); |
427 | $this->statsFactory->getCounter( 'parseroutputaccess_poolwork' ) |
428 | ->copyToStatsdAt( 'ParserOutputAccess.PoolWork.None' ) |
429 | ->setLabel( 'cache', self::CACHE_NONE ) |
430 | ->increment(); |
431 | |
432 | $useCache = $this->shouldUseCache( $page, $revision ); |
433 | |
434 | // T371713: Temporary statistics collection code to determine |
435 | // feasibility of Parsoid selective update |
436 | $sampleRate = MediaWikiServices::getInstance()->getMainConfig()->get( |
437 | MainConfigNames::ParsoidSelectiveUpdateSampleRate |
438 | ); |
439 | $doSample = ( $sampleRate && mt_rand( 1, $sampleRate ) === 1 ); |
440 | |
441 | if ( $previousOutput === null && ( $doSample || $parserOptions->getUseParsoid() ) ) { |
442 | // If $useCache === self::CACHE_SECONDARY we could potentially |
443 | // try to reuse the parse of $revision-1 from the secondary cache, |
444 | // but it is likely those template transclusions are out of date. |
445 | // Try to reuse the template transclusions from the most recent |
446 | // parse, which are more likely to reflect the current template. |
447 | if ( !( $options & self::OPT_NO_CHECK_CACHE ) ) { |
448 | $previousOutput = $this->getPrimaryCache( $parserOptions )->getDirty( $page, $parserOptions ) ?: null; |
449 | } |
450 | } |
451 | |
452 | $renderedRev = $this->revisionRenderer->getRenderedRevision( |
453 | $revision, |
454 | $parserOptions, |
455 | null, |
456 | [ |
457 | 'audience' => RevisionRecord::RAW, |
458 | 'previous-output' => $previousOutput, |
459 | ] |
460 | ); |
461 | |
462 | $output = $renderedRev->getRevisionParserOutput(); |
463 | |
464 | if ( $doSample ) { |
465 | $content = $revision->getContent( SlotRecord::MAIN ); |
466 | $labels = [ |
467 | 'source' => 'ParserOutputAccess', |
468 | 'type' => $previousOutput === null ? 'full' : 'selective', |
469 | 'reason' => $parserOptions->getRenderReason(), |
470 | 'parser' => $parserOptions->getUseParsoid() ? 'parsoid' : 'legacy', |
471 | 'opportunistic' => 'false', |
472 | 'wiki' => WikiMap::getCurrentWikiId(), |
473 | 'model' => $content ? $content->getModel() : 'unknown', |
474 | ]; |
475 | $this->statsFactory |
476 | ->getCounter( 'ParserCache_selective_total' ) |
477 | ->setLabels( $labels ) |
478 | ->increment(); |
479 | $this->statsFactory |
480 | ->getCounter( 'ParserCache_selective_cpu_seconds' ) |
481 | ->setLabels( $labels ) |
482 | ->incrementBy( $output->getTimeProfile( 'cpu' ) ); |
483 | } |
484 | |
485 | if ( !( $options & self::OPT_NO_UPDATE_CACHE ) && $output->isCacheable() ) { |
486 | if ( $useCache === self::CACHE_PRIMARY ) { |
487 | $primaryCache = $this->getPrimaryCache( $parserOptions ); |
488 | $primaryCache->save( $output, $page, $parserOptions ); |
489 | } elseif ( $useCache === self::CACHE_SECONDARY ) { |
490 | $secondaryCache = $this->getSecondaryCache( $parserOptions ); |
491 | $secondaryCache->save( $output, $revision, $parserOptions ); |
492 | } |
493 | } |
494 | |
495 | if ( $options & self::OPT_LINKS_UPDATE ) { |
496 | $this->wikiPageFactory->newFromTitle( $page ) |
497 | ->triggerOpportunisticLinksUpdate( $output ); |
498 | } |
499 | |
500 | return Status::newGood( $output ); |
501 | } |
502 | |
503 | /** |
504 | * @param PageRecord $page |
505 | * @param RevisionRecord|null $revision |
506 | * @param int $options |
507 | * |
508 | * @return Status|null |
509 | */ |
510 | private function checkPreconditions( |
511 | PageRecord $page, |
512 | ?RevisionRecord $revision = null, |
513 | int $options = 0 |
514 | ): ?Status { |
515 | if ( !$page->exists() ) { |
516 | return Status::newFatal( 'nopagetext' ); |
517 | } |
518 | |
519 | if ( !( $options & self::OPT_NO_UPDATE_CACHE ) && $revision && !$revision->getId() ) { |
520 | throw new InvalidArgumentException( |
521 | 'The revision does not have a known ID. Use OPT_NO_CACHE.' |
522 | ); |
523 | } |
524 | |
525 | if ( $revision && $revision->getPageId() !== $page->getId() ) { |
526 | throw new InvalidArgumentException( |
527 | 'The revision does not belong to the given page.' |
528 | ); |
529 | } |
530 | |
531 | if ( $revision && !( $options & self::OPT_NO_AUDIENCE_CHECK ) ) { |
532 | // NOTE: If per-user checks are desired, the caller should perform them and |
533 | // then set OPT_NO_AUDIENCE_CHECK if they passed. |
534 | if ( !$revision->audienceCan( RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_PUBLIC ) ) { |
535 | return Status::newFatal( |
536 | 'missing-revision-permission', |
537 | $revision->getId(), |
538 | $revision->getTimestamp(), |
539 | $this->titleFormatter->getPrefixedDBkey( $page ) |
540 | ); |
541 | } |
542 | } |
543 | |
544 | return null; |
545 | } |
546 | |
547 | /** |
548 | * @param PageRecord $page |
549 | * @param ParserOptions $parserOptions |
550 | * @param RevisionRecord $revision |
551 | * @param int $options |
552 | * |
553 | * @return PoolCounterWork |
554 | */ |
555 | protected function newPoolWorkArticleView( |
556 | PageRecord $page, |
557 | ParserOptions $parserOptions, |
558 | RevisionRecord $revision, |
559 | int $options |
560 | ): PoolCounterWork { |
561 | $useCache = $this->shouldUseCache( $page, $revision ); |
562 | |
563 | switch ( $useCache ) { |
564 | case self::CACHE_PRIMARY: |
565 | $this->statsFactory->getCounter( 'parseroutputaccess_poolwork' ) |
566 | ->setLabel( 'cache', self::CACHE_PRIMARY ) |
567 | ->copyToStatsdAt( 'ParserOutputAccess.PoolWork.Current' ) |
568 | ->increment(); |
569 | $primaryCache = $this->getPrimaryCache( $parserOptions ); |
570 | $parserCacheMetadata = $primaryCache->getMetadata( $page ); |
571 | $cacheKey = $primaryCache->makeParserOutputKey( $page, $parserOptions, |
572 | $parserCacheMetadata ? $parserCacheMetadata->getUsedOptions() : null |
573 | ); |
574 | |
575 | $workKey = $cacheKey . ':revid:' . $revision->getId(); |
576 | |
577 | return new PoolWorkArticleViewCurrent( |
578 | $workKey, |
579 | $page, |
580 | $revision, |
581 | $parserOptions, |
582 | $this->revisionRenderer, |
583 | $primaryCache, |
584 | $this->lbFactory, |
585 | $this->chronologyProtector, |
586 | $this->loggerSpi, |
587 | $this->wikiPageFactory, |
588 | !( $options & self::OPT_NO_UPDATE_CACHE ), |
589 | (bool)( $options & self::OPT_LINKS_UPDATE ) |
590 | ); |
591 | |
592 | case self::CACHE_SECONDARY: |
593 | $this->statsFactory->getCounter( 'parseroutputaccess_poolwork' ) |
594 | ->setLabel( 'cache', self::CACHE_SECONDARY ) |
595 | ->copyToStatsdAt( 'ParserOutputAccess.PoolWork.Old' ) |
596 | ->increment(); |
597 | $secondaryCache = $this->getSecondaryCache( $parserOptions ); |
598 | $workKey = $secondaryCache->makeParserOutputKey( $revision, $parserOptions ); |
599 | return new PoolWorkArticleViewOld( |
600 | $workKey, |
601 | $secondaryCache, |
602 | $revision, |
603 | $parserOptions, |
604 | $this->revisionRenderer, |
605 | $this->loggerSpi |
606 | ); |
607 | |
608 | default: |
609 | $this->statsFactory->getCounter( 'parseroutputaccess_poolwork' ) |
610 | ->setLabel( 'cache', self::CACHE_NONE ) |
611 | ->copyToStatsdAt( 'ParserOutputAccess.PoolWork.Uncached' ) |
612 | ->increment(); |
613 | $secondaryCache = $this->getSecondaryCache( $parserOptions ); |
614 | $workKey = $secondaryCache->makeParserOutputKeyOptionalRevId( $revision, $parserOptions ); |
615 | return new PoolWorkArticleView( |
616 | $workKey, |
617 | $revision, |
618 | $parserOptions, |
619 | $this->revisionRenderer, |
620 | $this->loggerSpi |
621 | ); |
622 | } |
623 | |
624 | // unreachable |
625 | } |
626 | |
627 | private function getPrimaryCache( ParserOptions $pOpts ): ParserCache { |
628 | if ( $pOpts->getUseParsoid() ) { |
629 | return $this->parserCacheFactory->getParserCache( |
630 | self::PARSOID_PCACHE_NAME |
631 | ); |
632 | } |
633 | |
634 | return $this->parserCacheFactory->getParserCache( |
635 | ParserCacheFactory::DEFAULT_NAME |
636 | ); |
637 | } |
638 | |
639 | private function getSecondaryCache( ParserOptions $pOpts ): RevisionOutputCache { |
640 | if ( $pOpts->getUseParsoid() ) { |
641 | return $this->parserCacheFactory->getRevisionOutputCache( |
642 | self::PARSOID_RCACHE_NAME |
643 | ); |
644 | } |
645 | |
646 | return $this->parserCacheFactory->getRevisionOutputCache( |
647 | ParserCacheFactory::DEFAULT_RCACHE_NAME |
648 | ); |
649 | } |
650 | |
651 | private function startOperationSpan( |
652 | string $opName, |
653 | PageRecord $page, |
654 | ?RevisionRecord $revision = null |
655 | ): SpanInterface { |
656 | $span = $this->tracer->createSpan( "ParserOutputAccess::$opName" ); |
657 | if ( $span->getContext()->isSampled() ) { |
658 | $span->setAttributes( [ |
659 | 'org.wikimedia.parser.page' => $page->__toString(), |
660 | 'org.wikimedia.parser.page.id' => $page->getId(), |
661 | 'org.wikimedia.parser.page.wiki' => $page->getWikiId(), |
662 | ] ); |
663 | if ( $revision ) { |
664 | $span->setAttributes( [ |
665 | 'org.wikimedia.parser.revision.id' => $revision->getId(), |
666 | 'org.wikimedia.parser.revision.parent_id' => $revision->getParentId(), |
667 | ] ); |
668 | } |
669 | } |
670 | return $span->start()->activate(); |
671 | } |
672 | } |