PageRenderTime 360ms CodeModel.GetById 135ms app.highlight 89ms RepoModel.GetById 125ms app.codeStats 0ms

/library/Zend/Cache/Backend/File.php

https://bitbucket.org/baruffaldi/cms-php-bfcms
PHP | 716 lines | 451 code | 28 blank | 237 comment | 47 complexity | ea6066b669e2ede0da88711b98d4aa50 MD5 | raw file
  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-2008 Zend Technologies USA Inc. (http://www.zend.com)
 19 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 20 */
 21
 22
 23/**
 24 * @see Zend_Cache_Backend_Interface
 25 */
 26require_once 'Zend/Cache/Backend/Interface.php';
 27
 28/**
 29 * @see Zend_Cache_Backend
 30 */
 31require_once 'Zend/Cache/Backend.php';
 32
 33
 34/**
 35 * @package    Zend_Cache
 36 * @subpackage Zend_Cache_Backend
 37 * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
 38 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 39 */
 40class Zend_Cache_Backend_File extends Zend_Cache_Backend implements Zend_Cache_Backend_Interface
 41{
 42    /**
 43     * Available options
 44     *
 45     * =====> (string) cache_dir :
 46     * - Directory where to put the cache files
 47     *
 48     * =====> (boolean) file_locking :
 49     * - Enable / disable file_locking
 50     * - Can avoid cache corruption under bad circumstances but it doesn't work on multithread
 51     * webservers and on NFS filesystems for example
 52     *
 53     * =====> (boolean) read_control :
 54     * - Enable / disable read control
 55     * - If enabled, a control key is embeded in cache file and this key is compared with the one
 56     * calculated after the reading.
 57     *
 58     * =====> (string) read_control_type :
 59     * - Type of read control (only if read control is enabled). Available values are :
 60     *   'md5' for a md5 hash control (best but slowest)
 61     *   'crc32' for a crc32 hash control (lightly less safe but faster, better choice)
 62     *   'adler32' for an adler32 hash control (excellent choice too, faster than crc32)
 63     *   'strlen' for a length only test (fastest)
 64     *
 65     * =====> (int) hashed_directory_level :
 66     * - Hashed directory level
 67     * - Set the hashed directory structure level. 0 means "no hashed directory
 68     * structure", 1 means "one level of directory", 2 means "two levels"...
 69     * This option can speed up the cache only when you have many thousands of
 70     * cache file. Only specific benchs can help you to choose the perfect value
 71     * for you. Maybe, 1 or 2 is a good start.
 72     *
 73     * =====> (int) hashed_directory_umask :
 74     * - Umask for hashed directory structure
 75     *
 76     * =====> (string) file_name_prefix :
 77     * - prefix for cache files
 78     * - be really carefull with this option because a too generic value in a system cache dir
 79     *   (like /tmp) can cause disasters when cleaning the cache
 80     *
 81     * =====> (int) cache_file_umask :
 82     * - Umask for cache files
 83     *
 84     * =====> (int) metatadatas_array_max_size :
 85     * - max size for the metadatas array (don't change this value unless you
 86     *   know what you are doing)
 87     *
 88     * @var array available options
 89     */
 90    protected $_options = array(
 91        'cache_dir' => null,
 92        'file_locking' => true,
 93        'read_control' => true,
 94        'read_control_type' => 'crc32',
 95        'hashed_directory_level' => 0,
 96        'hashed_directory_umask' => 0700,
 97        'file_name_prefix' => 'zend_cache',
 98        'cache_file_umask' => 0600,
 99        'metadatas_array_max_size' => 100
100    );
101
102    /**
103     * Array of metadatas (each item is an associative array)
104     *
105     * @var array
106     */
107    private $_metadatasArray = array();
108
109
110    /**
111     * Constructor
112     *
113     * @param  array $options associative array of options
114     * @throws Zend_Cache_Exception
115     * @return void
116     */
117    public function __construct(array $options = array())
118    {
119        parent::__construct($options);
120        if (!is_null($this->_options['cache_dir'])) { // particular case for this option
121            $this->setCacheDir($this->_options['cache_dir']);
122        } else {
123            $this->setCacheDir(self::getTmpDir() . DIRECTORY_SEPARATOR, false);
124        }
125        if (isset($this->_options['file_name_prefix'])) { // particular case for this option
126            if (!preg_match('~^[\w]+$~', $this->_options['file_name_prefix'])) {
127                Zend_Cache::throwException('Invalid file_name_prefix : must use only [a-zA-A0-9_]');
128            }
129        }
130        if ($this->_options['metadatas_array_max_size'] < 10) {
131            Zend_Cache::throwException('Invalid metadatas_array_max_size, must be > 10');
132        }
133    }
134
135    /**
136     * Set the cache_dir (particular case of setOption() method)
137     *
138     * @param  string  $value
139     * @param  boolean $trailingSeparator If true, add a trailing separator is necessary
140     * @throws Zend_Cache_Exception
141     * @return void
142     */
143    public function setCacheDir($value, $trailingSeparator = true)
144    {
145        if (!is_dir($value)) {
146            Zend_Cache::throwException('cache_dir must be a directory');
147        }
148        if (!is_writable($value)) {
149            Zend_Cache::throwException('cache_dir is not writable');
150        }
151        if ($trailingSeparator) {
152            // add a trailing DIRECTORY_SEPARATOR if necessary
153            $value = rtrim(realpath($value), '\\/') . DIRECTORY_SEPARATOR;
154        }
155        $this->_options['cache_dir'] = $value;
156    }
157
158    /**
159     * Test if a cache is available for the given id and (if yes) return it (false else)
160     *
161     * @param string $id cache id
162     * @param boolean $doNotTestCacheValidity if set to true, the cache validity won't be tested
163     * @return string|false cached datas
164     */
165    public function load($id, $doNotTestCacheValidity = false)
166    {
167        if (!($this->_test($id, $doNotTestCacheValidity))) {
168            // The cache is not hit !
169            return false;
170        }
171        $metadatas = $this->_getMetadatas($id);
172        $file = $this->_file($id);
173        $data = $this->_fileGetContents($file);
174        if ($this->_options['read_control']) {
175            $hashData = $this->_hash($data, $this->_options['read_control_type']);
176            $hashControl = $metadatas['hash'];
177            if ($hashData != $hashControl) {
178                // Problem detected by the read control !
179                $this->_log('Zend_Cache_Backend_File::load() / read_control : stored hash and computed hash do not match');
180                $this->remove($id);
181                return false;
182            }
183        }
184        return $data;
185    }
186
187    /**
188     * Test if a cache is available or not (for the given id)
189     *
190     * @param string $id cache id
191     * @return mixed false (a cache is not available) or "last modified" timestamp (int) of the available cache record
192     */
193    public function test($id)
194    {
195        clearstatcache();
196        return $this->_test($id, false);
197    }
198
199    /**
200     * Save some string datas into a cache record
201     *
202     * Note : $data is always "string" (serialization is done by the
203     * core not by the backend)
204     *
205     * @param  string $data             Datas to cache
206     * @param  string $id               Cache id
207     * @param  array  $tags             Array of strings, the cache record will be tagged by each string entry
208     * @param  int    $specificLifetime If != false, set a specific lifetime for this cache record (null => infinite lifetime)
209     * @return boolean true if no problem
210     */
211    public function save($data, $id, $tags = array(), $specificLifetime = false)
212    {
213        clearstatcache();
214        $file = $this->_file($id);
215        $path = $this->_path($id);
216        $firstTry = true;
217        $result = false;
218        if ($this->_options['hashed_directory_level'] > 0) {
219            if (!is_writable($path)) {
220                // maybe, we just have to build the directory structure
221                @mkdir($this->_path($id), $this->_options['hashed_directory_umask'], true);
222                @chmod($this->_path($id), $this->_options['hashed_directory_umask']); // see #ZF-320 (this line is required in some configurations)
223            }
224            if (!is_writable($path)) {
225                return false;
226            }
227        }
228        if ($this->_options['read_control']) {
229            $hash = $this->_hash($data, $this->_options['read_control_type']);
230        } else {
231            $hash = '';
232        }
233        $metadatas = array(
234            'hash' => $hash,
235            'mtime' => time(),
236            'expire' => $this->_expireTime($this->getLifetime($specificLifetime)),
237            'tags' => $tags
238        );
239        $res = $this->_setMetadatas($id, $metadatas);
240        if (!$res) {
241            // FIXME : log
242            return false;
243        }
244        $res = $this->_filePutContents($file, $data);
245        return $res;
246    }
247
248    /**
249     * Remove a cache record
250     *
251     * @param  string $id cache id
252     * @return boolean true if no problem
253     */
254    public function remove($id)
255    {
256        $file = $this->_file($id);
257        return ($this->_delMetadatas($id) && $this->_remove($file));
258    }
259
260    /**
261     * Clean some cache records
262     *
263     * Available modes are :
264     * 'all' (default)  => remove all cache entries ($tags is not used)
265     * 'old'            => remove too old cache entries ($tags is not used)
266     * 'matchingTag'    => remove cache entries matching all given tags
267     *                     ($tags can be an array of strings or a single string)
268     * 'notMatchingTag' => remove cache entries not matching one of the given tags
269     *                     ($tags can be an array of strings or a single string)
270     *
271     * @param string $mode clean mode
272     * @param tags array $tags array of tags
273     * @return boolean true if no problem
274     */
275    public function clean($mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array())
276    {
277        // We use this private method to hide the recursive stuff
278        clearstatcache();
279        return $this->_clean($this->_options['cache_dir'], $mode, $tags);
280    }
281
282    /**
283     * PUBLIC METHOD FOR UNIT TESTING ONLY !
284     *
285     * Force a cache record to expire
286     *
287     * @param string $id cache id
288     */
289    public function ___expire($id)
290    {
291        $metadatas = $this->_getMetadatas($id);
292        if ($metadatas) {
293            $metadatas['expire'] = 1;
294            $this->_setMetadatas($id, $metadatas);
295        }
296    }
297
298    /**
299     * Get a metadatas record
300     *
301     * @param  string $id  Cache id
302     * @return array|false Associative array of metadatas
303     */
304    private function _getMetadatas($id)
305    {
306        if (isset($this->_metadatasArray[$id])) {
307            return $this->_metadatasArray[$id];
308        } else {
309            $metadatas = $this->_loadMetadatas($id);
310            if (!$metadatas) {
311                return false;
312            }
313            $this->_setMetadatas($id, $metadatas, false);
314            return $metadatas;
315        }
316    }
317
318    /**
319     * Set a metadatas record
320     *
321     * @param  string $id        Cache id
322     * @param  array  $metadatas Associative array of metadatas
323     * @param  boolean $save     optional pass false to disable saving to file
324     * @return boolean True if no problem
325     */
326    private function _setMetadatas($id, $metadatas, $save = true)
327    {
328        if (count($this->_metadatasArray) >= $this->_options['metadatas_array_max_size']) {
329            $n = (int) ($this->_options['metadatas_array_max_size'] / 10);
330            $this->_metadatasArray = array_slice($this->_metadatasArray, $n);
331        }
332        if ($save) {
333            $result = $this->_saveMetadatas($id, $metadatas);
334            if (!$result) {
335                return false;
336            }
337        }
338        $this->_metadatasArray[$id] = $metadatas;
339        return true;
340    }
341
342    /**
343     * Drop a metadata record
344     *
345     * @param  string $id Cache id
346     * @return boolean True if no problem
347     */
348    private function _delMetadatas($id)
349    {
350        if (isset($this->_metadatasArray[$id])) {
351            unset($this->_metadatasArray[$id]);
352        }
353        $file = $this->_metadatasFile($id);
354        return $this->_remove($file);
355    }
356
357    /**
358     * Clear the metadatas array
359     *
360     * @return void
361     */
362    private function _cleanMetadatas()
363    {
364        $this->_metadatasArray = array();
365    }
366
367    /**
368     * Load metadatas from disk
369     *
370     * @param  string $id Cache id
371     * @return array|false Metadatas associative array
372     */
373    private function _loadMetadatas($id)
374    {
375        $file = $this->_metadatasFile($id);
376        $result = $this->_fileGetContents($file);
377        if (!$result) {
378            return false;
379        }
380        $tmp = @unserialize($result);
381        return $tmp;
382    }
383
384    /**
385     * Save metadatas to disk
386     *
387     * @param  string $id        Cache id
388     * @param  array  $metadatas Associative array
389     * @return boolean True if no problem
390     */
391    private function _saveMetadatas($id, $metadatas)
392    {
393        $file = $this->_metadatasFile($id);
394        $result = $this->_filePutContents($file, serialize($metadatas));
395        if (!$result) {
396            return false;
397        }
398        return true;
399    }
400
401    /**
402     * Make and return a file name (with path) for metadatas
403     *
404     * @param  string $id Cache id
405     * @return string Metadatas file name (with path)
406     */
407    private function _metadatasFile($id)
408    {
409        $path = $this->_path($id);
410        $fileName = $this->_idToFileName('internal-metadatas---' . $id);
411        return $path . $fileName;
412    }
413
414    /**
415     * Check if the given filename is a metadatas one
416     *
417     * @param  string $fileName File name
418     * @return boolean True if it's a metadatas one
419     */
420    private function _isMetadatasFile($fileName)
421    {
422        $id = $this->_fileNameToId($fileName);
423        if (substr($id, 0, 21) == 'internal-metadatas---') {
424            return true;
425        } else {
426            return false;
427        }
428    }
429
430    /**
431     * Remove a file
432     *
433     * If we can't remove the file (because of locks or any problem), we will touch
434     * the file to invalidate it
435     *
436     * @param  string $file Complete file path
437     * @return boolean True if ok
438     */
439    private function _remove($file)
440    {
441        if (!is_file($file)) {
442            return false;
443        }
444        if (!@unlink($file)) {
445            # we can't remove the file (because of locks or any problem)
446            $this->_log("Zend_Cache_Backend_File::_remove() : we can't remove $file");
447            return false;
448        }
449        return true;
450    }
451
452    /**
453     * Clean some cache records (private method used for recursive stuff)
454     *
455     * Available modes are :
456     * Zend_Cache::CLEANING_MODE_ALL (default)    => remove all cache entries ($tags is not used)
457     * Zend_Cache::CLEANING_MODE_OLD              => remove too old cache entries ($tags is not used)
458     * Zend_Cache::CLEANING_MODE_MATCHING_TAG     => remove cache entries matching all given tags
459     *                                               ($tags can be an array of strings or a single string)
460     * Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG => remove cache entries not {matching one of the given tags}
461     *                                               ($tags can be an array of strings or a single string)
462     *
463     * @param  string $dir  Directory to clean
464     * @param  string $mode Clean mode
465     * @param  array  $tags Array of tags
466     * @throws Zend_Cache_Exception
467     * @return boolean True if no problem
468     */
469    private function _clean($dir, $mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array())
470    {
471        if (!is_dir($dir)) {
472            return false;
473        }
474        $result = true;
475        $prefix = $this->_options['file_name_prefix'];
476        $glob = @glob($dir . $prefix . '--*');
477        if ($glob === false) {
478            return true;
479        }
480        foreach ($glob as $file)  {
481            if (is_file($file)) {
482                $fileName = basename($file);
483                if ($this->_isMetadatasFile($fileName)) {
484                    // in CLEANING_MODE_ALL, we drop anything, even remainings old metadatas files
485                    if ($mode != Zend_Cache::CLEANING_MODE_ALL) {
486                        continue;
487                    }
488                }
489                $id = $this->_fileNameToId($fileName);
490                $metadatas = $this->_getMetadatas($id);
491                if ($metadatas === FALSE) {
492                    $metadatas = array('expire' => 1, 'tags' => array());
493                }
494                switch ($mode) {
495                    case Zend_Cache::CLEANING_MODE_ALL:
496                        $res = $this->remove($id);
497                        if (!$res) {
498                            // in this case only, we accept a problem with the metadatas file drop
499                            $res = $this->_remove($file);
500                        }
501                        $result = $result && $res;
502                        break;
503                    case Zend_Cache::CLEANING_MODE_OLD:
504                        if (time() > $metadatas['expire']) {
505                            $result = ($result) && ($this->remove($id));
506                        }
507                        break;
508                    case Zend_Cache::CLEANING_MODE_MATCHING_TAG:
509                        $matching = true;
510                        foreach ($tags as $tag) {
511                            if (!in_array($tag, $metadatas['tags'])) {
512                                $matching = false;
513                                break;
514                            }
515                        }
516                        if ($matching) {
517                            $result = ($result) && ($this->remove($id));
518                        }
519                        break;
520                    case Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG:
521                        $matching = false;
522                        foreach ($tags as $tag) {
523                            if (in_array($tag, $metadatas['tags'])) {
524                                $matching = true;
525                                break;
526                            }
527                        }
528                        if (!$matching) {
529                            $result = ($result) && $this->remove($id);
530                        }
531                        break;
532                    default:
533                        Zend_Cache::throwException('Invalid mode for clean() method');
534                        break;
535                }
536            }
537            if ((is_dir($file)) and ($this->_options['hashed_directory_level']>0)) {
538                // Recursive call
539                $result = ($result) && ($this->_clean($file . DIRECTORY_SEPARATOR, $mode, $tags));
540                if ($mode=='all') {
541                    // if mode=='all', we try to drop the structure too
542                    @rmdir($file);
543                }
544            }
545        }
546        return $result;
547    }
548
549    /**
550     * Compute & return the expire time
551     *
552     * @return int expire time (unix timestamp)
553     */
554    private function _expireTime($lifetime)
555    {
556        if (is_null($lifetime)) {
557            return 9999999999;
558        }
559        return time() + $lifetime;
560    }
561
562    /**
563     * Make a control key with the string containing datas
564     *
565     * @param  string $data        Data
566     * @param  string $controlType Type of control 'md5', 'crc32' or 'strlen'
567     * @throws Zend_Cache_Exception
568     * @return string Control key
569     */
570    private function _hash($data, $controlType)
571    {
572        switch ($controlType) {
573        case 'md5':
574            return md5($data);
575        case 'crc32':
576            return crc32($data);
577        case 'strlen':
578            return strlen($data);
579        case 'adler32':
580            return hash('adler32', $data);
581        default:
582            Zend_Cache::throwException("Incorrect hash function : $controlType");
583        }
584    }
585
586    /**
587     * Transform a cache id into a file name and return it
588     *
589     * @param  string $id Cache id
590     * @return string File name
591     */
592    private function _idToFileName($id)
593    {
594        $prefix = $this->_options['file_name_prefix'];
595        $result = $prefix . '---' . $id;
596        return $result;
597    }
598
599    /**
600     * Make and return a file name (with path)
601     *
602     * @param  string $id Cache id
603     * @return string File name (with path)
604     */
605    private function _file($id)
606    {
607        $path = $this->_path($id);
608        $fileName = $this->_idToFileName($id);
609        return $path . $fileName;
610    }
611
612    /**
613     * Return the complete directory path of a filename (including hashedDirectoryStructure)
614     *
615     * @param  string $id Cache id
616     * @return string Complete directory path
617     */
618    private function _path($id)
619    {
620        $root = $this->_options['cache_dir'];
621        $prefix = $this->_options['file_name_prefix'];
622        if ($this->_options['hashed_directory_level']>0) {
623            $hash = hash('adler32', $id);
624            for ($i=0 ; $i < $this->_options['hashed_directory_level'] ; $i++) {
625                $root = $root . $prefix . '--' . substr($hash, 0, $i + 1) . DIRECTORY_SEPARATOR;
626            }
627        }
628        return $root;
629    }
630
631    /**
632     * Test if the given cache id is available (and still valid as a cache record)
633     *
634     * @param  string  $id                     Cache id
635     * @param  boolean $doNotTestCacheValidity If set to true, the cache validity won't be tested
636     * @return boolean|mixed false (a cache is not available) or "last modified" timestamp (int) of the available cache record
637     */
638    private function _test($id, $doNotTestCacheValidity)
639    {
640        $metadatas = $this->_getMetadatas($id);
641        if (!$metadatas) {
642            return false;
643        }
644        if ($doNotTestCacheValidity || (time() <= $metadatas['expire'])) {
645            return $metadatas['mtime'];
646        }
647        return false;
648    }
649
650    /**
651     * Return the file content of the given file
652     *
653     * @param  string $file File complete path
654     * @return string File content (or false if problem)
655     */
656    private function _fileGetContents($file)
657    {
658        $result = false;
659        if (!is_file($file)) {
660            return false;
661        }
662        if (function_exists('get_magic_quotes_runtime')) {
663            $mqr = @get_magic_quotes_runtime();
664            @set_magic_quotes_runtime(0);
665        }
666        $f = @fopen($file, 'rb');
667        if ($f) {
668            if ($this->_options['file_locking']) @flock($f, LOCK_SH);
669            $result = stream_get_contents($f);
670            if ($this->_options['file_locking']) @flock($f, LOCK_UN);
671            @fclose($f);
672        }
673        if (function_exists('set_magic_quotes_runtime')) {
674            @set_magic_quotes_runtime($mqr);
675        }
676        return $result;
677    }
678
679    /**
680     * Put the given string into the given file
681     *
682     * @param  string $file   File complete path
683     * @param  string $string String to put in file
684     * @return boolean true if no problem
685     */
686    private function _filePutContents($file, $string)
687    {
688        $result = false;
689        $f = @fopen($file, 'ab+');
690        if ($f) {
691            if ($this->_options['file_locking']) @flock($f, LOCK_EX);
692            fseek($f, 0);
693            ftruncate($f, 0);
694            $tmp = @fwrite($f, $string);
695            if (!($tmp === FALSE)) {
696                $result = true;
697            }
698            @fclose($f);
699        }
700        @chmod($file, $this->_options['cache_file_umask']);
701        return $result;
702    }
703
704    /**
705     * Transform a file name into cache id and return it
706     *
707     * @param  string $fileName File name
708     * @return string Cache id
709     */
710    private function _fileNameToId($fileName)
711    {
712        $prefix = $this->_options['file_name_prefix'];
713        return preg_replace('~^' . $prefix . '---(.*)$~', '$1', $fileName);
714    }
715
716}