Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
76.81% covered (warning)
76.81%
53 / 69
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
24namespace MediaWiki\Parser;
25
26use MediaWiki\Json\JsonDeserializable;
27use MediaWiki\Json\JsonDeserializableTrait;
28use MediaWiki\Json\JsonDeserializer;
29use MediaWiki\MainConfigNames;
30use MediaWiki\MediaWikiServices;
31use MediaWiki\Utils\MWTimestamp;
32use Wikimedia\Reflection\GhostFieldAccessTrait;
33
34/**
35 * Parser cache specific expiry check.
36 *
37 * @ingroup Parser
38 */
39class CacheTime implements ParserCacheMetadata, JsonDeserializable {
40    use GhostFieldAccessTrait;
41    use JsonDeserializableTrait;
42
43    /**
44     * @var true[] ParserOptions which have been taken into account
45     * to produce output, option names stored in array keys.
46     */
47    protected $mParseUsedOptions = [];
48
49    /**
50     * @var string|int TS_MW timestamp when this object was generated, or -1 for not cacheable. Used
51     * in ParserCache.
52     */
53    protected $mCacheTime = '';
54
55    /**
56     * @var int|null Seconds after which the object should expire, use 0 for not cacheable. Used in
57     * ParserCache.
58     */
59    protected $mCacheExpiry = null;
60
61    /**
62     * @var int|null Revision ID that was parsed
63     */
64    protected $mCacheRevisionId = null;
65
66    /**
67     * @return string|int TS_MW timestamp
68     */
69    public function getCacheTime() {
70        // NOTE: keep support for undocumented used of -1 to mean "not cacheable".
71        if ( $this->mCacheTime === '' ) {
72            $this->mCacheTime = MWTimestamp::now();
73        }
74        return $this->mCacheTime;
75    }
76
77    /**
78     * @return bool true if a cache time has been set
79     */
80    public function hasCacheTime(): bool {
81        return $this->mCacheTime !== '';
82    }
83
84    /**
85     * setCacheTime() sets the timestamp expressing when the page has been rendered.
86     * This does not control expiry, see updateCacheExpiry() for that!
87     * @param string $t TS_MW timestamp
88     * @return string
89     */
90    public function setCacheTime( $t ) {
91        // NOTE: keep support for undocumented used of -1 to mean "not cacheable".
92        if ( is_string( $t ) && $t !== '-1' ) {
93            $t = MWTimestamp::convert( TS_MW, $t );
94        }
95
96        if ( $t === -1 || $t === '-1' ) {
97            wfDeprecatedMsg( __METHOD__ . ' called with -1 as an argument', '1.36' );
98        }
99
100        return wfSetVar( $this->mCacheTime, $t );
101    }
102
103    /**
104     * @since 1.23
105     * @return int|null Revision id, if any was set
106     */
107    public function getCacheRevisionId(): ?int {
108        return $this->mCacheRevisionId;
109    }
110
111    /**
112     * @since 1.23
113     * @param int|null $id Revision ID
114     */
115    public function setCacheRevisionId( $id ) {
116        $this->mCacheRevisionId = $id;
117    }
118
119    /**
120     * Sets the number of seconds after which this object should expire.
121     *
122     * This value is used with the ParserCache.
123     * If called with a value greater than the value provided at any previous call,
124     * the new call has no effect. The value returned by getCacheExpiry is smaller
125     * or equal to the smallest number that was provided as an argument to
126     * updateCacheExpiry().
127     *
128     * Avoid using 0 if at all possible. Consider JavaScript for highly dynamic content.
129     *
130     * NOTE: Beware that reducing the TTL for reasons that do not relate to "dynamic content",
131     * may have the side-effect of incurring more RefreshLinksJob executions.
132     * See also WikiPage::triggerOpportunisticLinksUpdate.
133     *
134     * @param int $seconds
135     */
136    public function updateCacheExpiry( $seconds ) {
137        $seconds = (int)$seconds;
138
139        if ( $this->mCacheExpiry === null || $this->mCacheExpiry > $seconds ) {
140            $this->mCacheExpiry = $seconds;
141        }
142    }
143
144    /**
145     * Returns the number of seconds after which this object should expire.
146     * This method is used by ParserCache to determine how long the ParserOutput can be cached.
147     * The timestamp of expiry can be calculated by adding getCacheExpiry() to getCacheTime().
148     * The value returned by getCacheExpiry is smaller or equal to the smallest number
149     * that was provided to a call of updateCacheExpiry(), and smaller or equal to the
150     * value of $wgParserCacheExpireTime.
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}
329
330/** @deprecated class alias since 1.43 */
331class_alias( CacheTime::class, 'CacheTime' );