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