web/lib/Zend/Cache/Backend/Static.php
changeset 64 162c1de6545a
parent 19 1c2f13fd785c
child 68 ecaf28ffe26e
equal deleted inserted replaced
63:5b37998e522e 64:162c1de6545a
       
     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 }