Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
77.94% covered (warning)
77.94%
53 / 68
78.95% covered (warning)
78.95%
15 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
CacheTime
77.94% covered (warning)
77.94%
53 / 68
78.95% covered (warning)
78.95%
15 / 19
60.93
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
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 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%
8 / 8
100.00% covered (success)
100.00%
1 / 1
6
 __wakeup
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 __get
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 __set
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * Parser cache specific expiry check.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 * @ingroup Parser
22 */
23
24use MediaWiki\Json\JsonDeserializable;
25use MediaWiki\Json\JsonDeserializableTrait;
26use MediaWiki\Json\JsonDeserializer;
27use MediaWiki\MainConfigNames;
28use MediaWiki\MediaWikiServices;
29use MediaWiki\Parser\ParserCacheMetadata;
30use MediaWiki\Utils\MWTimestamp;
31use Wikimedia\Reflection\GhostFieldAccessTrait;
32
33/**
34 * Parser cache specific expiry check.
35 *
36 * @ingroup Parser
37 */
38class CacheTime implements ParserCacheMetadata, JsonDeserializable {
39    use GhostFieldAccessTrait;
40    use JsonDeserializableTrait;
41
42    /**
43     * @var true[] ParserOptions which have been taken into account
44     * to produce output, option names stored in array keys.
45     */
46    protected $mParseUsedOptions = [];
47
48    /**
49     * @var string|int TS_MW timestamp when this object was generated, or -1 for not cacheable. Used
50     * in ParserCache.
51     */
52    protected $mCacheTime = '';
53
54    /**
55     * @var int|null Seconds after which the object should expire, use 0 for not cacheable. Used in
56     * ParserCache.
57     */
58    protected $mCacheExpiry = null;
59
60    /**
61     * @var int|null Revision ID that was parsed
62     */
63    protected $mCacheRevisionId = null;
64
65    /**
66     * @return string|int TS_MW timestamp
67     */
68    public function getCacheTime() {
69        // NOTE: keep support for undocumented used of -1 to mean "not cacheable".
70        if ( $this->mCacheTime === '' ) {
71            $this->mCacheTime = MWTimestamp::now();
72        }
73        return $this->mCacheTime;
74    }
75
76    /**
77     * @return bool true if a cache time has been set
78     */
79    public function hasCacheTime(): bool {
80        return $this->mCacheTime !== '';
81    }
82
83    /**
84     * setCacheTime() sets the timestamp expressing when the page has been rendered.
85     * This does not control expiry, see updateCacheExpiry() for that!
86     * @param string $t TS_MW timestamp
87     * @return string
88     */
89    public function setCacheTime( $t ) {
90        // NOTE: keep support for undocumented used of -1 to mean "not cacheable".
91        if ( is_string( $t ) && $t !== '-1' ) {
92            $t = MWTimestamp::convert( TS_MW, $t );
93        }
94
95        if ( $t === -1 || $t === '-1' ) {
96            wfDeprecatedMsg( __METHOD__ . ' called with -1 as an argument', '1.36' );
97        }
98
99        return wfSetVar( $this->mCacheTime, $t );
100    }
101
102    /**
103     * @since 1.23
104     * @return int|null Revision id, if any was set
105     */
106    public function getCacheRevisionId(): ?int {
107        return $this->mCacheRevisionId;
108    }
109
110    /**
111     * @since 1.23
112     * @param int|null $id Revision ID
113     */
114    public function setCacheRevisionId( $id ) {
115        $this->mCacheRevisionId = $id;
116    }
117
118    /**
119     * Sets the number of seconds after which this object should expire.
120     *
121     * This value is used with the ParserCache.
122     * If called with a value greater than the value provided at any previous call,
123     * the new call has no effect. The value returned by getCacheExpiry is smaller
124     * or equal to the smallest number that was provided as an argument to
125     * updateCacheExpiry().
126     *
127     * Avoid using 0 if at all possible. Consider JavaScript for highly dynamic content.
128     *
129     * NOTE: Beware that reducing the TTL for reasons that do not relate to "dynamic content",
130     * may have the side-effect of incurring more RefreshLinksJob executions.
131     * See also WikiPage::triggerOpportunisticLinksUpdate.
132     *
133     * @param int $seconds
134     */
135    public function updateCacheExpiry( $seconds ) {
136        $seconds = (int)$seconds;
137
138        if ( $this->mCacheExpiry === null || $this->mCacheExpiry > $seconds ) {
139            $this->mCacheExpiry = $seconds;
140        }
141    }
142
143    /**
144     * Returns the number of seconds after which this object should expire.
145     * This method is used by ParserCache to determine how long the ParserOutput can be cached.
146     * The timestamp of expiry can be calculated by adding getCacheExpiry() to getCacheTime().
147     * The value returned by getCacheExpiry is smaller or equal to the smallest number
148     * that was provided to a call of updateCacheExpiry(), and smaller or equal to the
149     * value of $wgParserCacheExpireTime.
150     * @return int
151     */
152    public function getCacheExpiry(): int {
153        $parserCacheExpireTime = MediaWikiServices::getInstance()->getMainConfig()
154            ->get( MainConfigNames::ParserCacheExpireTime );
155
156        // NOTE: keep support for undocumented used of -1 to mean "not cacheable".
157        if ( $this->mCacheTime !== '' && $this->mCacheTime < 0 ) {
158            return 0;
159        }
160
161        $expire = min( $this->mCacheExpiry ?? $parserCacheExpireTime, $parserCacheExpireTime );
162        return $expire > 0 ? $expire : 0;
163    }
164
165    /**
166     * @return bool
167     */
168    public function isCacheable() {
169        return $this->getCacheExpiry() > 0;
170    }
171
172    /**
173     * Return true if this cached output object predates the global or
174     * per-article cache invalidation timestamps, or if it comes from
175     * an incompatible older version.
176     *
177     * @param string $touched The affected article's last touched timestamp
178     * @return bool
179     */
180    public function expired( $touched ) {
181        $cacheEpoch = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::CacheEpoch );
182
183        $expiry = MWTimestamp::convert( TS_MW, MWTimestamp::time() - $this->getCacheExpiry() );
184
185        return !$this->isCacheable() // parser says it's not cacheable
186            || $this->getCacheTime() < $touched
187            || $this->getCacheTime() <= $cacheEpoch
188            || $this->getCacheTime() < $expiry; // expiry period has passed
189    }
190
191    /**
192     * Return true if this cached output object is for a different revision of
193     * the page.
194     *
195     * @todo We always return false if $this->getCacheRevisionId() is null;
196     * this prevents invalidating the whole parser cache when this change is
197     * deployed. Someday that should probably be changed.
198     *
199     * @since 1.23
200     * @param int $id The affected article's current revision id
201     * @return bool
202     */
203    public function isDifferentRevision( $id ) {
204        $cached = $this->getCacheRevisionId();
205        return $cached !== null && $id !== $cached;
206    }
207
208    /**
209     * Returns the options from its ParserOptions which have been taken
210     * into account to produce the output.
211     * @since 1.36
212     * @return string[]
213     */
214    public function getUsedOptions(): array {
215        return array_keys( $this->mParseUsedOptions );
216    }
217
218    /**
219     * Tags a parser option for use in the cache key for this parser output.
220     * Registered as a watcher at ParserOptions::registerWatcher() by Parser::clearState().
221     * The information gathered here is available via getUsedOptions(),
222     * and is used by ParserCache::save().
223     *
224     * @see ParserCache::getMetadata
225     * @see ParserCache::save
226     * @see ParserOptions::addExtraKey
227     * @see ParserOptions::optionsHash
228     * @param string $option
229     */
230    public function recordOption( string $option ) {
231        $this->mParseUsedOptions[$option] = true;
232    }
233
234    /**
235     * Tags a list of parser option names for use in the cache key for this parser output.
236     *
237     * @see recordOption()
238     * @param string[] $options
239     */
240    public function recordOptions( array $options ) {
241        $this->mParseUsedOptions = array_merge(
242            $this->mParseUsedOptions,
243            array_fill_keys( $options, true )
244        );
245    }
246
247    /**
248     * Returns a JSON serializable structure representing this CacheTime instance.
249     * @see newFromJson()
250     *
251     * @return array
252     */
253    protected function toJsonArray(): array {
254        // WARNING: When changing how this class is serialized, follow the instructions
255        // at <https://www.mediawiki.org/wiki/Manual:Parser_cache/Serialization_compatibility>!
256
257        return [
258            'ParseUsedOptions' => $this->mParseUsedOptions,
259            'CacheExpiry' => $this->mCacheExpiry,
260            'CacheTime' => $this->mCacheTime,
261            'CacheRevisionId' => $this->mCacheRevisionId,
262        ];
263    }
264
265    public static function newFromJsonArray( JsonDeserializer $deserializer, array $json ) {
266        $cacheTime = new CacheTime();
267        $cacheTime->initFromJson( $deserializer, $json );
268        return $cacheTime;
269    }
270
271    /**
272     * Initialize member fields from an array returned by jsonSerialize().
273     * @param JsonDeserializer $deserializer Unused
274     * @param array $jsonData
275     */
276    protected function initFromJson( JsonDeserializer $deserializer, array $jsonData ) {
277        // WARNING: When changing how this class is serialized, follow the instructions
278        // at <https://www.mediawiki.org/wiki/Manual:Parser_cache/Serialization_compatibility>!
279
280        if ( array_key_exists( 'AccessedOptions', $jsonData ) ) {
281            // Backwards compatibility for ParserOutput
282            $this->mParseUsedOptions = $jsonData['AccessedOptions'] ?: [];
283        } elseif ( array_key_exists( 'UsedOptions', $jsonData ) ) {
284            // Backwards compatibility
285            $this->recordOptions( $jsonData['UsedOptions'] ?: [] );
286        } else {
287            $this->mParseUsedOptions = $jsonData['ParseUsedOptions'] ?: [];
288        }
289        $this->mCacheExpiry = $jsonData['CacheExpiry'];
290        $this->mCacheTime = $jsonData['CacheTime'];
291        $this->mCacheRevisionId = $jsonData['CacheRevisionId'];
292    }
293
294    public function __wakeup() {
295        // Backwards compatibility, pre 1.36
296        $priorOptions = $this->getGhostFieldValue( 'mUsedOptions' );
297        if ( $priorOptions ) {
298            $this->recordOptions( $priorOptions );
299        }
300    }
301
302    public function __get( $name ) {
303        if ( property_exists( get_called_class(), $name ) ) {
304            // Direct access to a public property, deprecated.
305            wfDeprecatedMsg( "CacheTime::{$name} public read access deprecated", '1.38' );
306            return $this->$name;
307        } elseif ( property_exists( $this, $name ) ) {
308            // Dynamic property access, deprecated.
309            wfDeprecatedMsg( "CacheTime::{$name} dynamic property read access deprecated", '1.38' );
310            return $this->$name;
311        } else {
312            trigger_error( "Inaccessible property via __set(): $name" );
313            return null;
314        }
315    }
316
317    public function __set( $name, $value ) {
318        if ( property_exists( get_called_class(), $name ) ) {
319            // Direct access to a public property, deprecated.
320            wfDeprecatedMsg( "CacheTime::$name public write access deprecated", '1.38' );
321            $this->$name = $value;
322        } else {
323            // Dynamic property access, deprecated.
324            wfDeprecatedMsg( "CacheTime::$name dynamic property write access deprecated", '1.38' );
325            $this->$name = $value;
326        }
327    }
328}