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     * @return int
152     */
153    public function getCacheExpiry(): int {
154        $parserCacheExpireTime = MediaWikiServices::getInstance()->getMainConfig()
155            ->get( MainConfigNames::ParserCacheExpireTime );
156
157        // NOTE: keep support for undocumented used of -1 to mean "not cacheable".
158        if ( $this->mCacheTime !== '' && $this->mCacheTime < 0 ) {
159            return 0;
160        }
161
162        $expire = min( $this->mCacheExpiry ?? $parserCacheExpireTime, $parserCacheExpireTime );
163        return $expire > 0 ? $expire : 0;
164    }
165
166    /**
167     * @return bool
168     */
169    public function isCacheable() {
170        return $this->getCacheExpiry() > 0;
171    }
172
173    /**
174     * Return true if this cached output object predates the global or
175     * per-article cache invalidation timestamps, or if it comes from
176     * an incompatible older version.
177     *
178     * @param string $touched The affected article's last touched timestamp
179     * @return bool
180     */
181    public function expired( $touched ) {
182        $cacheEpoch = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::CacheEpoch );
183
184        $expiry = MWTimestamp::convert( TS_MW, MWTimestamp::time() - $this->getCacheExpiry() );
185
186        return !$this->isCacheable() // parser says it's not cacheable
187            || $this->getCacheTime() < $touched
188            || $this->getCacheTime() <= $cacheEpoch
189            || $this->getCacheTime() < $expiry; // expiry period has passed
190    }
191
192    /**
193     * Return true if this cached output object is for a different revision of
194     * the page.
195     *
196     * @todo We always return false if $this->getCacheRevisionId() is null;
197     * this prevents invalidating the whole parser cache when this change is
198     * deployed. Someday that should probably be changed.
199     *
200     * @since 1.23
201     * @param int $id The affected article's current revision id
202     * @return bool
203     */
204    public function isDifferentRevision( $id ) {
205        $cached = $this->getCacheRevisionId();
206        return $cached !== null && $id !== $cached;
207    }
208
209    /**
210     * Returns the options from its ParserOptions which have been taken
211     * into account to produce the output.
212     * @since 1.36
213     * @return string[]
214     */
215    public function getUsedOptions(): array {
216        return array_keys( $this->mParseUsedOptions );
217    }
218
219    /**
220     * Tags a parser option for use in the cache key for this parser output.
221     * Registered as a watcher at ParserOptions::registerWatcher() by Parser::clearState().
222     * The information gathered here is available via getUsedOptions(),
223     * and is used by ParserCache::save().
224     *
225     * @see ParserCache::getMetadata
226     * @see ParserCache::save
227     * @see ParserOptions::addExtraKey
228     * @see ParserOptions::optionsHash
229     * @param string $option
230     */
231    public function recordOption( string $option ) {
232        $this->mParseUsedOptions[$option] = true;
233    }
234
235    /**
236     * Tags a list of parser option names for use in the cache key for this parser output.
237     *
238     * @see recordOption()
239     * @param string[] $options
240     */
241    public function recordOptions( array $options ) {
242        $this->mParseUsedOptions = array_merge(
243            $this->mParseUsedOptions,
244            array_fill_keys( $options, true )
245        );
246    }
247
248    /**
249     * Returns a JSON serializable structure representing this CacheTime instance.
250     * @see newFromJson()
251     *
252     * @return array
253     */
254    protected function toJsonArray(): array {
255        // WARNING: When changing how this class is serialized, follow the instructions
256        // at <https://www.mediawiki.org/wiki/Manual:Parser_cache/Serialization_compatibility>!
257
258        return [
259            'ParseUsedOptions' => $this->mParseUsedOptions,
260            'CacheExpiry' => $this->mCacheExpiry,
261            'CacheTime' => $this->mCacheTime,
262            'CacheRevisionId' => $this->mCacheRevisionId,
263        ];
264    }
265
266    public static function newFromJsonArray( JsonDeserializer $deserializer, array $json ) {
267        $cacheTime = new CacheTime();
268        $cacheTime->initFromJson( $deserializer, $json );
269        return $cacheTime;
270    }
271
272    /**
273     * Initialize member fields from an array returned by jsonSerialize().
274     * @param JsonDeserializer $deserializer Unused
275     * @param array $jsonData
276     */
277    protected function initFromJson( JsonDeserializer $deserializer, array $jsonData ) {
278        // WARNING: When changing how this class is serialized, follow the instructions
279        // at <https://www.mediawiki.org/wiki/Manual:Parser_cache/Serialization_compatibility>!
280
281        if ( array_key_exists( 'AccessedOptions', $jsonData ) ) {
282            // Backwards compatibility for ParserOutput
283            $this->mParseUsedOptions = $jsonData['AccessedOptions'] ?: [];
284        } elseif ( array_key_exists( 'UsedOptions', $jsonData ) ) {
285            // Backwards compatibility
286            $this->recordOptions( $jsonData['UsedOptions'] ?: [] );
287        } else {
288            $this->mParseUsedOptions = $jsonData['ParseUsedOptions'] ?: [];
289        }
290        $this->mCacheExpiry = $jsonData['CacheExpiry'];
291        $this->mCacheTime = $jsonData['CacheTime'];
292        $this->mCacheRevisionId = $jsonData['CacheRevisionId'];
293    }
294
295    public function __wakeup() {
296        // Backwards compatibility, pre 1.36
297        $priorOptions = $this->getGhostFieldValue( 'mUsedOptions' );
298        if ( $priorOptions ) {
299            $this->recordOptions( $priorOptions );
300        }
301    }
302
303    public function __get( $name ) {
304        if ( property_exists( get_called_class(), $name ) ) {
305            // Direct access to a public property, deprecated.
306            wfDeprecatedMsg( "CacheTime::{$name} public read access deprecated", '1.38' );
307            return $this->$name;
308        } elseif ( property_exists( $this, $name ) ) {
309            // Dynamic property access, deprecated.
310            wfDeprecatedMsg( "CacheTime::{$name} dynamic property read access deprecated", '1.38' );
311            return $this->$name;
312        } else {
313            trigger_error( "Inaccessible property via __set(): $name" );
314            return null;
315        }
316    }
317
318    public function __set( $name, $value ) {
319        if ( property_exists( get_called_class(), $name ) ) {
320            // Direct access to a public property, deprecated.
321            wfDeprecatedMsg( "CacheTime::$name public write access deprecated", '1.38' );
322            $this->$name = $value;
323        } else {
324            // Dynamic property access, deprecated.
325            wfDeprecatedMsg( "CacheTime::$name dynamic property write access deprecated", '1.38' );
326            $this->$name = $value;
327        }
328    }
329}
330
331/** @deprecated class alias since 1.43 */
332class_alias( CacheTime::class, 'CacheTime' );