Merge "Add ss_active_users in SiteStats::isSane"
[mediawiki.git] / includes / cache / FileCacheBase.php
blobd4bf5ee6387c84b418a398546b508a7a8144221d
1 <?php
2 /**
3 * Data storage in the file system.
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.
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.
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
20 * @file
21 * @ingroup Cache
24 /**
25 * Base class for data storage in the file system.
27 * @ingroup Cache
29 abstract class FileCacheBase {
30 protected $mKey;
31 protected $mType = 'object';
32 protected $mExt = 'cache';
33 protected $mFilePath;
34 protected $mUseGzip;
35 /* lazy loaded */
36 protected $mCached;
38 /* @todo configurable? */
39 const MISS_FACTOR = 15; // log 1 every MISS_FACTOR cache misses
40 const MISS_TTL_SEC = 3600; // how many seconds ago is "recent"
42 protected function __construct() {
43 global $wgUseGzip;
45 $this->mUseGzip = (bool)$wgUseGzip;
48 /**
49 * Get the base file cache directory
50 * @return string
52 final protected function baseCacheDirectory() {
53 global $wgFileCacheDirectory;
54 return $wgFileCacheDirectory;
57 /**
58 * Get the base cache directory (not specific to this file)
59 * @return string
61 abstract protected function cacheDirectory();
63 /**
64 * Get the path to the cache file
65 * @return string
67 protected function cachePath() {
68 if ( $this->mFilePath !== null ) {
69 return $this->mFilePath;
72 $dir = $this->cacheDirectory();
73 # Build directories (methods include the trailing "/")
74 $subDirs = $this->typeSubdirectory() . $this->hashSubdirectory();
75 # Avoid extension confusion
76 $key = str_replace( '.', '%2E', urlencode( $this->mKey ) );
77 # Build the full file path
78 $this->mFilePath = "{$dir}/{$subDirs}{$key}.{$this->mExt}";
79 if ( $this->useGzip() ) {
80 $this->mFilePath .= '.gz';
83 return $this->mFilePath;
86 /**
87 * Check if the cache file exists
88 * @return bool
90 public function isCached() {
91 if ( $this->mCached === null ) {
92 $this->mCached = file_exists( $this->cachePath() );
94 return $this->mCached;
97 /**
98 * Get the last-modified timestamp of the cache file
99 * @return string|bool TS_MW timestamp
101 public function cacheTimestamp() {
102 $timestamp = filemtime( $this->cachePath() );
103 return ( $timestamp !== false )
104 ? wfTimestamp( TS_MW, $timestamp )
105 : false;
109 * Check if up to date cache file exists
110 * @param string $timestamp MW_TS timestamp
112 * @return bool
114 public function isCacheGood( $timestamp = '' ) {
115 global $wgCacheEpoch;
117 if ( !$this->isCached() ) {
118 return false;
121 $cachetime = $this->cacheTimestamp();
122 $good = ( $timestamp <= $cachetime && $wgCacheEpoch <= $cachetime );
123 wfDebug( __METHOD__ . ": cachetime $cachetime, touched '{$timestamp}' epoch {$wgCacheEpoch}, good $good\n" );
125 return $good;
129 * Check if the cache is gzipped
130 * @return bool
132 protected function useGzip() {
133 return $this->mUseGzip;
137 * Get the uncompressed text from the cache
138 * @return string
140 public function fetchText() {
141 if ( $this->useGzip() ) {
142 $fh = gzopen( $this->cachePath(), 'rb' );
143 return stream_get_contents( $fh );
144 } else {
145 return file_get_contents( $this->cachePath() );
150 * Save and compress text to the cache
151 * @return string compressed text
153 public function saveText( $text ) {
154 global $wgUseFileCache;
156 if ( !$wgUseFileCache ) {
157 return false;
160 if ( $this->useGzip() ) {
161 $text = gzencode( $text );
164 $this->checkCacheDirs(); // build parent dir
165 if ( !file_put_contents( $this->cachePath(), $text, LOCK_EX ) ) {
166 wfDebug( __METHOD__ . "() failed saving " . $this->cachePath() . "\n" );
167 $this->mCached = null;
168 return false;
171 $this->mCached = true;
172 return $text;
176 * Clear the cache for this page
177 * @return void
179 public function clearCache() {
180 wfSuppressWarnings();
181 unlink( $this->cachePath() );
182 wfRestoreWarnings();
183 $this->mCached = false;
187 * Create parent directors of $this->cachePath()
188 * @return void
190 protected function checkCacheDirs() {
191 wfMkdirParents( dirname( $this->cachePath() ), null, __METHOD__ );
195 * Get the cache type subdirectory (with trailing slash)
196 * An extending class could use that method to alter the type -> directory
197 * mapping. @see HTMLFileCache::typeSubdirectory() for an example.
199 * @return string
201 protected function typeSubdirectory() {
202 return $this->mType . '/';
206 * Return relative multi-level hash subdirectory (with trailing slash)
207 * or the empty string if not $wgFileCacheDepth
208 * @return string
210 protected function hashSubdirectory() {
211 global $wgFileCacheDepth;
213 $subdir = '';
214 if ( $wgFileCacheDepth > 0 ) {
215 $hash = md5( $this->mKey );
216 for ( $i = 1; $i <= $wgFileCacheDepth; $i++ ) {
217 $subdir .= substr( $hash, 0, $i ) . '/';
221 return $subdir;
225 * Roughly increments the cache misses in the last hour by unique visitors
226 * @param $request WebRequest
227 * @return void
229 public function incrMissesRecent( WebRequest $request ) {
230 global $wgMemc;
231 if ( mt_rand( 0, self::MISS_FACTOR - 1 ) == 0 ) {
232 # Get a large IP range that should include the user even if that
233 # person's IP address changes
234 $ip = $request->getIP();
235 if ( !IP::isValid( $ip ) ) {
236 return;
238 $ip = IP::isIPv6( $ip )
239 ? IP::sanitizeRange( "$ip/32" )
240 : IP::sanitizeRange( "$ip/16" );
242 # Bail out if a request already came from this range...
243 $key = wfMemcKey( get_class( $this ), 'attempt', $this->mType, $this->mKey, $ip );
244 if ( $wgMemc->get( $key ) ) {
245 return; // possibly the same user
247 $wgMemc->set( $key, 1, self::MISS_TTL_SEC );
249 # Increment the number of cache misses...
250 $key = $this->cacheMissKey();
251 if ( $wgMemc->get( $key ) === false ) {
252 $wgMemc->set( $key, 1, self::MISS_TTL_SEC );
253 } else {
254 $wgMemc->incr( $key );
260 * Roughly gets the cache misses in the last hour by unique visitors
261 * @return int
263 public function getMissesRecent() {
264 global $wgMemc;
265 return self::MISS_FACTOR * $wgMemc->get( $this->cacheMissKey() );
269 * @return string
271 protected function cacheMissKey() {
272 return wfMemcKey( get_class( $this ), 'misses', $this->mType, $this->mKey );