Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 74 |
|
0.00% |
0 / 16 |
CRAP | |
0.00% |
0 / 1 |
FileCacheBase | |
0.00% |
0 / 73 |
|
0.00% |
0 / 16 |
930 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
baseCacheDirectory | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
cacheDirectory | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
cachePath | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
isCached | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
cacheTimestamp | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
isCacheGood | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
useGzip | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
fetchText | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
saveText | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
clearCache | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
checkCacheDirs | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
typeSubdirectory | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
hashSubdirectory | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
incrMissesRecent | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
30 | |||
getMissesRecent | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
cacheMissKey | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * Data storage in the file system. |
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 Cache |
22 | */ |
23 | |
24 | namespace MediaWiki\Cache; |
25 | |
26 | use MediaWiki\Config\ServiceOptions; |
27 | use MediaWiki\MainConfigNames; |
28 | use MediaWiki\MediaWikiServices; |
29 | use MediaWiki\Request\WebRequest; |
30 | use Wikimedia\AtEase\AtEase; |
31 | use Wikimedia\IPUtils; |
32 | use Wikimedia\ObjectCache\BagOStuff; |
33 | |
34 | /** |
35 | * Base class for data storage in the file system. |
36 | * |
37 | * @ingroup Cache |
38 | */ |
39 | abstract class FileCacheBase { |
40 | |
41 | private const CONSTRUCTOR_OPTIONS = [ |
42 | MainConfigNames::CacheEpoch, |
43 | MainConfigNames::FileCacheDepth, |
44 | MainConfigNames::FileCacheDirectory, |
45 | MainConfigNames::MimeType, |
46 | MainConfigNames::UseGzip, |
47 | ]; |
48 | |
49 | /** @var string */ |
50 | protected $mKey; |
51 | /** @var string */ |
52 | protected $mType = 'object'; |
53 | /** @var string */ |
54 | protected $mExt = 'cache'; |
55 | /** @var string|null */ |
56 | protected $mFilePath; |
57 | /** @var bool */ |
58 | protected $mUseGzip; |
59 | /** @var bool|null lazy loaded */ |
60 | protected $mCached; |
61 | /** @var ServiceOptions */ |
62 | protected $options; |
63 | |
64 | /* @todo configurable? */ |
65 | private const MISS_FACTOR = 15; // log 1 every MISS_FACTOR cache misses |
66 | private const MISS_TTL_SEC = 3600; // how many seconds ago is "recent" |
67 | |
68 | protected function __construct() { |
69 | $this->options = new ServiceOptions( |
70 | self::CONSTRUCTOR_OPTIONS, |
71 | MediaWikiServices::getInstance()->getMainConfig() |
72 | ); |
73 | $this->mUseGzip = (bool)$this->options->get( MainConfigNames::UseGzip ); |
74 | } |
75 | |
76 | /** |
77 | * Get the base file cache directory |
78 | * @return string |
79 | */ |
80 | final protected function baseCacheDirectory() { |
81 | return $this->options->get( MainConfigNames::FileCacheDirectory ); |
82 | } |
83 | |
84 | /** |
85 | * Get the base cache directory (not specific to this file) |
86 | * @return string |
87 | */ |
88 | abstract protected function cacheDirectory(); |
89 | |
90 | /** |
91 | * Get the path to the cache file |
92 | * @return string |
93 | */ |
94 | protected function cachePath() { |
95 | if ( $this->mFilePath !== null ) { |
96 | return $this->mFilePath; |
97 | } |
98 | |
99 | $dir = $this->cacheDirectory(); |
100 | # Build directories (methods include the trailing "/") |
101 | $subDirs = $this->typeSubdirectory() . $this->hashSubdirectory(); |
102 | # Avoid extension confusion |
103 | $key = str_replace( '.', '%2E', urlencode( $this->mKey ) ); |
104 | # Build the full file path |
105 | $this->mFilePath = "{$dir}/{$subDirs}{$key}.{$this->mExt}"; |
106 | if ( $this->useGzip() ) { |
107 | $this->mFilePath .= '.gz'; |
108 | } |
109 | |
110 | return $this->mFilePath; |
111 | } |
112 | |
113 | /** |
114 | * Check if the cache file exists |
115 | * @return bool |
116 | */ |
117 | public function isCached() { |
118 | $this->mCached ??= is_file( $this->cachePath() ); |
119 | |
120 | return $this->mCached; |
121 | } |
122 | |
123 | /** |
124 | * Get the last-modified timestamp of the cache file |
125 | * @return string|bool TS_MW timestamp |
126 | */ |
127 | public function cacheTimestamp() { |
128 | $timestamp = filemtime( $this->cachePath() ); |
129 | |
130 | return ( $timestamp !== false ) |
131 | ? wfTimestamp( TS_MW, $timestamp ) |
132 | : false; |
133 | } |
134 | |
135 | /** |
136 | * Check if up to date cache file exists |
137 | * @param string $timestamp MW_TS timestamp |
138 | * |
139 | * @return bool |
140 | */ |
141 | public function isCacheGood( $timestamp = '' ) { |
142 | $cacheEpoch = $this->options->get( MainConfigNames::CacheEpoch ); |
143 | |
144 | if ( !$this->isCached() ) { |
145 | return false; |
146 | } |
147 | |
148 | $cachetime = $this->cacheTimestamp(); |
149 | $good = ( $timestamp <= $cachetime && $cacheEpoch <= $cachetime ); |
150 | wfDebug( __METHOD__ . |
151 | ": cachetime $cachetime, touched '{$timestamp}' epoch {$cacheEpoch}, good " . wfBoolToStr( $good ) ); |
152 | |
153 | return $good; |
154 | } |
155 | |
156 | /** |
157 | * Check if the cache is gzipped |
158 | * @return bool |
159 | */ |
160 | protected function useGzip() { |
161 | return $this->mUseGzip; |
162 | } |
163 | |
164 | /** |
165 | * Get the uncompressed text from the cache |
166 | * @return string |
167 | */ |
168 | public function fetchText() { |
169 | if ( $this->useGzip() ) { |
170 | $fh = gzopen( $this->cachePath(), 'rb' ); |
171 | |
172 | return stream_get_contents( $fh ); |
173 | } else { |
174 | return file_get_contents( $this->cachePath() ); |
175 | } |
176 | } |
177 | |
178 | /** |
179 | * Save and compress text to the cache |
180 | * @param string $text |
181 | * @return string|false Compressed text |
182 | */ |
183 | public function saveText( $text ) { |
184 | if ( $this->useGzip() ) { |
185 | $text = gzencode( $text ); |
186 | } |
187 | |
188 | $this->checkCacheDirs(); // build parent dir |
189 | if ( !file_put_contents( $this->cachePath(), $text, LOCK_EX ) ) { |
190 | wfDebug( __METHOD__ . "() failed saving " . $this->cachePath() ); |
191 | $this->mCached = null; |
192 | |
193 | return false; |
194 | } |
195 | |
196 | $this->mCached = true; |
197 | |
198 | return $text; |
199 | } |
200 | |
201 | /** |
202 | * Clear the cache for this page |
203 | * @return void |
204 | */ |
205 | public function clearCache() { |
206 | AtEase::suppressWarnings(); |
207 | unlink( $this->cachePath() ); |
208 | AtEase::restoreWarnings(); |
209 | $this->mCached = false; |
210 | } |
211 | |
212 | /** |
213 | * Create parent directors of $this->cachePath() |
214 | * @return void |
215 | */ |
216 | protected function checkCacheDirs() { |
217 | wfMkdirParents( dirname( $this->cachePath() ), null, __METHOD__ ); |
218 | } |
219 | |
220 | /** |
221 | * Get the cache type subdirectory (with trailing slash) |
222 | * An extending class could use that method to alter the type -> directory |
223 | * mapping. @see HTMLFileCache::typeSubdirectory() for an example. |
224 | * |
225 | * @return string |
226 | */ |
227 | protected function typeSubdirectory() { |
228 | return $this->mType . '/'; |
229 | } |
230 | |
231 | /** |
232 | * Return relative multi-level hash subdirectory (with trailing slash) |
233 | * or the empty string if not $wgFileCacheDepth |
234 | * @return string |
235 | */ |
236 | protected function hashSubdirectory() { |
237 | $fileCacheDepth = $this->options->get( MainConfigNames::FileCacheDepth ); |
238 | |
239 | $subdir = ''; |
240 | if ( $fileCacheDepth > 0 ) { |
241 | $hash = md5( $this->mKey ); |
242 | for ( $i = 1; $i <= $fileCacheDepth; $i++ ) { |
243 | $subdir .= substr( $hash, 0, $i ) . '/'; |
244 | } |
245 | } |
246 | |
247 | return $subdir; |
248 | } |
249 | |
250 | /** |
251 | * Roughly increments the cache misses in the last hour by unique visitors |
252 | * @param WebRequest $request |
253 | * @return void |
254 | */ |
255 | public function incrMissesRecent( WebRequest $request ) { |
256 | if ( mt_rand( 1, self::MISS_FACTOR ) == 1 ) { |
257 | # Get a large IP range that should include the user even if that |
258 | # person's IP address changes |
259 | $ip = $request->getIP(); |
260 | if ( !IPUtils::isValid( $ip ) ) { |
261 | return; |
262 | } |
263 | |
264 | $ip = IPUtils::isIPv6( $ip ) |
265 | ? IPUtils::sanitizeRange( "$ip/32" ) |
266 | : IPUtils::sanitizeRange( "$ip/16" ); |
267 | |
268 | # Bail out if a request already came from this range... |
269 | $cache = MediaWikiServices::getInstance()->getObjectCacheFactory() |
270 | ->getLocalClusterInstance(); |
271 | $key = $cache->makeKey( static::class, 'attempt', $this->mType, $this->mKey, $ip ); |
272 | if ( !$cache->add( $key, 1, self::MISS_TTL_SEC ) ) { |
273 | return; // possibly the same user |
274 | } |
275 | |
276 | # Increment the number of cache misses... |
277 | $cache->incrWithInit( $this->cacheMissKey( $cache ), self::MISS_TTL_SEC ); |
278 | } |
279 | } |
280 | |
281 | /** |
282 | * Roughly gets the cache misses in the last hour by unique visitors |
283 | * @return int |
284 | */ |
285 | public function getMissesRecent() { |
286 | $cache = MediaWikiServices::getInstance()->getObjectCacheFactory() |
287 | ->getLocalClusterInstance(); |
288 | |
289 | return self::MISS_FACTOR * $cache->get( $this->cacheMissKey( $cache ) ); |
290 | } |
291 | |
292 | /** |
293 | * @param BagOStuff $cache Instance that the key will be used with |
294 | * @return string |
295 | */ |
296 | protected function cacheMissKey( BagOStuff $cache ) { |
297 | return $cache->makeKey( static::class, 'misses', $this->mType, $this->mKey ); |
298 | } |
299 | } |
300 | |
301 | /** @deprecated class alias since 1.42 */ |
302 | class_alias( FileCacheBase::class, 'FileCacheBase' ); |