Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
81.67% |
49 / 60 |
|
40.00% |
2 / 5 |
CRAP | |
0.00% |
0 / 1 |
HTMLCacheUpdater | |
83.05% |
49 / 59 |
|
40.00% |
2 / 5 |
30.55 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
fieldHasFlag | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
purgeUrls | |
72.73% |
8 / 11 |
|
0.00% |
0 / 1 |
6.73 | |||
purgeTitleUrls | |
78.95% |
15 / 19 |
|
0.00% |
0 / 1 |
10.93 | |||
getUrls | |
86.96% |
20 / 23 |
|
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 | |
21 | namespace MediaWiki\Cache; |
22 | |
23 | use MediaWiki\Deferred\CdnCacheUpdate; |
24 | use MediaWiki\Deferred\DeferredUpdates; |
25 | use MediaWiki\Deferred\HtmlFileCacheUpdate; |
26 | use MediaWiki\HookContainer\HookContainer; |
27 | use MediaWiki\HookContainer\HookRunner; |
28 | use MediaWiki\Page\PageIdentity; |
29 | use MediaWiki\Page\PageReference; |
30 | use MediaWiki\Title\TitleFactory; |
31 | use 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 | */ |
39 | class 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 */ |
261 | class_alias( HTMLCacheUpdater::class, 'HtmlCacheUpdater' ); |