Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.92% covered (success)
95.92%
47 / 49
93.75% covered (success)
93.75%
15 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
CacheTime
97.92% covered (success)
97.92%
47 / 48
93.75% covered (success)
93.75%
15 / 16
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
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
5
 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%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 getCacheExpiry
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
4.07
 isCacheable
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 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%
6 / 6
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
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
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\JsonCodec\JsonCodecable;
16use Wikimedia\JsonCodec\JsonCodecableTrait;
17use Wikimedia\Timestamp\TimestampFormat as TS;
18
19/**
20 * Parser cache specific expiry check.
21 *
22 * @ingroup Parser
23 */
24class CacheTime implements ParserCacheMetadata, JsonCodecable {
25    use JsonCodecableTrait;
26
27    /**
28     * @var array<string,true> ParserOptions which have been taken into account
29     * to produce output, option names stored in array keys.
30     */
31    protected array $mParseUsedOptions = [];
32
33    /**
34     * @var string|int TS::MW timestamp when this object was generated, or -1 for not cacheable. Used
35     * in ParserCache.
36     */
37    protected string|int $mCacheTime = '';
38
39    /**
40     * @var int|null Seconds after which the object should expire, use 0 for not cacheable. Used in
41     * ParserCache.
42     */
43    protected ?int $mCacheExpiry = null;
44
45    /**
46     * @var int|null Revision ID that was parsed
47     */
48    protected ?int $mCacheRevisionId = null;
49
50    /**
51     * @return string|int TS::MW timestamp
52     */
53    public function getCacheTime() {
54        // NOTE: keep support for undocumented used of -1 to mean "not cacheable".
55        if ( $this->mCacheTime === '' ) {
56            $this->mCacheTime = MWTimestamp::now();
57        }
58        return $this->mCacheTime;
59    }
60
61    /**
62     * @return bool true if a cache time has been set
63     */
64    public function hasCacheTime(): bool {
65        return $this->mCacheTime !== '';
66    }
67
68    /**
69     * setCacheTime() sets the timestamp expressing when the page has been rendered.
70     * This does not control expiry, see updateCacheExpiry() for that!
71     * @param string $t TS::MW timestamp
72     * @return string
73     */
74    public function setCacheTime( $t ) {
75        // NOTE: keep support for undocumented used of -1 to mean "not cacheable".
76        if ( is_string( $t ) && $t !== '-1' ) {
77            $t = MWTimestamp::convert( TS::MW, $t );
78        }
79
80        if ( $t === -1 || $t === '-1' ) {
81            wfDeprecatedMsg( __METHOD__ . ' called with -1 as an argument', '1.36' );
82        }
83
84        return wfSetVar( $this->mCacheTime, $t );
85    }
86
87    /**
88     * @since 1.23
89     * @return int|null Revision id, if any was set
90     */
91    public function getCacheRevisionId(): ?int {
92        return $this->mCacheRevisionId;
93    }
94
95    /**
96     * @since 1.23
97     * @param int|null $id Revision ID
98     */
99    public function setCacheRevisionId( $id ) {
100        $this->mCacheRevisionId = $id;
101    }
102
103    /**
104     * Sets the number of seconds after which this object should expire.
105     *
106     * This value is used with the ParserCache.
107     * If called with a value greater than the value provided at any previous call,
108     * the new call has no effect. The value returned by getCacheExpiry is smaller
109     * or equal to the smallest number that was provided as an argument to
110     * updateCacheExpiry().
111     *
112     * Avoid using 0 if at all possible. Consider JavaScript for highly dynamic content.
113     *
114     * NOTE: Beware that reducing the TTL for reasons that do not relate to "dynamic content",
115     * may have the side-effect of incurring more RefreshLinksJob executions.
116     * See also WikiPage::triggerOpportunisticLinksUpdate.
117     *
118     * @param int $seconds
119     */
120    public function updateCacheExpiry( $seconds ) {
121        $seconds = (int)$seconds;
122
123        if ( $this->mCacheExpiry === null || $this->mCacheExpiry > $seconds ) {
124            $this->mCacheExpiry = $seconds;
125        }
126    }
127
128    /**
129     * Returns the number of seconds after which this object should expire.
130     * This method is used by ParserCache to determine how long the ParserOutput can be cached.
131     * The timestamp of expiry can be calculated by adding getCacheExpiry() to getCacheTime().
132     * The value returned by getCacheExpiry is smaller or equal to the smallest number
133     * that was provided to a call of updateCacheExpiry(), and smaller or equal to the
134     * value of $wgParserCacheExpireTime.
135     */
136    public function getCacheExpiry(): int {
137        $parserCacheExpireTime = MediaWikiServices::getInstance()->getMainConfig()
138            ->get( MainConfigNames::ParserCacheExpireTime );
139
140        // NOTE: keep support for undocumented used of -1 to mean "not cacheable".
141        if ( $this->mCacheTime !== '' && $this->mCacheTime < 0 ) {
142            return 0;
143        }
144
145        $expire = min( $this->mCacheExpiry ?? $parserCacheExpireTime, $parserCacheExpireTime );
146        return $expire > 0 ? $expire : 0;
147    }
148
149    /**
150     * @return bool
151     */
152    public function isCacheable() {
153        return $this->getCacheExpiry() > 0;
154    }
155
156    /**
157     * Return true if this cached output object predates the global or
158     * per-article cache invalidation timestamps, or if it comes from
159     * an incompatible older version.
160     *
161     * @param string $touched The affected article's last touched timestamp
162     * @return bool
163     */
164    public function expired( $touched ) {
165        $cacheEpoch = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::CacheEpoch );
166
167        $expiry = MWTimestamp::convert( TS::MW, MWTimestamp::time() - $this->getCacheExpiry() );
168
169        return !$this->isCacheable() // parser says it's not cacheable
170            || $this->getCacheTime() < $touched
171            || $this->getCacheTime() <= $cacheEpoch
172            || $this->getCacheTime() < $expiry; // expiry period has passed
173    }
174
175    /**
176     * Return true if this cached output object is for a different revision of
177     * the page.
178     *
179     * @todo We always return false if $this->getCacheRevisionId() is null;
180     * this prevents invalidating the whole parser cache when this change is
181     * deployed. Someday that should probably be changed.
182     *
183     * @since 1.23
184     * @param int $id The affected article's current revision id
185     * @return bool
186     */
187    public function isDifferentRevision( $id ) {
188        $cached = $this->getCacheRevisionId();
189        return $cached !== null && $id !== $cached;
190    }
191
192    /**
193     * Returns the options from its ParserOptions which have been taken
194     * into account to produce the output.
195     * @since 1.36
196     * @return string[]
197     */
198    public function getUsedOptions(): array {
199        return array_keys( $this->mParseUsedOptions );
200    }
201
202    /**
203     * Tags a parser option for use in the cache key for this parser output.
204     * Registered as a watcher at ParserOptions::registerWatcher() by Parser::clearState().
205     * The information gathered here is available via getUsedOptions(),
206     * and is used by ParserCache::save().
207     *
208     * @see ParserCache::getMetadata
209     * @see ParserCache::save
210     * @see ParserOptions::addExtraKey
211     * @see ParserOptions::optionsHash
212     * @param string $option
213     */
214    public function recordOption( string $option ) {
215        $this->mParseUsedOptions[$option] = true;
216    }
217
218    /**
219     * Tags a list of parser option names for use in the cache key for this parser output.
220     *
221     * @see recordOption()
222     * @param string[] $options
223     */
224    public function recordOptions( array $options ) {
225        $this->mParseUsedOptions = array_merge(
226            $this->mParseUsedOptions,
227            array_fill_keys( $options, true )
228        );
229    }
230
231    /**
232     * Returns a JSON serializable structure representing this CacheTime instance.
233     * @see ::newFromJsonArray()
234     *
235     * @return array
236     */
237    public function toJsonArray(): array {
238        // WARNING: When changing how this class is serialized, follow the instructions
239        // at <https://www.mediawiki.org/wiki/Manual:Parser_cache/Serialization_compatibility>!
240
241        return [
242            'ParseUsedOptions' => $this->mParseUsedOptions,
243            'CacheExpiry' => $this->mCacheExpiry,
244            'CacheTime' => $this->mCacheTime,
245            'CacheRevisionId' => $this->mCacheRevisionId,
246        ];
247    }
248
249    public static function newFromJsonArray( array $json ): self {
250        $cacheTime = new CacheTime();
251        $cacheTime->initFromJson( $json );
252        return $cacheTime;
253    }
254
255    /**
256     * Initialize member fields from an array returned by toJsonArray().
257     * @param array $jsonData
258     */
259    protected function initFromJson( array $jsonData ) {
260        // WARNING: When changing how this class is serialized, follow the instructions
261        // at <https://www.mediawiki.org/wiki/Manual:Parser_cache/Serialization_compatibility>!
262
263        $this->mParseUsedOptions = $jsonData['ParseUsedOptions'] ?? [];
264        $this->mCacheExpiry = $jsonData['CacheExpiry'] ?? null;
265        $this->mCacheTime = $jsonData['CacheTime'] ?? '';
266        $this->mCacheRevisionId = $jsonData['CacheRevisionId'] ?? null;
267    }
268}
269
270/** @deprecated class alias since 1.43 */
271class_alias( CacheTime::class, 'CacheTime' );