|
1 <?php |
|
2 /** |
|
3 * Zend Framework |
|
4 * |
|
5 * LICENSE |
|
6 * |
|
7 * This source file is subject to the new BSD license that is bundled |
|
8 * with this package in the file LICENSE.txt. |
|
9 * It is also available through the world-wide-web at this URL: |
|
10 * http://framework.zend.com/license/new-bsd |
|
11 * If you did not receive a copy of the license and are unable to |
|
12 * obtain it through the world-wide-web, please send an email |
|
13 * to license@zend.com so we can send you a copy immediately. |
|
14 * |
|
15 * @category Zend |
|
16 * @package Zend_Cache |
|
17 * @subpackage Zend_Cache_Backend |
|
18 * @copyright Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com) |
|
19 * @license http://framework.zend.com/license/new-bsd New BSD License |
|
20 * @version $Id: Static.php 22950 2010-09-16 19:33:00Z mabe $ |
|
21 */ |
|
22 |
|
23 /** |
|
24 * @see Zend_Cache_Backend_Interface |
|
25 */ |
|
26 require_once 'Zend/Cache/Backend/Interface.php'; |
|
27 |
|
28 /** |
|
29 * @see Zend_Cache_Backend |
|
30 */ |
|
31 require_once 'Zend/Cache/Backend.php'; |
|
32 |
|
33 /** |
|
34 * @package Zend_Cache |
|
35 * @subpackage Zend_Cache_Backend |
|
36 * @copyright Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com) |
|
37 * @license http://framework.zend.com/license/new-bsd New BSD License |
|
38 */ |
|
39 class Zend_Cache_Backend_Static |
|
40 extends Zend_Cache_Backend |
|
41 implements Zend_Cache_Backend_Interface |
|
42 { |
|
43 const INNER_CACHE_NAME = 'zend_cache_backend_static_tagcache'; |
|
44 |
|
45 /** |
|
46 * Static backend options |
|
47 * @var array |
|
48 */ |
|
49 protected $_options = array( |
|
50 'public_dir' => null, |
|
51 'sub_dir' => 'html', |
|
52 'file_extension' => '.html', |
|
53 'index_filename' => 'index', |
|
54 'file_locking' => true, |
|
55 'cache_file_umask' => 0600, |
|
56 'cache_directory_umask' => 0700, |
|
57 'debug_header' => false, |
|
58 'tag_cache' => null, |
|
59 'disable_caching' => false |
|
60 ); |
|
61 |
|
62 /** |
|
63 * Cache for handling tags |
|
64 * @var Zend_Cache_Core |
|
65 */ |
|
66 protected $_tagCache = null; |
|
67 |
|
68 /** |
|
69 * Tagged items |
|
70 * @var array |
|
71 */ |
|
72 protected $_tagged = null; |
|
73 |
|
74 /** |
|
75 * Interceptor child method to handle the case where an Inner |
|
76 * Cache object is being set since it's not supported by the |
|
77 * standard backend interface |
|
78 * |
|
79 * @param string $name |
|
80 * @param mixed $value |
|
81 * @return Zend_Cache_Backend_Static |
|
82 */ |
|
83 public function setOption($name, $value) |
|
84 { |
|
85 if ($name == 'tag_cache') { |
|
86 $this->setInnerCache($value); |
|
87 } else { |
|
88 parent::setOption($name, $value); |
|
89 } |
|
90 return $this; |
|
91 } |
|
92 |
|
93 /** |
|
94 * Retrieve any option via interception of the parent's statically held |
|
95 * options including the local option for a tag cache. |
|
96 * |
|
97 * @param string $name |
|
98 * @return mixed |
|
99 */ |
|
100 public function getOption($name) |
|
101 { |
|
102 if ($name == 'tag_cache') { |
|
103 return $this->getInnerCache(); |
|
104 } else { |
|
105 if (in_array($name, $this->_options)) { |
|
106 return $this->_options[$name]; |
|
107 } |
|
108 if ($name == 'lifetime') { |
|
109 return parent::getLifetime(); |
|
110 } |
|
111 return null; |
|
112 } |
|
113 } |
|
114 |
|
115 /** |
|
116 * Test if a cache is available for the given id and (if yes) return it (false else) |
|
117 * |
|
118 * Note : return value is always "string" (unserialization is done by the core not by the backend) |
|
119 * |
|
120 * @param string $id Cache id |
|
121 * @param boolean $doNotTestCacheValidity If set to true, the cache validity won't be tested |
|
122 * @return string|false cached datas |
|
123 */ |
|
124 public function load($id, $doNotTestCacheValidity = false) |
|
125 { |
|
126 if (empty($id)) { |
|
127 $id = $this->_detectId(); |
|
128 } else { |
|
129 $id = $this->_decodeId($id); |
|
130 } |
|
131 if (!$this->_verifyPath($id)) { |
|
132 Zend_Cache::throwException('Invalid cache id: does not match expected public_dir path'); |
|
133 } |
|
134 if ($doNotTestCacheValidity) { |
|
135 $this->_log("Zend_Cache_Backend_Static::load() : \$doNotTestCacheValidity=true is unsupported by the Static backend"); |
|
136 } |
|
137 |
|
138 $fileName = basename($id); |
|
139 if (empty($fileName)) { |
|
140 $fileName = $this->_options['index_filename']; |
|
141 } |
|
142 $pathName = $this->_options['public_dir'] . dirname($id); |
|
143 $file = rtrim($pathName, '/') . '/' . $fileName . $this->_options['file_extension']; |
|
144 if (file_exists($file)) { |
|
145 $content = file_get_contents($file); |
|
146 return $content; |
|
147 } |
|
148 |
|
149 return false; |
|
150 } |
|
151 |
|
152 /** |
|
153 * Test if a cache is available or not (for the given id) |
|
154 * |
|
155 * @param string $id cache id |
|
156 * @return bool |
|
157 */ |
|
158 public function test($id) |
|
159 { |
|
160 $id = $this->_decodeId($id); |
|
161 if (!$this->_verifyPath($id)) { |
|
162 Zend_Cache::throwException('Invalid cache id: does not match expected public_dir path'); |
|
163 } |
|
164 |
|
165 $fileName = basename($id); |
|
166 if (empty($fileName)) { |
|
167 $fileName = $this->_options['index_filename']; |
|
168 } |
|
169 if ($this->_tagged === null && $tagged = $this->getInnerCache()->load(self::INNER_CACHE_NAME)) { |
|
170 $this->_tagged = $tagged; |
|
171 } elseif (!$this->_tagged) { |
|
172 return false; |
|
173 } |
|
174 $pathName = $this->_options['public_dir'] . dirname($id); |
|
175 |
|
176 // Switch extension if needed |
|
177 if (isset($this->_tagged[$id])) { |
|
178 $extension = $this->_tagged[$id]['extension']; |
|
179 } else { |
|
180 $extension = $this->_options['file_extension']; |
|
181 } |
|
182 $file = $pathName . '/' . $fileName . $extension; |
|
183 if (file_exists($file)) { |
|
184 return true; |
|
185 } |
|
186 return false; |
|
187 } |
|
188 |
|
189 /** |
|
190 * Save some string datas into a cache record |
|
191 * |
|
192 * Note : $data is always "string" (serialization is done by the |
|
193 * core not by the backend) |
|
194 * |
|
195 * @param string $data Datas to cache |
|
196 * @param string $id Cache id |
|
197 * @param array $tags Array of strings, the cache record will be tagged by each string entry |
|
198 * @param int $specificLifetime If != false, set a specific lifetime for this cache record (null => infinite lifetime) |
|
199 * @return boolean true if no problem |
|
200 */ |
|
201 public function save($data, $id, $tags = array(), $specificLifetime = false) |
|
202 { |
|
203 if ($this->_options['disable_caching']) { |
|
204 return true; |
|
205 } |
|
206 $extension = null; |
|
207 if ($this->_isSerialized($data)) { |
|
208 $data = unserialize($data); |
|
209 $extension = '.' . ltrim($data[1], '.'); |
|
210 $data = $data[0]; |
|
211 } |
|
212 |
|
213 clearstatcache(); |
|
214 if ($id === null || strlen($id) == 0) { |
|
215 $id = $this->_detectId(); |
|
216 } else { |
|
217 $id = $this->_decodeId($id); |
|
218 } |
|
219 |
|
220 $fileName = basename($id); |
|
221 if (empty($fileName)) { |
|
222 $fileName = $this->_options['index_filename']; |
|
223 } |
|
224 |
|
225 $pathName = realpath($this->_options['public_dir']) . dirname($id); |
|
226 $this->_createDirectoriesFor($pathName); |
|
227 |
|
228 if ($id === null || strlen($id) == 0) { |
|
229 $dataUnserialized = unserialize($data); |
|
230 $data = $dataUnserialized['data']; |
|
231 } |
|
232 $ext = $this->_options['file_extension']; |
|
233 if ($extension) $ext = $extension; |
|
234 $file = rtrim($pathName, '/') . '/' . $fileName . $ext; |
|
235 if ($this->_options['file_locking']) { |
|
236 $result = file_put_contents($file, $data, LOCK_EX); |
|
237 } else { |
|
238 $result = file_put_contents($file, $data); |
|
239 } |
|
240 @chmod($file, $this->_octdec($this->_options['cache_file_umask'])); |
|
241 |
|
242 if ($this->_tagged === null && $tagged = $this->getInnerCache()->load(self::INNER_CACHE_NAME)) { |
|
243 $this->_tagged = $tagged; |
|
244 } elseif ($this->_tagged === null) { |
|
245 $this->_tagged = array(); |
|
246 } |
|
247 if (!isset($this->_tagged[$id])) { |
|
248 $this->_tagged[$id] = array(); |
|
249 } |
|
250 if (!isset($this->_tagged[$id]['tags'])) { |
|
251 $this->_tagged[$id]['tags'] = array(); |
|
252 } |
|
253 $this->_tagged[$id]['tags'] = array_unique(array_merge($this->_tagged[$id]['tags'], $tags)); |
|
254 $this->_tagged[$id]['extension'] = $ext; |
|
255 $this->getInnerCache()->save($this->_tagged, self::INNER_CACHE_NAME); |
|
256 return (bool) $result; |
|
257 } |
|
258 |
|
259 /** |
|
260 * Recursively create the directories needed to write the static file |
|
261 */ |
|
262 protected function _createDirectoriesFor($path) |
|
263 { |
|
264 if (!is_dir($path)) { |
|
265 $oldUmask = umask(0); |
|
266 if ( !@mkdir($path, $this->_octdec($this->_options['cache_directory_umask']), true)) { |
|
267 $lastErr = error_get_last(); |
|
268 umask($oldUmask); |
|
269 Zend_Cache::throwException("Can't create directory: {$lastErr['message']}"); |
|
270 } |
|
271 umask($oldUmask); |
|
272 } |
|
273 } |
|
274 |
|
275 /** |
|
276 * Detect serialization of data (cannot predict since this is the only way |
|
277 * to obey the interface yet pass in another parameter). |
|
278 * |
|
279 * In future, ZF 2.0, check if we can just avoid the interface restraints. |
|
280 * |
|
281 * This format is the only valid one possible for the class, so it's simple |
|
282 * to just run a regular expression for the starting serialized format. |
|
283 */ |
|
284 protected function _isSerialized($data) |
|
285 { |
|
286 return preg_match("/a:2:\{i:0;s:\d+:\"/", $data); |
|
287 } |
|
288 |
|
289 /** |
|
290 * Remove a cache record |
|
291 * |
|
292 * @param string $id Cache id |
|
293 * @return boolean True if no problem |
|
294 */ |
|
295 public function remove($id) |
|
296 { |
|
297 if (!$this->_verifyPath($id)) { |
|
298 Zend_Cache::throwException('Invalid cache id: does not match expected public_dir path'); |
|
299 } |
|
300 $fileName = basename($id); |
|
301 if ($this->_tagged === null && $tagged = $this->getInnerCache()->load(self::INNER_CACHE_NAME)) { |
|
302 $this->_tagged = $tagged; |
|
303 } elseif (!$this->_tagged) { |
|
304 return false; |
|
305 } |
|
306 if (isset($this->_tagged[$id])) { |
|
307 $extension = $this->_tagged[$id]['extension']; |
|
308 } else { |
|
309 $extension = $this->_options['file_extension']; |
|
310 } |
|
311 if (empty($fileName)) { |
|
312 $fileName = $this->_options['index_filename']; |
|
313 } |
|
314 $pathName = $this->_options['public_dir'] . dirname($id); |
|
315 $file = realpath($pathName) . '/' . $fileName . $extension; |
|
316 if (!file_exists($file)) { |
|
317 return false; |
|
318 } |
|
319 return unlink($file); |
|
320 } |
|
321 |
|
322 /** |
|
323 * Remove a cache record recursively for the given directory matching a |
|
324 * REQUEST_URI based relative path (deletes the actual file matching this |
|
325 * in addition to the matching directory) |
|
326 * |
|
327 * @param string $id Cache id |
|
328 * @return boolean True if no problem |
|
329 */ |
|
330 public function removeRecursively($id) |
|
331 { |
|
332 if (!$this->_verifyPath($id)) { |
|
333 Zend_Cache::throwException('Invalid cache id: does not match expected public_dir path'); |
|
334 } |
|
335 $fileName = basename($id); |
|
336 if (empty($fileName)) { |
|
337 $fileName = $this->_options['index_filename']; |
|
338 } |
|
339 $pathName = $this->_options['public_dir'] . dirname($id); |
|
340 $file = $pathName . '/' . $fileName . $this->_options['file_extension']; |
|
341 $directory = $pathName . '/' . $fileName; |
|
342 if (file_exists($directory)) { |
|
343 if (!is_writable($directory)) { |
|
344 return false; |
|
345 } |
|
346 foreach (new DirectoryIterator($directory) as $file) { |
|
347 if (true === $file->isFile()) { |
|
348 if (false === unlink($file->getPathName())) { |
|
349 return false; |
|
350 } |
|
351 } |
|
352 } |
|
353 rmdir(dirname($path)); |
|
354 } |
|
355 if (file_exists($file)) { |
|
356 if (!is_writable($file)) { |
|
357 return false; |
|
358 } |
|
359 return unlink($file); |
|
360 } |
|
361 return true; |
|
362 } |
|
363 |
|
364 /** |
|
365 * Clean some cache records |
|
366 * |
|
367 * Available modes are : |
|
368 * Zend_Cache::CLEANING_MODE_ALL (default) => remove all cache entries ($tags is not used) |
|
369 * Zend_Cache::CLEANING_MODE_OLD => remove too old cache entries ($tags is not used) |
|
370 * Zend_Cache::CLEANING_MODE_MATCHING_TAG => remove cache entries matching all given tags |
|
371 * ($tags can be an array of strings or a single string) |
|
372 * Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG => remove cache entries not {matching one of the given tags} |
|
373 * ($tags can be an array of strings or a single string) |
|
374 * Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG => remove cache entries matching any given tags |
|
375 * ($tags can be an array of strings or a single string) |
|
376 * |
|
377 * @param string $mode Clean mode |
|
378 * @param array $tags Array of tags |
|
379 * @return boolean true if no problem |
|
380 */ |
|
381 public function clean($mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array()) |
|
382 { |
|
383 $result = false; |
|
384 switch ($mode) { |
|
385 case Zend_Cache::CLEANING_MODE_MATCHING_TAG: |
|
386 case Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG: |
|
387 if (empty($tags)) { |
|
388 throw new Zend_Exception('Cannot use tag matching modes as no tags were defined'); |
|
389 } |
|
390 if ($this->_tagged === null && $tagged = $this->getInnerCache()->load(self::INNER_CACHE_NAME)) { |
|
391 $this->_tagged = $tagged; |
|
392 } elseif (!$this->_tagged) { |
|
393 return true; |
|
394 } |
|
395 foreach ($tags as $tag) { |
|
396 $urls = array_keys($this->_tagged); |
|
397 foreach ($urls as $url) { |
|
398 if (isset($this->_tagged[$url]['tags']) && in_array($tag, $this->_tagged[$url]['tags'])) { |
|
399 $this->remove($url); |
|
400 unset($this->_tagged[$url]); |
|
401 } |
|
402 } |
|
403 } |
|
404 $this->getInnerCache()->save($this->_tagged, self::INNER_CACHE_NAME); |
|
405 $result = true; |
|
406 break; |
|
407 case Zend_Cache::CLEANING_MODE_ALL: |
|
408 if ($this->_tagged === null) { |
|
409 $tagged = $this->getInnerCache()->load(self::INNER_CACHE_NAME); |
|
410 $this->_tagged = $tagged; |
|
411 } |
|
412 if ($this->_tagged === null || empty($this->_tagged)) { |
|
413 return true; |
|
414 } |
|
415 $urls = array_keys($this->_tagged); |
|
416 foreach ($urls as $url) { |
|
417 $this->remove($url); |
|
418 unset($this->_tagged[$url]); |
|
419 } |
|
420 $this->getInnerCache()->save($this->_tagged, self::INNER_CACHE_NAME); |
|
421 $result = true; |
|
422 break; |
|
423 case Zend_Cache::CLEANING_MODE_OLD: |
|
424 $this->_log("Zend_Cache_Backend_Static : Selected Cleaning Mode Currently Unsupported By This Backend"); |
|
425 break; |
|
426 case Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG: |
|
427 if (empty($tags)) { |
|
428 throw new Zend_Exception('Cannot use tag matching modes as no tags were defined'); |
|
429 } |
|
430 if ($this->_tagged === null) { |
|
431 $tagged = $this->getInnerCache()->load(self::INNER_CACHE_NAME); |
|
432 $this->_tagged = $tagged; |
|
433 } |
|
434 if ($this->_tagged === null || empty($this->_tagged)) { |
|
435 return true; |
|
436 } |
|
437 $urls = array_keys($this->_tagged); |
|
438 foreach ($urls as $url) { |
|
439 $difference = array_diff($tags, $this->_tagged[$url]['tags']); |
|
440 if (count($tags) == count($difference)) { |
|
441 $this->remove($url); |
|
442 unset($this->_tagged[$url]); |
|
443 } |
|
444 } |
|
445 $this->getInnerCache()->save($this->_tagged, self::INNER_CACHE_NAME); |
|
446 $result = true; |
|
447 break; |
|
448 default: |
|
449 Zend_Cache::throwException('Invalid mode for clean() method'); |
|
450 break; |
|
451 } |
|
452 return $result; |
|
453 } |
|
454 |
|
455 /** |
|
456 * Set an Inner Cache, used here primarily to store Tags associated |
|
457 * with caches created by this backend. Note: If Tags are lost, the cache |
|
458 * should be completely cleaned as the mapping of tags to caches will |
|
459 * have been irrevocably lost. |
|
460 * |
|
461 * @param Zend_Cache_Core |
|
462 * @return void |
|
463 */ |
|
464 public function setInnerCache(Zend_Cache_Core $cache) |
|
465 { |
|
466 $this->_tagCache = $cache; |
|
467 $this->_options['tag_cache'] = $cache; |
|
468 } |
|
469 |
|
470 /** |
|
471 * Get the Inner Cache if set |
|
472 * |
|
473 * @return Zend_Cache_Core |
|
474 */ |
|
475 public function getInnerCache() |
|
476 { |
|
477 if ($this->_tagCache === null) { |
|
478 Zend_Cache::throwException('An Inner Cache has not been set; use setInnerCache()'); |
|
479 } |
|
480 return $this->_tagCache; |
|
481 } |
|
482 |
|
483 /** |
|
484 * Verify path exists and is non-empty |
|
485 * |
|
486 * @param string $path |
|
487 * @return bool |
|
488 */ |
|
489 protected function _verifyPath($path) |
|
490 { |
|
491 $path = realpath($path); |
|
492 $base = realpath($this->_options['public_dir']); |
|
493 return strncmp($path, $base, strlen($base)) !== 0; |
|
494 } |
|
495 |
|
496 /** |
|
497 * Determine the page to save from the request |
|
498 * |
|
499 * @return string |
|
500 */ |
|
501 protected function _detectId() |
|
502 { |
|
503 return $_SERVER['REQUEST_URI']; |
|
504 } |
|
505 |
|
506 /** |
|
507 * Validate a cache id or a tag (security, reliable filenames, reserved prefixes...) |
|
508 * |
|
509 * Throw an exception if a problem is found |
|
510 * |
|
511 * @param string $string Cache id or tag |
|
512 * @throws Zend_Cache_Exception |
|
513 * @return void |
|
514 * @deprecated Not usable until perhaps ZF 2.0 |
|
515 */ |
|
516 protected static function _validateIdOrTag($string) |
|
517 { |
|
518 if (!is_string($string)) { |
|
519 Zend_Cache::throwException('Invalid id or tag : must be a string'); |
|
520 } |
|
521 |
|
522 // Internal only checked in Frontend - not here! |
|
523 if (substr($string, 0, 9) == 'internal-') { |
|
524 return; |
|
525 } |
|
526 |
|
527 // Validation assumes no query string, fragments or scheme included - only the path |
|
528 if (!preg_match( |
|
529 '/^(?:\/(?:(?:%[[:xdigit:]]{2}|[A-Za-z0-9-_.!~*\'()\[\]:@&=+$,;])*)?)+$/', |
|
530 $string |
|
531 ) |
|
532 ) { |
|
533 Zend_Cache::throwException("Invalid id or tag '$string' : must be a valid URL path"); |
|
534 } |
|
535 } |
|
536 |
|
537 /** |
|
538 * Detect an octal string and return its octal value for file permission ops |
|
539 * otherwise return the non-string (assumed octal or decimal int already) |
|
540 * |
|
541 * @param $val The potential octal in need of conversion |
|
542 * @return int |
|
543 */ |
|
544 protected function _octdec($val) |
|
545 { |
|
546 if (is_string($val) && decoct(octdec($val)) == $val) { |
|
547 return octdec($val); |
|
548 } |
|
549 return $val; |
|
550 } |
|
551 |
|
552 /** |
|
553 * Decode a request URI from the provided ID |
|
554 */ |
|
555 protected function _decodeId($id) |
|
556 { |
|
557 return pack('H*', $id);; |
|
558 } |
|
559 } |