1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14:
15: namespace Cake\Cache\Engine;
16:
17: use Cake\Cache\CacheEngine;
18: use Cake\Utility\Inflector;
19: use CallbackFilterIterator;
20: use Exception;
21: use LogicException;
22: use RecursiveDirectoryIterator;
23: use RecursiveIteratorIterator;
24: use SplFileInfo;
25: use SplFileObject;
26:
27: 28: 29: 30: 31: 32: 33:
34: class FileEngine extends CacheEngine
35: {
36:
37: 38: 39: 40: 41:
42: protected $_File;
43:
44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61:
62: protected $_defaultConfig = [
63: 'duration' => 3600,
64: 'groups' => [],
65: 'isWindows' => false,
66: 'lock' => true,
67: 'mask' => 0664,
68: 'path' => null,
69: 'prefix' => 'cake_',
70: 'probability' => 100,
71: 'serialize' => true
72: ];
73:
74: 75: 76: 77: 78:
79: protected $_init = true;
80:
81: 82: 83: 84: 85: 86: 87: 88:
89: public function init(array $config = [])
90: {
91: parent::init($config);
92:
93: if ($this->_config['path'] === null) {
94: $this->_config['path'] = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'cake_cache' . DIRECTORY_SEPARATOR;
95: }
96: if (DIRECTORY_SEPARATOR === '\\') {
97: $this->_config['isWindows'] = true;
98: }
99: if (substr($this->_config['path'], -1) !== DIRECTORY_SEPARATOR) {
100: $this->_config['path'] .= DIRECTORY_SEPARATOR;
101: }
102: if ($this->_groupPrefix) {
103: $this->_groupPrefix = str_replace('_', DIRECTORY_SEPARATOR, $this->_groupPrefix);
104: }
105:
106: return $this->_active();
107: }
108:
109: 110: 111: 112: 113: 114:
115: public function gc($expires = null)
116: {
117: return $this->clear(true);
118: }
119:
120: 121: 122: 123: 124: 125: 126:
127: public function write($key, $data)
128: {
129: if ($data === '' || !$this->_init) {
130: return false;
131: }
132:
133: $key = $this->_key($key);
134:
135: if ($this->_setKey($key, true) === false) {
136: return false;
137: }
138:
139: $lineBreak = "\n";
140:
141: if ($this->_config['isWindows']) {
142: $lineBreak = "\r\n";
143: }
144:
145: if (!empty($this->_config['serialize'])) {
146: if ($this->_config['isWindows']) {
147: $data = str_replace('\\', '\\\\\\\\', serialize($data));
148: } else {
149: $data = serialize($data);
150: }
151: }
152:
153: $duration = $this->_config['duration'];
154: $expires = time() + $duration;
155: $contents = implode([$expires, $lineBreak, $data, $lineBreak]);
156:
157: if ($this->_config['lock']) {
158: $this->_File->flock(LOCK_EX);
159: }
160:
161: $this->_File->rewind();
162: $success = $this->_File->ftruncate(0) &&
163: $this->_File->fwrite($contents) &&
164: $this->_File->fflush();
165:
166: if ($this->_config['lock']) {
167: $this->_File->flock(LOCK_UN);
168: }
169: $this->_File = null;
170:
171: return $success;
172: }
173:
174: 175: 176: 177: 178: 179: 180:
181: public function read($key)
182: {
183: $key = $this->_key($key);
184:
185: if (!$this->_init || $this->_setKey($key) === false) {
186: return false;
187: }
188:
189: if ($this->_config['lock']) {
190: $this->_File->flock(LOCK_SH);
191: }
192:
193: $this->_File->rewind();
194: $time = time();
195: $cachetime = (int)$this->_File->current();
196:
197: if ($cachetime < $time) {
198: if ($this->_config['lock']) {
199: $this->_File->flock(LOCK_UN);
200: }
201:
202: return false;
203: }
204:
205: $data = '';
206: $this->_File->next();
207: while ($this->_File->valid()) {
208: $data .= $this->_File->current();
209: $this->_File->next();
210: }
211:
212: if ($this->_config['lock']) {
213: $this->_File->flock(LOCK_UN);
214: }
215:
216: $data = trim($data);
217:
218: if ($data !== '' && !empty($this->_config['serialize'])) {
219: if ($this->_config['isWindows']) {
220: $data = str_replace('\\\\\\\\', '\\', $data);
221: }
222: $data = unserialize((string)$data);
223: }
224:
225: return $data;
226: }
227:
228: 229: 230: 231: 232: 233: 234:
235: public function delete($key)
236: {
237: $key = $this->_key($key);
238:
239: if ($this->_setKey($key) === false || !$this->_init) {
240: return false;
241: }
242:
243: $path = $this->_File->getRealPath();
244: $this->_File = null;
245:
246:
247: return @unlink($path);
248:
249: }
250:
251: 252: 253: 254: 255: 256:
257: public function clear($check)
258: {
259: if (!$this->_init) {
260: return false;
261: }
262: $this->_File = null;
263:
264: $threshold = $now = false;
265: if ($check) {
266: $now = time();
267: $threshold = $now - $this->_config['duration'];
268: }
269:
270: $this->_clearDirectory($this->_config['path'], $now, $threshold);
271:
272: $directory = new RecursiveDirectoryIterator(
273: $this->_config['path'],
274: \FilesystemIterator::SKIP_DOTS
275: );
276: $contents = new RecursiveIteratorIterator(
277: $directory,
278: RecursiveIteratorIterator::SELF_FIRST
279: );
280: $cleared = [];
281: foreach ($contents as $path) {
282: if ($path->isFile()) {
283: continue;
284: }
285:
286: $path = $path->getRealPath() . DIRECTORY_SEPARATOR;
287: if (!in_array($path, $cleared)) {
288: $this->_clearDirectory($path, $now, $threshold);
289: $cleared[] = $path;
290: }
291: }
292:
293: return true;
294: }
295:
296: 297: 298: 299: 300: 301: 302: 303:
304: protected function _clearDirectory($path, $now, $threshold)
305: {
306: if (!is_dir($path)) {
307: return;
308: }
309: $prefixLength = strlen($this->_config['prefix']);
310:
311: $dir = dir($path);
312: while (($entry = $dir->read()) !== false) {
313: if (substr($entry, 0, $prefixLength) !== $this->_config['prefix']) {
314: continue;
315: }
316:
317: try {
318: $file = new SplFileObject($path . $entry, 'r');
319: } catch (Exception $e) {
320: continue;
321: }
322:
323: if ($threshold) {
324: $mtime = $file->getMTime();
325: if ($mtime > $threshold) {
326: continue;
327: }
328:
329: $expires = (int)$file->current();
330: if ($expires > $now) {
331: continue;
332: }
333: }
334: if ($file->isFile()) {
335: $filePath = $file->getRealPath();
336: $file = null;
337:
338:
339: @unlink($filePath);
340:
341: }
342: }
343:
344: $dir->close();
345: }
346:
347: 348: 349: 350: 351: 352: 353: 354:
355: public function decrement($key, $offset = 1)
356: {
357: throw new LogicException('Files cannot be atomically decremented.');
358: }
359:
360: 361: 362: 363: 364: 365: 366: 367:
368: public function increment($key, $offset = 1)
369: {
370: throw new LogicException('Files cannot be atomically incremented.');
371: }
372:
373: 374: 375: 376: 377: 378: 379: 380:
381: protected function _setKey($key, $createKey = false)
382: {
383: $groups = null;
384: if ($this->_groupPrefix) {
385: $groups = vsprintf($this->_groupPrefix, $this->groups());
386: }
387: $dir = $this->_config['path'] . $groups;
388:
389: if (!is_dir($dir)) {
390: mkdir($dir, 0775, true);
391: }
392:
393: $path = new SplFileInfo($dir . $key);
394:
395: if (!$createKey && !$path->isFile()) {
396: return false;
397: }
398: if (empty($this->_File) ||
399: $this->_File->getBasename() !== $key ||
400: $this->_File->valid() === false
401: ) {
402: $exists = file_exists($path->getPathname());
403: try {
404: $this->_File = $path->openFile('c+');
405: } catch (Exception $e) {
406: trigger_error($e->getMessage(), E_USER_WARNING);
407:
408: return false;
409: }
410: unset($path);
411:
412: if (!$exists && !chmod($this->_File->getPathname(), (int)$this->_config['mask'])) {
413: trigger_error(sprintf(
414: 'Could not apply permission mask "%s" on cache file "%s"',
415: $this->_File->getPathname(),
416: $this->_config['mask']
417: ), E_USER_WARNING);
418: }
419: }
420:
421: return true;
422: }
423:
424: 425: 426: 427: 428:
429: protected function _active()
430: {
431: $dir = new SplFileInfo($this->_config['path']);
432: $path = $dir->getPathname();
433: $success = true;
434: if (!is_dir($path)) {
435:
436: $success = @mkdir($path, 0775, true);
437:
438: }
439:
440: $isWritableDir = ($dir->isDir() && $dir->isWritable());
441: if (!$success || ($this->_init && !$isWritableDir)) {
442: $this->_init = false;
443: trigger_error(sprintf(
444: '%s is not writable',
445: $this->_config['path']
446: ), E_USER_WARNING);
447: }
448:
449: return $success;
450: }
451:
452: 453: 454: 455: 456: 457:
458: public function key($key)
459: {
460: if (empty($key)) {
461: return false;
462: }
463:
464: $key = Inflector::underscore(str_replace(
465: [DIRECTORY_SEPARATOR, '/', '.', '<', '>', '?', ':', '|', '*', '"'],
466: '_',
467: (string)$key
468: ));
469:
470: return $key;
471: }
472:
473: 474: 475: 476: 477: 478:
479: public function clearGroup($group)
480: {
481: $this->_File = null;
482:
483: $prefix = (string)$this->_config['prefix'];
484:
485: $directoryIterator = new RecursiveDirectoryIterator($this->_config['path']);
486: $contents = new RecursiveIteratorIterator(
487: $directoryIterator,
488: RecursiveIteratorIterator::CHILD_FIRST
489: );
490: $filtered = new CallbackFilterIterator(
491: $contents,
492: function (SplFileInfo $current) use ($group, $prefix) {
493: if (!$current->isFile()) {
494: return false;
495: }
496:
497: $hasPrefix = $prefix === ''
498: || strpos($current->getBasename(), $prefix) === 0;
499: if ($hasPrefix === false) {
500: return false;
501: }
502:
503: $pos = strpos(
504: $current->getPathname(),
505: DIRECTORY_SEPARATOR . $group . DIRECTORY_SEPARATOR
506: );
507:
508: return $pos !== false;
509: }
510: );
511: foreach ($filtered as $object) {
512: $path = $object->getPathname();
513: $object = null;
514:
515: @unlink($path);
516: }
517:
518: return true;
519: }
520: }
521: