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