(bug 17602) fix Monobook action tabs not quite touching the page body
[lhc/web/wiklou.git] / includes / cache / FileCacheBase.php
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 /**
25 * Base class for data storage in the file system.
26 *
27 * @ingroup Cache
28 */
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;
37
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"
41
42 protected function __construct() {
43 global $wgUseGzip;
44
45 $this->mUseGzip = (bool)$wgUseGzip;
46 }
47
48 /**
49 * Get the base file cache directory
50 * @return string
51 */
52 final protected function baseCacheDirectory() {
53 global $wgFileCacheDirectory;
54 return $wgFileCacheDirectory;
55 }
56
57 /**
58 * Get the base cache directory (not specific to this file)
59 * @return string
60 */
61 abstract protected function cacheDirectory();
62
63 /**
64 * Get the path to the cache file
65 * @return string
66 */
67 protected function cachePath() {
68 if ( $this->mFilePath !== null ) {
69 return $this->mFilePath;
70 }
71
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';
81 }
82
83 return $this->mFilePath;
84 }
85
86 /**
87 * Check if the cache file exists
88 * @return bool
89 */
90 public function isCached() {
91 if ( $this->mCached === null ) {
92 $this->mCached = file_exists( $this->cachePath() );
93 }
94 return $this->mCached;
95 }
96
97 /**
98 * Get the last-modified timestamp of the cache file
99 * @return string|bool TS_MW timestamp
100 */
101 public function cacheTimestamp() {
102 $timestamp = filemtime( $this->cachePath() );
103 return ( $timestamp !== false )
104 ? wfTimestamp( TS_MW, $timestamp )
105 : false;
106 }
107
108 /**
109 * Check if up to date cache file exists
110 * @param string $timestamp MW_TS timestamp
111 *
112 * @return bool
113 */
114 public function isCacheGood( $timestamp = '' ) {
115 global $wgCacheEpoch;
116
117 if ( !$this->isCached() ) {
118 return false;
119 }
120
121 $cachetime = $this->cacheTimestamp();
122 $good = ( $timestamp <= $cachetime && $wgCacheEpoch <= $cachetime );
123 wfDebug( __METHOD__ . ": cachetime $cachetime, touched '{$timestamp}' epoch {$wgCacheEpoch}, good $good\n" );
124
125 return $good;
126 }
127
128 /**
129 * Check if the cache is gzipped
130 * @return bool
131 */
132 protected function useGzip() {
133 return $this->mUseGzip;
134 }
135
136 /**
137 * Get the uncompressed text from the cache
138 * @return string
139 */
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() );
146 }
147 }
148
149 /**
150 * Save and compress text to the cache
151 * @return string compressed text
152 */
153 public function saveText( $text ) {
154 global $wgUseFileCache;
155
156 if ( !$wgUseFileCache ) {
157 return false;
158 }
159
160 if ( $this->useGzip() ) {
161 $text = gzencode( $text );
162 }
163
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;
169 }
170
171 $this->mCached = true;
172 return $text;
173 }
174
175 /**
176 * Clear the cache for this page
177 * @return void
178 */
179 public function clearCache() {
180 wfSuppressWarnings();
181 unlink( $this->cachePath() );
182 wfRestoreWarnings();
183 $this->mCached = false;
184 }
185
186 /**
187 * Create parent directors of $this->cachePath()
188 * @return void
189 */
190 protected function checkCacheDirs() {
191 wfMkdirParents( dirname( $this->cachePath() ), null, __METHOD__ );
192 }
193
194 /**
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.
198 *
199 * @return string
200 */
201 protected function typeSubdirectory() {
202 return $this->mType . '/';
203 }
204
205 /**
206 * Return relative multi-level hash subdirectory (with trailing slash)
207 * or the empty string if not $wgFileCacheDepth
208 * @return string
209 */
210 protected function hashSubdirectory() {
211 global $wgFileCacheDepth;
212
213 $subdir = '';
214 if ( $wgFileCacheDepth > 0 ) {
215 $hash = md5( $this->mKey );
216 for ( $i = 1; $i <= $wgFileCacheDepth; $i++ ) {
217 $subdir .= substr( $hash, 0, $i ) . '/';
218 }
219 }
220
221 return $subdir;
222 }
223
224 /**
225 * Roughly increments the cache misses in the last hour by unique visitors
226 * @param $request WebRequest
227 * @return void
228 */
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;
237 }
238 $ip = IP::isIPv6( $ip )
239 ? IP::sanitizeRange( "$ip/32" )
240 : IP::sanitizeRange( "$ip/16" );
241
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
246 }
247 $wgMemc->set( $key, 1, self::MISS_TTL_SEC );
248
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 );
255 }
256 }
257 }
258
259 /**
260 * Roughly gets the cache misses in the last hour by unique visitors
261 * @return int
262 */
263 public function getMissesRecent() {
264 global $wgMemc;
265 return self::MISS_FACTOR * $wgMemc->get( $this->cacheMissKey() );
266 }
267
268 /**
269 * @return string
270 */
271 protected function cacheMissKey() {
272 return wfMemcKey( get_class( $this ), 'misses', $this->mType, $this->mKey );
273 }
274 }