Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
91.07% covered (success)
91.07%
51 / 56
88.24% covered (warning)
88.24%
15 / 17
CRAP
0.00% covered (danger)
0.00%
0 / 1
CacheTime
92.73% covered (success)
92.73%
51 / 55
88.24% covered (warning)
88.24%
15 / 17
28.30
0.00% covered (danger)
0.00%
0 / 1
 getCacheTime
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 hasCacheTime
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setCacheTime
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
2.09
 getCacheRevisionId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setCacheRevisionId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 updateCacheExpiry
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 getCacheExpiry
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getCacheExpirySource
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isCacheable
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 expired
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 isDifferentRevision
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getUsedOptions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 recordOption
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 recordOptions
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 toJsonArray
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 newFromJsonArray
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 initFromJson
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
2.06
1<?php
2/**
3 * Parser cache specific expiry check.
4 *
5 * @license GPL-2.0-or-later
6 * @file
7 * @ingroup Parser
8 */
9
10namespace MediaWiki\Parser;
11
12use MediaWiki\MainConfigNames;
13use MediaWiki\MediaWikiServices;
14use MediaWiki\Utils\MWTimestamp;
15use Wikimedia\Assert\Assert;
16use Wikimedia\JsonCodec\JsonCodecable;
17use Wikimedia\JsonCodec\JsonCodecableTrait;
18use Wikimedia\Timestamp\TimestampFormat as TS;
19
20/**
21 * Parser cache specific expiry check.
22 *
23 * @ingroup Parser
24 */
25class CacheTime implements ParserCacheMetadata, JsonCodecable {
26    use JsonCodecableTrait;
27
28    /**
29     * @var array<string,true> ParserOptions which have been taken into account
30     * to produce output, option names stored in array keys.
31     */
32    protected array $mParseUsedOptions = [];
33
34    /**
35     * @var string TS::MW timestamp when this object was generated, or
36     *  '' if not yet set. Used in ParserCache.
37     */
38    protected string $mCacheTime = '';
39
40    /**
41     * @var int|null Seconds after which the object should expire, use 0
42     *  for not cacheable and null for "the default cache expiration time"
43     *  (which is assumed to be greater than zero).
44     *  Used in ParserCache.
45     */
46    protected ?int $mCacheExpiry = null;
47
48    /**
49     * @var int|null Revision ID that was parsed
50     */
51    protected ?int $mCacheRevisionId = null;
52
53    /**
54     * @var string|null Human-readable label identifying what caused the
55     * current cache expiry value (e.g. "Template:Foo (currentday)").
56     * Updated only when updateCacheExpiry() actually lowers the TTL.
57     */
58    protected ?string $mCacheExpirySource = null;
59
60    /**
61     * @return string TS::MW timestamp
62     */
63    public function getCacheTime() {
64        if ( $this->mCacheTime === '' ) {
65            $this->mCacheTime = MWTimestamp::now();
66        }
67        return $this->mCacheTime;
68    }
69
70    /**
71     * @return bool true if a cache time has been set
72     */
73    public function hasCacheTime(): bool {
74        return $this->mCacheTime !== '';
75    }
76
77    /**
78     * setCacheTime() sets the timestamp expressing when the page has been rendered.
79     * This does not control expiry, see updateCacheExpiry() for that!
80     * @param string $t TS::MW timestamp
81     * @return string
82     */
83    public function setCacheTime( $t ): string {
84        if ( !is_string( $t ) ) {
85            wfDeprecated( __METHOD__ . ": with non-string argument (" . get_debug_type( $t ) . ")", "1.46" );
86            $t = (string)$t;
87        }
88        // A long time ago we used to use -1 to mean "not cacheable"
89        Assert::invariant( $t !== '-1', "not a TS::MW timestamp" );
90
91        $old = $this->mCacheTime;
92        $this->mCacheTime = MWTimestamp::convert( TS::MW, $t );
93        return $old;
94    }
95
96    /**
97     * @since 1.23
98     * @return int|null Revision id, if any was set
99     */
100    public function getCacheRevisionId(): ?int {
101        return $this->mCacheRevisionId;
102    }
103
104    /**
105     * @since 1.23
106     * @param int|null $id Revision ID
107     */
108    public function setCacheRevisionId( $id ) {
109        $this->mCacheRevisionId = $id;
110    }
111
112    /**
113     * Reduce the number of seconds after which this object should expire.
114     *
115     * This value is used with the ParserCache.
116     * If called with a value greater than the value provided at any previous call,
117     * the new call has no effect.
118     *
119     * Avoid using 0 if at all possible. Consider JavaScript for highly dynamic content.
120     *
121     * NOTE: Beware that reducing the TTL for reasons that do not relate to "dynamic content",
122     * may have the side-effect of incurring more RefreshLinksJob executions.
123     * See also WikiPage::triggerOpportunisticLinksUpdate.
124     *
125     * @param int $seconds
126     * @param string|null $source Human-readable label identifying what is
127     *   responsible for this TTL (e.g. a magic word or template name).
128     *   Recorded only when the expiry is actually lowered. @since 1.46
129     * @deprecated since 1.46 Calling this method without $source.
130     */
131    public function updateCacheExpiry( $seconds, ?string $source = null ) {
132        $seconds = (int)$seconds;
133
134        if ( $this->mCacheExpiry === null || $this->mCacheExpiry > $seconds ) {
135            $this->mCacheExpiry = $seconds;
136            $this->mCacheExpirySource = $source;
137        }
138    }
139
140    /**
141     * Returns the number of seconds after which this object should expire.
142     * This method is used by ParserCache to determine how long the ParserOutput can be cached.
143     * The timestamp of expiry can be calculated by adding getCacheExpiry() to getCacheTime().
144     * The value returned by getCacheExpiry() is smaller or equal to the
145     * value of $wgParserCacheExpireTime and influenced by the values provided
146     * to calls to updateCacheExpiry(), but child classes may adjust the raw
147     * value: for example, to add minimums, dynamic adjustments, or reductions
148     * to the default expiry based on output properties.
149     *
150     * @note Use the protected $mCacheExpiry property to access the "real"
151     * minimum value provided to `updateCacheExpiry`, but as this should
152     * generally not be accessed outside this class no public getter method
153     * has been provided.
154     * @note This method should return 0 if and only if ::isCacheable()
155     *   returns false.
156     */
157    public function getCacheExpiry(): int {
158        $parserCacheExpireTime = MediaWikiServices::getInstance()->getMainConfig()
159            ->get( MainConfigNames::ParserCacheExpireTime );
160
161        $expire = min( $this->mCacheExpiry ?? $parserCacheExpireTime, $parserCacheExpireTime );
162        return $expire > 0 ? $expire : 0;
163    }
164
165    /**
166     * @return string|null Human-readable label identifying what caused the
167     *   current cache expiry, or null if no source was recorded.
168     * @see updateCacheExpiry()
169     * @since 1.46
170     */
171    public function getCacheExpirySource(): ?string {
172        return $this->mCacheExpirySource;
173    }
174
175    /**
176     * @return bool
177     */
178    public function isCacheable() {
179        // Must return false if $mCacheExpiry is 0, but may return false
180        // in other cases as well, if subclasses wish to extend this.
181        return $this->mCacheExpiry === null || $this->mCacheExpiry > 0;
182    }
183
184    /**
185     * Return true if this cached output object predates the global or
186     * per-article cache invalidation timestamps, or if it comes from
187     * an incompatible older version.
188     *
189     * @param string $touched The affected article's last touched timestamp
190     * @return bool
191     */
192    public function expired( $touched ) {
193        $cacheEpoch = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::CacheEpoch );
194
195        $expiry = MWTimestamp::convert( TS::MW, MWTimestamp::time() - $this->getCacheExpiry() );
196
197        return !$this->isCacheable() // parser says it's not cacheable
198            || $this->getCacheTime() < $touched
199            || $this->getCacheTime() <= $cacheEpoch
200            || $this->getCacheTime() < $expiry; // expiry period has passed
201    }
202
203    /**
204     * Return true if this cached output object is for a different revision of
205     * the page.
206     *
207     * @todo We always return false if $this->getCacheRevisionId() is null;
208     * this prevents invalidating the whole parser cache when this change is
209     * deployed. Someday that should probably be changed.
210     *
211     * @since 1.23
212     * @param int $id The affected article's latest revision id
213     * @return bool
214     */
215    public function isDifferentRevision( $id ) {
216        $cached = $this->getCacheRevisionId();
217        return $cached !== null && $id !== $cached;
218    }
219
220    /**
221     * Returns the options from its ParserOptions which have been taken
222     * into account to produce the output.
223     * @since 1.36
224     * @return string[]
225     */
226    public function getUsedOptions(): array {
227        return array_keys( $this->mParseUsedOptions );
228    }
229
230    /**
231     * Tags a parser option for use in the cache key for this parser output.
232     * Registered as a watcher at ParserOptions::registerWatcher() by Parser::clearState().
233     * The information gathered here is available via getUsedOptions(),
234     * and is used by ParserCache::save().
235     *
236     * @see ParserCache::getMetadata
237     * @see ParserCache::save
238     * @see ParserOptions::addExtraKey
239     * @see ParserOptions::optionsHash
240     * @param string $option
241     */
242    public function recordOption( string $option ) {
243        $this->mParseUsedOptions[$option] = true;
244    }
245
246    /**
247     * Tags a list of parser option names for use in the cache key for this parser output.
248     *
249     * @see recordOption()
250     * @param string[] $options
251     */
252    public function recordOptions( array $options ) {
253        $this->mParseUsedOptions = array_merge(
254            $this->mParseUsedOptions,
255            array_fill_keys( $options, true )
256        );
257    }
258
259    /**
260     * Returns a JSON serializable structure representing this CacheTime instance.
261     * @see ::newFromJsonArray()
262     *
263     * @return array
264     */
265    public function toJsonArray(): array {
266        // WARNING: When changing how this class is serialized, follow the instructions
267        // at <https://www.mediawiki.org/wiki/Manual:Parser_cache/Serialization_compatibility>!
268
269        return [
270            'ParseUsedOptions' => $this->mParseUsedOptions,
271            'CacheExpiry' => $this->mCacheExpiry,
272            'CacheTime' => $this->mCacheTime,
273            'CacheRevisionId' => $this->mCacheRevisionId,
274            'CacheExpirySource' => $this->mCacheExpirySource,
275        ];
276    }
277
278    public static function newFromJsonArray( array $json ): self {
279        $cacheTime = new CacheTime();
280        $cacheTime->initFromJson( $json );
281        return $cacheTime;
282    }
283
284    /**
285     * Initialize member fields from an array returned by toJsonArray().
286     * @param array $jsonData
287     */
288    protected function initFromJson( array $jsonData ) {
289        // WARNING: When changing how this class is serialized, follow the instructions
290        // at <https://www.mediawiki.org/wiki/Manual:Parser_cache/Serialization_compatibility>!
291
292        $this->mParseUsedOptions = $jsonData['ParseUsedOptions'] ?? [];
293        $this->mCacheExpiry = $jsonData['CacheExpiry'] ?? null;
294        $this->mCacheTime = $jsonData['CacheTime'] ?? '';
295        $this->mCacheRevisionId = $jsonData['CacheRevisionId'] ?? null;
296        $this->mCacheExpirySource = $jsonData['CacheExpirySource'] ?? null;
297
298        // Backward compatibility
299        if ( $this->mCacheTime === '-1' ) {
300            $this->mCacheExpiry = 0;
301            $this->mCacheTime = '';
302        }
303    }
304}
305
306/** @deprecated class alias since 1.43 */
307class_alias( CacheTime::class, 'CacheTime' );