File indexing completed on 2025-01-19 05:20:57

0001 <?php
0002 /**
0003  * Zend Framework
0004  *
0005  * LICENSE
0006  *
0007  * This source file is subject to the new BSD license that is bundled
0008  * with this package in the file LICENSE.txt.
0009  * It is also available through the world-wide-web at this URL:
0010  * http://framework.zend.com/license/new-bsd
0011  * If you did not receive a copy of the license and are unable to
0012  * obtain it through the world-wide-web, please send an email
0013  * to license@zend.com so we can send you a copy immediately.
0014  *
0015  * @category   Zend
0016  * @package    Zend_Cache
0017  * @subpackage Zend_Cache_Backend
0018  * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
0019  * @license    http://framework.zend.com/license/new-bsd     New BSD License
0020  * @version    $Id$
0021  */
0022 
0023 /**
0024  * @see Zend_Cache_Backend_Interface
0025  */
0026 // require_once 'Zend/Cache/Backend/Interface.php';
0027 
0028 /**
0029  * @see Zend_Cache_Backend
0030  */
0031 // require_once 'Zend/Cache/Backend.php';
0032 
0033 /**
0034  * @package    Zend_Cache
0035  * @subpackage Zend_Cache_Backend
0036  * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
0037  * @license    http://framework.zend.com/license/new-bsd     New BSD License
0038  */
0039 class Zend_Cache_Backend_Static
0040     extends Zend_Cache_Backend
0041     implements Zend_Cache_Backend_Interface
0042 {
0043     const INNER_CACHE_NAME = 'zend_cache_backend_static_tagcache';
0044 
0045     /**
0046      * Static backend options
0047      * @var array
0048      */
0049     protected $_options = array(
0050         'public_dir'           => null,
0051         'sub_dir'              => 'html',
0052         'file_extension'       => '.html',
0053         'index_filename'       => 'index',
0054         'file_locking'         => true,
0055         'cache_file_perm'      => 0600,
0056         'cache_directory_perm' => 0700,
0057         'debug_header'         => false,
0058         'tag_cache'            => null,
0059         'disable_caching'      => false
0060     );
0061 
0062     /**
0063      * Cache for handling tags
0064      * @var Zend_Cache_Core
0065      */
0066     protected $_tagCache = null;
0067 
0068     /**
0069      * Tagged items
0070      * @var array
0071      */
0072     protected $_tagged = null;
0073 
0074     /**
0075      * Interceptor child method to handle the case where an Inner
0076      * Cache object is being set since it's not supported by the
0077      * standard backend interface
0078      *
0079      * @param  string $name
0080      * @param  mixed $value
0081      * @return Zend_Cache_Backend_Static
0082      */
0083     public function setOption($name, $value)
0084     {
0085         if ($name == 'tag_cache') {
0086             $this->setInnerCache($value);
0087         } else {
0088             // See #ZF-12047 and #GH-91
0089             if ($name == 'cache_file_umask') {
0090                 trigger_error(
0091                     "'cache_file_umask' is deprecated -> please use 'cache_file_perm' instead",
0092                     E_USER_NOTICE
0093                 );
0094 
0095                 $name = 'cache_file_perm';
0096             }
0097             if ($name == 'cache_directory_umask') {
0098                 trigger_error(
0099                     "'cache_directory_umask' is deprecated -> please use 'cache_directory_perm' instead",
0100                     E_USER_NOTICE
0101                 );
0102 
0103                 $name = 'cache_directory_perm';
0104             }
0105 
0106             parent::setOption($name, $value);
0107         }
0108         return $this;
0109     }
0110 
0111     /**
0112      * Retrieve any option via interception of the parent's statically held
0113      * options including the local option for a tag cache.
0114      *
0115      * @param  string $name
0116      * @return mixed
0117      */
0118     public function getOption($name)
0119     {
0120         $name = strtolower($name);
0121 
0122         if ($name == 'tag_cache') {
0123             return $this->getInnerCache();
0124         }
0125 
0126         return parent::getOption($name);
0127     }
0128 
0129     /**
0130      * Test if a cache is available for the given id and (if yes) return it (false else)
0131      *
0132      * Note : return value is always "string" (unserialization is done by the core not by the backend)
0133      *
0134      * @param  string  $id                     Cache id
0135      * @param  boolean $doNotTestCacheValidity If set to true, the cache validity won't be tested
0136      * @return string|false cached datas
0137      */
0138     public function load($id, $doNotTestCacheValidity = false)
0139     {
0140         if (($id = (string)$id) === '') {
0141             $id = $this->_detectId();
0142         } else {
0143             $id = $this->_decodeId($id);
0144         }
0145         if (!$this->_verifyPath($id)) {
0146             Zend_Cache::throwException('Invalid cache id: does not match expected public_dir path');
0147         }
0148         if ($doNotTestCacheValidity) {
0149             $this->_log("Zend_Cache_Backend_Static::load() : \$doNotTestCacheValidity=true is unsupported by the Static backend");
0150         }
0151 
0152         $fileName = basename($id);
0153         if ($fileName === '') {
0154             $fileName = $this->_options['index_filename'];
0155         }
0156         $pathName = $this->_options['public_dir'] . dirname($id);
0157         $file     = rtrim($pathName, '/') . '/' . $fileName . $this->_options['file_extension'];
0158         if (file_exists($file)) {
0159             $content = file_get_contents($file);
0160             return $content;
0161         }
0162 
0163         return false;
0164     }
0165 
0166     /**
0167      * Test if a cache is available or not (for the given id)
0168      *
0169      * @param  string $id cache id
0170      * @return bool
0171      */
0172     public function test($id)
0173     {
0174         $id = $this->_decodeId($id);
0175         if (!$this->_verifyPath($id)) {
0176             Zend_Cache::throwException('Invalid cache id: does not match expected public_dir path');
0177         }
0178 
0179         $fileName = basename($id);
0180         if ($fileName === '') {
0181             $fileName = $this->_options['index_filename'];
0182         }
0183         if ($this->_tagged === null && $tagged = $this->getInnerCache()->load(self::INNER_CACHE_NAME)) {
0184             $this->_tagged = $tagged;
0185         } elseif (!$this->_tagged) {
0186             return false;
0187         }
0188         $pathName = $this->_options['public_dir'] . dirname($id);
0189 
0190         // Switch extension if needed
0191         if (isset($this->_tagged[$id])) {
0192             $extension = $this->_tagged[$id]['extension'];
0193         } else {
0194             $extension = $this->_options['file_extension'];
0195         }
0196         $file     = $pathName . '/' . $fileName . $extension;
0197         if (file_exists($file)) {
0198             return true;
0199         }
0200         return false;
0201     }
0202 
0203     /**
0204      * Save some string datas into a cache record
0205      *
0206      * Note : $data is always "string" (serialization is done by the
0207      * core not by the backend)
0208      *
0209      * @param  string $data            Datas to cache
0210      * @param  string $id              Cache id
0211      * @param  array $tags             Array of strings, the cache record will be tagged by each string entry
0212      * @param  int   $specificLifetime If != false, set a specific lifetime for this cache record (null => infinite lifetime)
0213      * @return boolean true if no problem
0214      */
0215     public function save($data, $id, $tags = array(), $specificLifetime = false)
0216     {
0217         if ($this->_options['disable_caching']) {
0218             return true;
0219         }
0220         $extension = null;
0221         if ($this->_isSerialized($data)) {
0222             $data = unserialize($data);
0223             $extension = '.' . ltrim($data[1], '.');
0224             $data = $data[0];
0225         }
0226 
0227         clearstatcache();
0228         if (($id = (string)$id) === '') {
0229             $id = $this->_detectId();
0230         } else {
0231             $id = $this->_decodeId($id);
0232         }
0233 
0234         $fileName = basename($id);
0235         if ($fileName === '') {
0236             $fileName = $this->_options['index_filename'];
0237         }
0238 
0239         $pathName = realpath($this->_options['public_dir']) . dirname($id);
0240         $this->_createDirectoriesFor($pathName);
0241 
0242         if ($id === null || strlen($id) == 0) {
0243             $dataUnserialized = unserialize($data);
0244             $data = $dataUnserialized['data'];
0245         }
0246         $ext = $this->_options['file_extension'];
0247         if ($extension) $ext = $extension;
0248         $file = rtrim($pathName, '/') . '/' . $fileName . $ext;
0249         if ($this->_options['file_locking']) {
0250             $result = file_put_contents($file, $data, LOCK_EX);
0251         } else {
0252             $result = file_put_contents($file, $data);
0253         }
0254         @chmod($file, $this->_octdec($this->_options['cache_file_perm']));
0255 
0256         if ($this->_tagged === null && $tagged = $this->getInnerCache()->load(self::INNER_CACHE_NAME)) {
0257             $this->_tagged = $tagged;
0258         } elseif ($this->_tagged === null) {
0259             $this->_tagged = array();
0260         }
0261         if (!isset($this->_tagged[$id])) {
0262             $this->_tagged[$id] = array();
0263         }
0264         if (!isset($this->_tagged[$id]['tags'])) {
0265             $this->_tagged[$id]['tags'] = array();
0266         }
0267         $this->_tagged[$id]['tags'] = array_unique(array_merge($this->_tagged[$id]['tags'], $tags));
0268         $this->_tagged[$id]['extension'] = $ext;
0269         $this->getInnerCache()->save($this->_tagged, self::INNER_CACHE_NAME);
0270         return (bool) $result;
0271     }
0272 
0273     /**
0274      * Recursively create the directories needed to write the static file
0275      */
0276     protected function _createDirectoriesFor($path)
0277     {
0278         if (!is_dir($path)) {
0279             $oldUmask = umask(0);
0280             if ( !@mkdir($path, $this->_octdec($this->_options['cache_directory_perm']), true)) {
0281                 $lastErr = error_get_last();
0282                 umask($oldUmask);
0283                 Zend_Cache::throwException("Can't create directory: {$lastErr['message']}");
0284             }
0285             umask($oldUmask);
0286         }
0287     }
0288 
0289     /**
0290      * Detect serialization of data (cannot predict since this is the only way
0291      * to obey the interface yet pass in another parameter).
0292      *
0293      * In future, ZF 2.0, check if we can just avoid the interface restraints.
0294      *
0295      * This format is the only valid one possible for the class, so it's simple
0296      * to just run a regular expression for the starting serialized format.
0297      */
0298     protected function _isSerialized($data)
0299     {
0300         return preg_match("/a:2:\{i:0;s:\d+:\"/", $data);
0301     }
0302 
0303     /**
0304      * Remove a cache record
0305      *
0306      * @param  string $id Cache id
0307      * @return boolean True if no problem
0308      */
0309     public function remove($id)
0310     {
0311         if (!$this->_verifyPath($id)) {
0312             Zend_Cache::throwException('Invalid cache id: does not match expected public_dir path');
0313         }
0314         $fileName = basename($id);
0315         if ($this->_tagged === null && $tagged = $this->getInnerCache()->load(self::INNER_CACHE_NAME)) {
0316             $this->_tagged = $tagged;
0317         } elseif (!$this->_tagged) {
0318             return false;
0319         }
0320         if (isset($this->_tagged[$id])) {
0321             $extension = $this->_tagged[$id]['extension'];
0322         } else {
0323             $extension = $this->_options['file_extension'];
0324         }
0325         if ($fileName === '') {
0326             $fileName = $this->_options['index_filename'];
0327         }
0328         $pathName = $this->_options['public_dir'] . dirname($id);
0329         $file     = realpath($pathName) . '/' . $fileName . $extension;
0330         if (!file_exists($file)) {
0331             return false;
0332         }
0333         return unlink($file);
0334     }
0335 
0336     /**
0337      * Remove a cache record recursively for the given directory matching a
0338      * REQUEST_URI based relative path (deletes the actual file matching this
0339      * in addition to the matching directory)
0340      *
0341      * @param  string $id Cache id
0342      * @return boolean True if no problem
0343      */
0344     public function removeRecursively($id)
0345     {
0346         if (!$this->_verifyPath($id)) {
0347             Zend_Cache::throwException('Invalid cache id: does not match expected public_dir path');
0348         }
0349         $fileName = basename($id);
0350         if ($fileName === '') {
0351             $fileName = $this->_options['index_filename'];
0352         }
0353         $pathName  = $this->_options['public_dir'] . dirname($id);
0354         $file      = $pathName . '/' . $fileName . $this->_options['file_extension'];
0355         $directory = $pathName . '/' . $fileName;
0356         if (file_exists($directory)) {
0357             if (!is_writable($directory)) {
0358                 return false;
0359             }
0360             if (is_dir($directory)) {
0361                 foreach (new DirectoryIterator($directory) as $file) {
0362                     if (true === $file->isFile()) {
0363                         if (false === unlink($file->getPathName())) {
0364                             return false;
0365                         }
0366                     }
0367                 }
0368             }
0369             rmdir($directory);
0370         }
0371         if (file_exists($file)) {
0372             if (!is_writable($file)) {
0373                 return false;
0374             }
0375             return unlink($file);
0376         }
0377         return true;
0378     }
0379 
0380     /**
0381      * Clean some cache records
0382      *
0383      * Available modes are :
0384      * Zend_Cache::CLEANING_MODE_ALL (default)    => remove all cache entries ($tags is not used)
0385      * Zend_Cache::CLEANING_MODE_OLD              => remove too old cache entries ($tags is not used)
0386      * Zend_Cache::CLEANING_MODE_MATCHING_TAG     => remove cache entries matching all given tags
0387      *                                               ($tags can be an array of strings or a single string)
0388      * Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG => remove cache entries not {matching one of the given tags}
0389      *                                               ($tags can be an array of strings or a single string)
0390      * Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG => remove cache entries matching any given tags
0391      *                                               ($tags can be an array of strings or a single string)
0392      *
0393      * @param  string $mode Clean mode
0394      * @param  array  $tags Array of tags
0395      * @return boolean true if no problem
0396      * @throws Zend_Exception
0397      */
0398     public function clean($mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array())
0399     {
0400         $result = false;
0401         switch ($mode) {
0402             case Zend_Cache::CLEANING_MODE_MATCHING_TAG:
0403             case Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG:
0404                 if (empty($tags)) {
0405                     throw new Zend_Exception('Cannot use tag matching modes as no tags were defined');
0406                 }
0407                 if ($this->_tagged === null && $tagged = $this->getInnerCache()->load(self::INNER_CACHE_NAME)) {
0408                     $this->_tagged = $tagged;
0409                 } elseif (!$this->_tagged) {
0410                     return true;
0411                 }
0412                 foreach ($tags as $tag) {
0413                     $urls = array_keys($this->_tagged);
0414                     foreach ($urls as $url) {
0415                         if (isset($this->_tagged[$url]['tags']) && in_array($tag, $this->_tagged[$url]['tags'])) {
0416                             $this->remove($url);
0417                             unset($this->_tagged[$url]);
0418                         }
0419                     }
0420                 }
0421                 $this->getInnerCache()->save($this->_tagged, self::INNER_CACHE_NAME);
0422                 $result = true;
0423                 break;
0424             case Zend_Cache::CLEANING_MODE_ALL:
0425                 if ($this->_tagged === null) {
0426                     $tagged = $this->getInnerCache()->load(self::INNER_CACHE_NAME);
0427                     $this->_tagged = $tagged;
0428                 }
0429                 if ($this->_tagged === null || empty($this->_tagged)) {
0430                     return true;
0431                 }
0432                 $urls = array_keys($this->_tagged);
0433                 foreach ($urls as $url) {
0434                     $this->remove($url);
0435                     unset($this->_tagged[$url]);
0436                 }
0437                 $this->getInnerCache()->save($this->_tagged, self::INNER_CACHE_NAME);
0438                 $result = true;
0439                 break;
0440             case Zend_Cache::CLEANING_MODE_OLD:
0441                 $this->_log("Zend_Cache_Backend_Static : Selected Cleaning Mode Currently Unsupported By This Backend");
0442                 break;
0443             case Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG:
0444                 if (empty($tags)) {
0445                     throw new Zend_Exception('Cannot use tag matching modes as no tags were defined');
0446                 }
0447                 if ($this->_tagged === null) {
0448                     $tagged = $this->getInnerCache()->load(self::INNER_CACHE_NAME);
0449                     $this->_tagged = $tagged;
0450                 }
0451                 if ($this->_tagged === null || empty($this->_tagged)) {
0452                     return true;
0453                 }
0454                 $urls = array_keys($this->_tagged);
0455                 foreach ($urls as $url) {
0456                     $difference = array_diff($tags, $this->_tagged[$url]['tags']);
0457                     if (count($tags) == count($difference)) {
0458                         $this->remove($url);
0459                         unset($this->_tagged[$url]);
0460                     }
0461                 }
0462                 $this->getInnerCache()->save($this->_tagged, self::INNER_CACHE_NAME);
0463                 $result = true;
0464                 break;
0465             default:
0466                 Zend_Cache::throwException('Invalid mode for clean() method');
0467                 break;
0468         }
0469         return $result;
0470     }
0471 
0472     /**
0473      * Set an Inner Cache, used here primarily to store Tags associated
0474      * with caches created by this backend. Note: If Tags are lost, the cache
0475      * should be completely cleaned as the mapping of tags to caches will
0476      * have been irrevocably lost.
0477      *
0478      * @param  Zend_Cache_Core
0479      * @return void
0480      */
0481     public function setInnerCache(Zend_Cache_Core $cache)
0482     {
0483         $this->_tagCache = $cache;
0484         $this->_options['tag_cache'] = $cache;
0485     }
0486 
0487     /**
0488      * Get the Inner Cache if set
0489      *
0490      * @return Zend_Cache_Core
0491      */
0492     public function getInnerCache()
0493     {
0494         if ($this->_tagCache === null) {
0495             Zend_Cache::throwException('An Inner Cache has not been set; use setInnerCache()');
0496         }
0497         return $this->_tagCache;
0498     }
0499 
0500     /**
0501      * Verify path exists and is non-empty
0502      *
0503      * @param  string $path
0504      * @return bool
0505      */
0506     protected function _verifyPath($path)
0507     {
0508         $path = realpath($path);
0509         $base = realpath($this->_options['public_dir']);
0510         return strncmp($path, $base, strlen($base)) !== 0;
0511     }
0512 
0513     /**
0514      * Determine the page to save from the request
0515      *
0516      * @return string
0517      */
0518     protected function _detectId()
0519     {
0520         return $_SERVER['REQUEST_URI'];
0521     }
0522 
0523     /**
0524      * Validate a cache id or a tag (security, reliable filenames, reserved prefixes...)
0525      *
0526      * Throw an exception if a problem is found
0527      *
0528      * @param  string $string Cache id or tag
0529      * @throws Zend_Cache_Exception
0530      * @return void
0531      * @deprecated Not usable until perhaps ZF 2.0
0532      */
0533     protected static function _validateIdOrTag($string)
0534     {
0535         if (!is_string($string)) {
0536             Zend_Cache::throwException('Invalid id or tag : must be a string');
0537         }
0538 
0539         // Internal only checked in Frontend - not here!
0540         if (substr($string, 0, 9) == 'internal-') {
0541             return;
0542         }
0543 
0544         // Validation assumes no query string, fragments or scheme included - only the path
0545         if (!preg_match(
0546                 '/^(?:\/(?:(?:%[[:xdigit:]]{2}|[A-Za-z0-9-_.!~*\'()\[\]:@&=+$,;])*)?)+$/',
0547                 $string
0548             )
0549         ) {
0550             Zend_Cache::throwException("Invalid id or tag '$string' : must be a valid URL path");
0551         }
0552     }
0553 
0554     /**
0555      * Detect an octal string and return its octal value for file permission ops
0556      * otherwise return the non-string (assumed octal or decimal int already)
0557      *
0558      * @param string $val The potential octal in need of conversion
0559      * @return int
0560      */
0561     protected function _octdec($val)
0562     {
0563         if (is_string($val) && decoct(octdec($val)) == $val) {
0564             return octdec($val);
0565         }
0566         return $val;
0567     }
0568 
0569     /**
0570      * Decode a request URI from the provided ID
0571      *
0572      * @param string $id
0573      * @return string
0574      */
0575     protected function _decodeId($id)
0576     {
0577         return pack('H*', $id);
0578     }
0579 }