Completed
Branch master (174b3a)
by
unknown
26:51
created

FileCacheBase::saveText()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 10
nc 4
nop 1
dl 0
loc 17
rs 9.4285
c 0
b 0
f 0
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
55
		return $wgFileCacheDirectory;
56
	}
57
58
	/**
59
	 * Get the base cache directory (not specific to this file)
60
	 * @return string
61
	 */
62
	abstract protected function cacheDirectory();
63
64
	/**
65
	 * Get the path to the cache file
66
	 * @return string
67
	 */
68
	protected function cachePath() {
69
		if ( $this->mFilePath !== null ) {
70
			return $this->mFilePath;
71
		}
72
73
		$dir = $this->cacheDirectory();
74
		# Build directories (methods include the trailing "/")
75
		$subDirs = $this->typeSubdirectory() . $this->hashSubdirectory();
76
		# Avoid extension confusion
77
		$key = str_replace( '.', '%2E', urlencode( $this->mKey ) );
78
		# Build the full file path
79
		$this->mFilePath = "{$dir}/{$subDirs}{$key}.{$this->mExt}";
80
		if ( $this->useGzip() ) {
81
			$this->mFilePath .= '.gz';
82
		}
83
84
		return $this->mFilePath;
85
	}
86
87
	/**
88
	 * Check if the cache file exists
89
	 * @return bool
90
	 */
91
	public function isCached() {
92
		if ( $this->mCached === null ) {
93
			$this->mCached = file_exists( $this->cachePath() );
94
		}
95
96
		return $this->mCached;
97
	}
98
99
	/**
100
	 * Get the last-modified timestamp of the cache file
101
	 * @return string|bool TS_MW timestamp
102
	 */
103
	public function cacheTimestamp() {
104
		$timestamp = filemtime( $this->cachePath() );
105
106
		return ( $timestamp !== false )
107
			? wfTimestamp( TS_MW, $timestamp )
108
			: false;
109
	}
110
111
	/**
112
	 * Check if up to date cache file exists
113
	 * @param string $timestamp MW_TS timestamp
114
	 *
115
	 * @return bool
116
	 */
117
	public function isCacheGood( $timestamp = '' ) {
118
		global $wgCacheEpoch;
119
120
		if ( !$this->isCached() ) {
121
			return false;
122
		}
123
124
		$cachetime = $this->cacheTimestamp();
125
		$good = ( $timestamp <= $cachetime && $wgCacheEpoch <= $cachetime );
126
		wfDebug( __METHOD__ .
127
			": cachetime $cachetime, touched '{$timestamp}' epoch {$wgCacheEpoch}, good $good\n" );
128
129
		return $good;
130
	}
131
132
	/**
133
	 * Check if the cache is gzipped
134
	 * @return bool
135
	 */
136
	protected function useGzip() {
137
		return $this->mUseGzip;
138
	}
139
140
	/**
141
	 * Get the uncompressed text from the cache
142
	 * @return string
143
	 */
144
	public function fetchText() {
145
		if ( $this->useGzip() ) {
146
			$fh = gzopen( $this->cachePath(), 'rb' );
147
148
			return stream_get_contents( $fh );
149
		} else {
150
			return file_get_contents( $this->cachePath() );
151
		}
152
	}
153
154
	/**
155
	 * Save and compress text to the cache
156
	 * @param string $text
157
	 * @return string Compressed text
158
	 */
159
	public function saveText( $text ) {
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
169
			return false;
170
		}
171
172
		$this->mCached = true;
173
174
		return $text;
175
	}
176
177
	/**
178
	 * Clear the cache for this page
179
	 * @return void
180
	 */
181
	public function clearCache() {
182
		MediaWiki\suppressWarnings();
183
		unlink( $this->cachePath() );
184
		MediaWiki\restoreWarnings();
185
		$this->mCached = false;
186
	}
187
188
	/**
189
	 * Create parent directors of $this->cachePath()
190
	 * @return void
191
	 */
192
	protected function checkCacheDirs() {
193
		wfMkdirParents( dirname( $this->cachePath() ), null, __METHOD__ );
194
	}
195
196
	/**
197
	 * Get the cache type subdirectory (with trailing slash)
198
	 * An extending class could use that method to alter the type -> directory
199
	 * mapping. @see HTMLFileCache::typeSubdirectory() for an example.
200
	 *
201
	 * @return string
202
	 */
203
	protected function typeSubdirectory() {
204
		return $this->mType . '/';
205
	}
206
207
	/**
208
	 * Return relative multi-level hash subdirectory (with trailing slash)
209
	 * or the empty string if not $wgFileCacheDepth
210
	 * @return string
211
	 */
212 View Code Duplication
	protected function hashSubdirectory() {
213
		global $wgFileCacheDepth;
214
215
		$subdir = '';
216
		if ( $wgFileCacheDepth > 0 ) {
217
			$hash = md5( $this->mKey );
218
			for ( $i = 1; $i <= $wgFileCacheDepth; $i++ ) {
219
				$subdir .= substr( $hash, 0, $i ) . '/';
220
			}
221
		}
222
223
		return $subdir;
224
	}
225
226
	/**
227
	 * Roughly increments the cache misses in the last hour by unique visitors
228
	 * @param WebRequest $request
229
	 * @return void
230
	 */
231
	public function incrMissesRecent( WebRequest $request ) {
232
		if ( mt_rand( 0, self::MISS_FACTOR - 1 ) == 0 ) {
233
			$cache = ObjectCache::getLocalClusterInstance();
234
			# Get a large IP range that should include the user  even if that
235
			# person's IP address changes
236
			$ip = $request->getIP();
237
			if ( !IP::isValid( $ip ) ) {
238
				return;
239
			}
240
			$ip = IP::isIPv6( $ip )
241
				? IP::sanitizeRange( "$ip/32" )
242
				: IP::sanitizeRange( "$ip/16" );
243
244
			# Bail out if a request already came from this range...
245
			$key = wfMemcKey( get_class( $this ), 'attempt', $this->mType, $this->mKey, $ip );
246
			if ( $cache->get( $key ) ) {
247
				return; // possibly the same user
248
			}
249
			$cache->set( $key, 1, self::MISS_TTL_SEC );
250
251
			# Increment the number of cache misses...
252
			$key = $this->cacheMissKey();
253
			if ( $cache->get( $key ) === false ) {
254
				$cache->set( $key, 1, self::MISS_TTL_SEC );
255
			} else {
256
				$cache->incr( $key );
257
			}
258
		}
259
	}
260
261
	/**
262
	 * Roughly gets the cache misses in the last hour by unique visitors
263
	 * @return int
264
	 */
265
	public function getMissesRecent() {
266
		$cache = ObjectCache::getLocalClusterInstance();
267
268
		return self::MISS_FACTOR * $cache->get( $this->cacheMissKey() );
269
	}
270
271
	/**
272
	 * @return string
273
	 */
274
	protected function cacheMissKey() {
275
		return wfMemcKey( get_class( $this ), 'misses', $this->mType, $this->mKey );
276
	}
277
}
278