Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
81.67% covered (warning)
81.67%
49 / 60
40.00% covered (danger)
40.00%
2 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
HTMLCacheUpdater
83.05% covered (warning)
83.05%
49 / 59
40.00% covered (danger)
40.00%
2 / 5
30.55
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 fieldHasFlag
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 purgeUrls
72.73% covered (warning)
72.73%
8 / 11
0.00% covered (danger)
0.00%
0 / 1
6.73
 purgeTitleUrls
78.95% covered (warning)
78.95%
15 / 19
0.00% covered (danger)
0.00%
0 / 1
10.93
 getUrls
86.96% covered (warning)
86.96%
20 / 23
0.00% covered (danger)
0.00%
0 / 1
9.18
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20
21namespace MediaWiki\Cache;
22
23use MediaWiki\Deferred\CdnCacheUpdate;
24use MediaWiki\Deferred\DeferredUpdates;
25use MediaWiki\Deferred\HtmlFileCacheUpdate;
26use MediaWiki\HookContainer\HookContainer;
27use MediaWiki\HookContainer\HookRunner;
28use MediaWiki\Page\PageIdentity;
29use MediaWiki\Page\PageReference;
30use MediaWiki\Title\TitleFactory;
31use Traversable;
32
33/**
34 * Class to invalidate the CDN and HTMLFileCache entries associated with URLs/titles
35 *
36 * @ingroup Cache
37 * @since 1.35
38 */
39class HTMLCacheUpdater {
40    /** @var int Seconds between initial and rebound purges; 0 if disabled */
41    private $reboundDelay;
42    /** @var bool Whether filesystem-based HTML output caching is enabled */
43    private $useFileCache;
44    /** @var int Max seconds for CDN to served cached objects without revalidation */
45    private $cdnMaxAge;
46
47    /** @var HookRunner */
48    private $hookRunner;
49
50    /** @var int Issue purge immediately and do not schedule a rebound purge */
51    public const PURGE_NAIVE = 0;
52    /**
53     * @var int Defer purge via PRESEND deferred updates. The pending DeferrableUpdate instances
54     *  will combined/de-duplicated into a single DeferrableUpdate instance; this lowers overhead
55     *  and improves HTTP PURGE request pipelining.
56     */
57    public const PURGE_PRESEND = 1;
58    /**
59     * @var int Upon purge, schedule a delayed CDN purge if rebound purges are enabled
60     *  ($wgCdnReboundPurgeDelay). Rebound purges are schedule via the job queue.
61     */
62    public const PURGE_REBOUND = 2;
63
64    /**
65     * @var int Defer purge until no LBFactory transaction round is pending and then schedule
66     *  a delayed rebound purge if rebound purges are enabled ($wgCdnReboundPurgeDelay). This is
67     *  useful for CDN purges triggered by data changes in the current or last transaction round.
68     *  Even if the origin server uses lagged replicas, the use of rebound purges corrects the
69     *  cache in cases where lag is less than the rebound delay. If the lag is anywhere near the
70     *  rebound delay, then auxiliary mechanisms should lower the cache TTL ($wgCdnMaxageLagged).
71     */
72    public const PURGE_INTENT_TXROUND_REFLECTED = self::PURGE_PRESEND | self::PURGE_REBOUND;
73
74    /**
75     * Reduce set of URLs to be purged to only those that may be affected by
76     * change propagation from LinksUpdate (e.g. after a used template was edited).
77     *
78     * Specifically, this means URLs only affected by direct revision edits,
79     * will not be purged.
80     * @var int
81     */
82    public const PURGE_URLS_LINKSUPDATE_ONLY = 4;
83
84    /**
85     * Do not bother purging cache items if the default max cache TTL implies that no objects
86     * can still be in cache from before the given timestamp.
87     *
88     * @var string
89     */
90    public const UNLESS_CACHE_MTIME_AFTER = 'unless-timestamp-exceeds';
91
92    /** @var TitleFactory */
93    private $titleFactory;
94
95    /**
96     * @param HookContainer $hookContainer
97     * @param TitleFactory $titleFactory
98     * @param int $reboundDelay $wgCdnReboundPurgeDelay
99     * @param bool $useFileCache $wgUseFileCache
100     * @param int $cdnMaxAge $wgCdnMaxAge
101     *
102     * @internal For use with MediaWikiServices->getHtmlCacheUpdater()
103     */
104    public function __construct(
105        HookContainer $hookContainer,
106        TitleFactory $titleFactory,
107        $reboundDelay,
108        $useFileCache,
109        $cdnMaxAge
110    ) {
111        $this->hookRunner = new HookRunner( $hookContainer );
112        $this->titleFactory = $titleFactory;
113        $this->reboundDelay = $reboundDelay;
114        $this->useFileCache = $useFileCache;
115        $this->cdnMaxAge = $cdnMaxAge;
116    }
117
118    /**
119     * @param int $flags Bit field
120     * @param int $flag Constant to check for
121     * @return bool If $flags contains $flag
122     */
123    private function fieldHasFlag( $flags, $flag ) {
124        return ( ( $flags & $flag ) === $flag );
125    }
126
127    /**
128     * Purge the CDN for a URL or list of URLs
129     *
130     * @param string[]|string $urls URL or list of URLs
131     * @param int $flags Bit field of class PURGE_* constants
132     *  [Default: HTMLCacheUpdater::PURGE_PRESEND]
133     * @param mixed[] $unless Optional map of (HTMLCacheUpdater::UNLESS_* constant => value)
134     */
135    public function purgeUrls( $urls, $flags = self::PURGE_PRESEND, array $unless = [] ) {
136        $minFreshCacheMtime = $unless[self::UNLESS_CACHE_MTIME_AFTER] ?? null;
137        if ( $minFreshCacheMtime && time() > ( $minFreshCacheMtime + $this->cdnMaxAge ) ) {
138            return;
139        }
140
141        $urls = is_string( $urls ) ? [ $urls ] : $urls;
142
143        $reboundDelay = $this->fieldHasFlag( $flags, self::PURGE_REBOUND )
144            ? $this->reboundDelay
145            : 0; // no second purge
146
147        $update = new CdnCacheUpdate( $urls, [ 'reboundDelay' => $reboundDelay ] );
148        if ( $this->fieldHasFlag( $flags, self::PURGE_PRESEND ) ) {
149            DeferredUpdates::addUpdate( $update, DeferredUpdates::PRESEND );
150        } else {
151            $update->doUpdate();
152        }
153    }
154
155    /**
156     * Purge the CDN/HTMLFileCache for a title or the titles yielded by an iterator
157     *
158     * All cacheable canonical URLs associated with the titles will be purged from CDN.
159     * All cacheable actions associated with the titles will be purged from HTMLFileCache.
160     *
161     * @param Traversable|PageReference[]|PageReference $pages PageReference or iterator yielding
162     *        PageReference instances
163     * @param int $flags Bit field of class PURGE_* constants
164     *  [Default: HTMLCacheUpdater::PURGE_PRESEND]
165     * @param mixed[] $unless Optional map of (HTMLCacheUpdater::UNLESS_* constant => value)
166     */
167    public function purgeTitleUrls( $pages, $flags = self::PURGE_PRESEND, array $unless = [] ) {
168        $pages = is_iterable( $pages ) ? $pages : [ $pages ];
169        $pageIdentities = [];
170
171        foreach ( $pages as $page ) {
172            // TODO: We really only need to cast to PageIdentity. We could use a LinkBatch for that.
173            $title = $this->titleFactory->newFromPageReference( $page );
174
175            if ( $title->canExist() ) {
176                $pageIdentities[] = $title;
177            }
178        }
179
180        if ( !$pageIdentities ) {
181            return;
182        }
183
184        if ( $this->useFileCache ) {
185            $update = HtmlFileCacheUpdate::newFromPages( $pageIdentities );
186            if ( $this->fieldHasFlag( $flags, self::PURGE_PRESEND ) ) {
187                DeferredUpdates::addUpdate( $update, DeferredUpdates::PRESEND );
188            } else {
189                $update->doUpdate();
190            }
191        }
192
193        $minFreshCacheMtime = $unless[self::UNLESS_CACHE_MTIME_AFTER] ?? null;
194        if ( !$minFreshCacheMtime || time() <= ( $minFreshCacheMtime + $this->cdnMaxAge ) ) {
195            $urls = [];
196            foreach ( $pageIdentities as $pi ) {
197                /** @var PageIdentity $pi */
198                $urls = array_merge( $urls, $this->getUrls( $pi, $flags ) );
199            }
200            $this->purgeUrls( $urls, $flags );
201        }
202    }
203
204    /**
205     * Get a list of URLs to purge from the CDN cache when this page changes.
206     *
207     * @param PageReference $page
208     * @param int $flags Bit field of `PURGE_URLS_*` class constants (optional).
209     * @return string[] URLs
210     */
211    public function getUrls( PageReference $page, int $flags = 0 ): array {
212        $title = $this->titleFactory->newFromPageReference( $page );
213
214        if ( !$title->canExist() ) {
215            return [];
216        }
217
218        // These urls are affected both by direct revisions as well,
219        // as re-rendering of the same content during a LinksUpdate.
220        $urls = [
221            $title->getInternalURL()
222        ];
223        // Language variant page views are currently not cached
224        // and thus not purged (T250511).
225
226        // These urls are only affected by direct revisions, and do not require
227        // purging when a LinksUpdate merely rerenders the same content.
228        // This exists to avoid large amounts of redundant PURGE traffic (T250261).
229        if ( !$this->fieldHasFlag( $flags, self::PURGE_URLS_LINKSUPDATE_ONLY ) ) {
230            $urls[] = $title->getInternalURL( 'action=history' );
231
232            // Canonical action=raw URLs for user and site config pages (T58874, T261371).
233            if ( $title->isUserJsConfigPage() || $title->isSiteJsConfigPage() ) {
234                $urls[] = $title->getInternalURL( 'action=raw&ctype=text/javascript' );
235            } elseif ( $title->isUserJsonConfigPage() || $title->isSiteJsonConfigPage() ) {
236                $urls[] = $title->getInternalURL( 'action=raw&ctype=application/json' );
237            } elseif ( $title->isUserCssConfigPage() || $title->isSiteCssConfigPage() ) {
238                $urls[] = $title->getInternalURL( 'action=raw&ctype=text/css' );
239            }
240        }
241
242        // Extensions may add novel ways to access this content
243        $append = [];
244        $mode = $flags & self::PURGE_URLS_LINKSUPDATE_ONLY;
245        $this->hookRunner->onHtmlCacheUpdaterAppendUrls( $title, $mode, $append );
246        $urls = array_merge( $urls, $append );
247
248        // Extensions may add novel ways to access the site overall
249        $append = [];
250        $this->hookRunner->onHtmlCacheUpdaterVaryUrls( $urls, $append );
251        $urls = array_merge( $urls, $append );
252
253        // Legacy. TODO: Deprecate this
254        $this->hookRunner->onTitleSquidURLs( $title, $urls );
255
256        return $urls;
257    }
258}
259
260/** @deprecated class alias since 1.42 */
261class_alias( HTMLCacheUpdater::class, 'HtmlCacheUpdater' );