1<?php 2/** 3 * File Storage engine for cache. Filestorage is the slowest cache storage 4 * to read and write. However, it is good for servers that don't have other storage 5 * engine available, or have content which is not performance sensitive. 6 * 7 * You can configure a FileEngine cache, using Cache::config() 8 * 9 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org) 10 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) 11 * 12 * Licensed under The MIT License 13 * For full copyright and license information, please see the LICENSE.txt 14 * Redistributions of files must retain the above copyright notice. 15 * 16 * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) 17 * @link https://cakephp.org CakePHP(tm) Project 18 * @since CakePHP(tm) v 1.2.0.4933 19 * @license https://opensource.org/licenses/mit-license.php MIT License 20 */ 21 22/** 23 * File Storage engine for cache. Filestorage is the slowest cache storage 24 * to read and write. However, it is good for servers that don't have other storage 25 * engine available, or have content which is not performance sensitive. 26 * 27 * You can configure a FileEngine cache, using Cache::config() 28 * 29 * @package Cake.Cache.Engine 30 */ 31class FileEngine extends CacheEngine { 32 33/** 34 * Instance of SplFileObject class 35 * 36 * @var File 37 */ 38 protected $_File = null; 39 40/** 41 * Settings 42 * 43 * - path = absolute path to cache directory, default => CACHE 44 * - prefix = string prefix for filename, default => cake_ 45 * - lock = enable file locking on write, default => true 46 * - serialize = serialize the data, default => true 47 * 48 * @var array 49 * @see CacheEngine::__defaults 50 */ 51 public $settings = array(); 52 53/** 54 * True unless FileEngine::__active(); fails 55 * 56 * @var bool 57 */ 58 protected $_init = true; 59 60/** 61 * Initialize the Cache Engine 62 * 63 * Called automatically by the cache frontend 64 * To reinitialize the settings call Cache::engine('EngineName', [optional] settings = array()); 65 * 66 * @param array $settings array of setting for the engine 67 * @return bool True if the engine has been successfully initialized, false if not 68 */ 69 public function init($settings = array()) { 70 $settings += array( 71 'engine' => 'File', 72 'path' => CACHE, 73 'prefix' => 'cake_', 74 'lock' => true, 75 'serialize' => true, 76 'isWindows' => false, 77 'mask' => 0664 78 ); 79 parent::init($settings); 80 81 if (DS === '\\') { 82 $this->settings['isWindows'] = true; 83 } 84 if (substr($this->settings['path'], -1) !== DS) { 85 $this->settings['path'] .= DS; 86 } 87 if (!empty($this->_groupPrefix)) { 88 $this->_groupPrefix = str_replace('_', DS, $this->_groupPrefix); 89 } 90 return $this->_active(); 91 } 92 93/** 94 * Garbage collection. Permanently remove all expired and deleted data 95 * 96 * @param int $expires [optional] An expires timestamp, invalidating all data before. 97 * @return bool True if garbage collection was successful, false on failure 98 */ 99 public function gc($expires = null) { 100 return $this->clear(true); 101 } 102 103/** 104 * Write data for key into cache 105 * 106 * @param string $key Identifier for the data 107 * @param mixed $data Data to be cached 108 * @param int $duration How long to cache the data, in seconds 109 * @return bool True if the data was successfully cached, false on failure 110 */ 111 public function write($key, $data, $duration) { 112 if (!$this->_init) { 113 return false; 114 } 115 116 if ($this->_setKey($key, true) === false) { 117 return false; 118 } 119 120 $lineBreak = "\n"; 121 122 if ($this->settings['isWindows']) { 123 $lineBreak = "\r\n"; 124 } 125 126 if (!empty($this->settings['serialize'])) { 127 if ($this->settings['isWindows']) { 128 $data = str_replace('\\', '\\\\\\\\', serialize($data)); 129 } else { 130 $data = serialize($data); 131 } 132 } 133 134 $expires = time() + $duration; 135 $contents = implode(array($expires, $lineBreak, $data, $lineBreak)); 136 137 if ($this->settings['lock']) { 138 $this->_File->flock(LOCK_EX); 139 } 140 141 $this->_File->rewind(); 142 $success = $this->_File->ftruncate(0) && $this->_File->fwrite($contents) && $this->_File->fflush(); 143 144 if ($this->settings['lock']) { 145 $this->_File->flock(LOCK_UN); 146 } 147 148 return $success; 149 } 150 151/** 152 * Read a key from the cache 153 * 154 * @param string $key Identifier for the data 155 * @return mixed The cached data, or false if the data doesn't exist, has expired, or if there was an error fetching it 156 */ 157 public function read($key) { 158 if (!$this->_init || $this->_setKey($key) === false) { 159 return false; 160 } 161 162 if ($this->settings['lock']) { 163 $this->_File->flock(LOCK_SH); 164 } 165 166 $this->_File->rewind(); 167 $time = time(); 168 $cachetime = (int)$this->_File->current(); 169 170 if ($cachetime !== false && ($cachetime < $time || ($time + $this->settings['duration']) < $cachetime)) { 171 if ($this->settings['lock']) { 172 $this->_File->flock(LOCK_UN); 173 } 174 return false; 175 } 176 177 $data = ''; 178 $this->_File->next(); 179 while ($this->_File->valid()) { 180 $data .= $this->_File->current(); 181 $this->_File->next(); 182 } 183 184 if ($this->settings['lock']) { 185 $this->_File->flock(LOCK_UN); 186 } 187 188 $data = trim($data); 189 190 if ($data !== '' && !empty($this->settings['serialize'])) { 191 if ($this->settings['isWindows']) { 192 $data = str_replace('\\\\\\\\', '\\', $data); 193 } 194 $data = unserialize((string)$data); 195 } 196 return $data; 197 } 198 199/** 200 * Delete a key from the cache 201 * 202 * @param string $key Identifier for the data 203 * @return bool True if the value was successfully deleted, false if it didn't exist or couldn't be removed 204 */ 205 public function delete($key) { 206 if ($this->_setKey($key) === false || !$this->_init) { 207 return false; 208 } 209 $path = $this->_File->getRealPath(); 210 $this->_File = null; 211 212 //@codingStandardsIgnoreStart 213 return @unlink($path); 214 //@codingStandardsIgnoreEnd 215 } 216 217/** 218 * Delete all values from the cache 219 * 220 * @param bool $check Optional - only delete expired cache items 221 * @return bool True if the cache was successfully cleared, false otherwise 222 */ 223 public function clear($check) { 224 if (!$this->_init) { 225 return false; 226 } 227 $this->_File = null; 228 229 $threshold = $now = false; 230 if ($check) { 231 $now = time(); 232 $threshold = $now - $this->settings['duration']; 233 } 234 235 $this->_clearDirectory($this->settings['path'], $now, $threshold); 236 237 $directory = new RecursiveDirectoryIterator($this->settings['path']); 238 $contents = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST); 239 $cleared = array(); 240 foreach ($contents as $path) { 241 if ($path->isFile()) { 242 continue; 243 } 244 245 $path = $path->getRealPath() . DS; 246 if (!in_array($path, $cleared)) { 247 $this->_clearDirectory($path, $now, $threshold); 248 $cleared[] = $path; 249 } 250 } 251 return true; 252 } 253 254/** 255 * Used to clear a directory of matching files. 256 * 257 * @param string $path The path to search. 258 * @param int $now The current timestamp 259 * @param int $threshold Any file not modified after this value will be deleted. 260 * @return void 261 */ 262 protected function _clearDirectory($path, $now, $threshold) { 263 $prefixLength = strlen($this->settings['prefix']); 264 265 if (!is_dir($path)) { 266 return; 267 } 268 269 $dir = dir($path); 270 if ($dir === false) { 271 return; 272 } 273 274 while (($entry = $dir->read()) !== false) { 275 if (substr($entry, 0, $prefixLength) !== $this->settings['prefix']) { 276 continue; 277 } 278 279 try { 280 $file = new SplFileObject($path . $entry, 'r'); 281 } catch (Exception $e) { 282 continue; 283 } 284 285 if ($threshold) { 286 $mtime = $file->getMTime(); 287 288 if ($mtime > $threshold) { 289 continue; 290 } 291 $expires = (int)$file->current(); 292 293 if ($expires > $now) { 294 continue; 295 } 296 } 297 if ($file->isFile()) { 298 $filePath = $file->getRealPath(); 299 $file = null; 300 301 //@codingStandardsIgnoreStart 302 @unlink($filePath); 303 //@codingStandardsIgnoreEnd 304 } 305 } 306 } 307 308/** 309 * Not implemented 310 * 311 * @param string $key The key to decrement 312 * @param int $offset The number to offset 313 * @return void 314 * @throws CacheException 315 */ 316 public function decrement($key, $offset = 1) { 317 throw new CacheException(__d('cake_dev', 'Files cannot be atomically decremented.')); 318 } 319 320/** 321 * Not implemented 322 * 323 * @param string $key The key to decrement 324 * @param int $offset The number to offset 325 * @return void 326 * @throws CacheException 327 */ 328 public function increment($key, $offset = 1) { 329 throw new CacheException(__d('cake_dev', 'Files cannot be atomically incremented.')); 330 } 331 332/** 333 * Sets the current cache key this class is managing, and creates a writable SplFileObject 334 * for the cache file the key is referring to. 335 * 336 * @param string $key The key 337 * @param bool $createKey Whether the key should be created if it doesn't exists, or not 338 * @return bool true if the cache key could be set, false otherwise 339 */ 340 protected function _setKey($key, $createKey = false) { 341 $groups = null; 342 if (!empty($this->_groupPrefix)) { 343 $groups = vsprintf($this->_groupPrefix, $this->groups()); 344 } 345 $dir = $this->settings['path'] . $groups; 346 347 if (!is_dir($dir)) { 348 mkdir($dir, 0775, true); 349 } 350 $path = new SplFileInfo($dir . $key); 351 352 if (!$createKey && !$path->isFile()) { 353 return false; 354 } 355 if ( 356 empty($this->_File) || 357 $this->_File->getBaseName() !== $key || 358 $this->_File->valid() === false 359 ) { 360 $exists = file_exists($path->getPathname()); 361 try { 362 $this->_File = $path->openFile('c+'); 363 } catch (Exception $e) { 364 trigger_error($e->getMessage(), E_USER_WARNING); 365 return false; 366 } 367 unset($path); 368 369 if (!$exists && !chmod($this->_File->getPathname(), (int)$this->settings['mask'])) { 370 trigger_error(__d( 371 'cake_dev', 'Could not apply permission mask "%s" on cache file "%s"', 372 array($this->_File->getPathname(), $this->settings['mask'])), E_USER_WARNING); 373 } 374 } 375 return true; 376 } 377 378/** 379 * Determine is cache directory is writable 380 * 381 * @return bool 382 */ 383 protected function _active() { 384 $dir = new SplFileInfo($this->settings['path']); 385 if (Configure::read('debug')) { 386 $path = $dir->getPathname(); 387 if (!is_dir($path)) { 388 mkdir($path, 0775, true); 389 } 390 } 391 if ($this->_init && !($dir->isDir() && $dir->isWritable())) { 392 $this->_init = false; 393 trigger_error(__d('cake_dev', '%s is not writable', $this->settings['path']), E_USER_WARNING); 394 return false; 395 } 396 return true; 397 } 398 399/** 400 * Generates a safe key for use with cache engine storage engines. 401 * 402 * @param string $key the key passed over 403 * @return mixed string $key or false 404 */ 405 public function key($key) { 406 if (empty($key)) { 407 return false; 408 } 409 410 $key = Inflector::underscore(str_replace(array(DS, '/', '.', '<', '>', '?', ':', '|', '*', '"'), '_', strval($key))); 411 return $key; 412 } 413 414/** 415 * Recursively deletes all files under any directory named as $group 416 * 417 * @param string $group The group to clear. 418 * @return bool success 419 */ 420 public function clearGroup($group) { 421 $this->_File = null; 422 $directoryIterator = new RecursiveDirectoryIterator($this->settings['path']); 423 $contents = new RecursiveIteratorIterator($directoryIterator, RecursiveIteratorIterator::CHILD_FIRST); 424 foreach ($contents as $object) { 425 $containsGroup = strpos($object->getPathName(), DS . $group . DS) !== false; 426 $hasPrefix = true; 427 if (strlen($this->settings['prefix']) !== 0) { 428 $hasPrefix = strpos($object->getBaseName(), $this->settings['prefix']) === 0; 429 } 430 if ($object->isFile() && $containsGroup && $hasPrefix) { 431 $path = $object->getPathName(); 432 $object = null; 433 //@codingStandardsIgnoreStart 434 @unlink($path); 435 //@codingStandardsIgnoreEnd 436 } 437 } 438 return true; 439 } 440 441/** 442 * Write data for key into cache if it doesn't exist already. 443 * If it already exists, it fails and returns false. 444 * 445 * @param string $key Identifier for the data. 446 * @param mixed $value Data to be cached. 447 * @param int $duration How long to cache the data, in seconds. 448 * @return bool True if the data was successfully cached, false on failure. 449 */ 450 public function add($key, $value, $duration) { 451 $cachedValue = $this->read($key); 452 if ($cachedValue === false) { 453 return $this->write($key, $value, $duration); 454 } 455 return false; 456 } 457} 458