MDL-35558 mod_data: Show only own entries while there are required pending.
[moodle.git] / cache / stores / file / lib.php
CommitLineData
62704f33
SH
1<?php
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16
17/**
18 * The library file for the file cache store.
19 *
20 * This file is part of the file cache store, it contains the API for interacting with an instance of the store.
21 * This is used as a default cache store within the Cache API. It should never be deleted.
22 *
6fec1820 23 * @package cachestore_file
62704f33
SH
24 * @category cache
25 * @copyright 2012 Sam Hemelryk
26 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27 */
28
29/**
30 * The file store class.
31 *
32 * Configuration options
33 * path: string: path to the cache directory, if left empty one will be created in the cache directory
34 * autocreate: true, false
35 * prescan: true, false
36 *
37 * @copyright 2012 Sam Hemelryk
38 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39 */
34c84c72 40class cachestore_file implements cache_store, cache_is_key_aware {
62704f33
SH
41
42 /**
43 * The name of the store.
44 * @var string
45 */
46 protected $name;
47
48 /**
49 * The path to use for the file storage.
50 * @var string
51 */
52 protected $path = null;
53
54 /**
55 * Set to true when a prescan has been performed.
56 * @var bool
57 */
58 protected $prescan = false;
59
08aaa637
SH
60 /**
61 * Set to true if we should store files within a single directory.
62 * By default we use a nested structure in order to reduce the chance of conflicts and avoid any file system
63 * limitations such as maximum files per directory.
64 * @var bool
65 */
66 protected $singledirectory = false;
67
62704f33
SH
68 /**
69 * Set to true when the path should be automatically created if it does not yet exist.
70 * @var bool
71 */
72 protected $autocreate = false;
73
74 /**
75 * Set to true if a custom path is being used.
76 * @var bool
77 */
78 protected $custompath = false;
79
80 /**
81 * An array of keys we are sure about presently.
82 * @var array
83 */
84 protected $keys = array();
85
86 /**
87 * True when the store is ready to be initialised.
88 * @var bool
89 */
90 protected $isready = false;
91
62704f33
SH
92 /**
93 * The cache definition this instance has been initialised with.
94 * @var cache_definition
95 */
96 protected $definition;
97
98 /**
99 * Constructs the store instance.
100 *
101 * Noting that this function is not an initialisation. It is used to prepare the store for use.
102 * The store will be initialised when required and will be provided with a cache_definition at that time.
103 *
104 * @param string $name
105 * @param array $configuration
106 */
107 public function __construct($name, array $configuration = array()) {
108 $this->name = $name;
109 if (array_key_exists('path', $configuration) && $configuration['path'] !== '') {
110 $this->custompath = true;
111 $this->autocreate = !empty($configuration['autocreate']);
112 $path = (string)$configuration['path'];
113 if (!is_dir($path)) {
114 if ($this->autocreate) {
115 if (!make_writable_directory($path, false)) {
116 $path = false;
117 debugging('Error trying to autocreate file store path. '.$path, DEBUG_DEVELOPER);
118 }
119 } else {
120 $path = false;
121 debugging('The given file cache store path does not exist. '.$path, DEBUG_DEVELOPER);
122 }
123 }
124 if ($path !== false && !is_writable($path)) {
125 $path = false;
126 debugging('The given file cache store path is not writable. '.$path, DEBUG_DEVELOPER);
127 }
128 } else {
6fec1820 129 $path = make_cache_directory('cachestore_file/'.preg_replace('#[^a-zA-Z0-9\.\-_]+#', '', $name));
62704f33
SH
130 }
131 $this->isready = $path !== false;
132 $this->path = $path;
08aaa637
SH
133 // Check if we should prescan the directory.
134 if (array_key_exists('prescan', $configuration)) {
702651c7 135 $this->prescan = (bool)$configuration['prescan'];
08aaa637
SH
136 } else {
137 // Default is no, we should not prescan.
702651c7 138 $this->prescan = false;
08aaa637
SH
139 }
140 // Check if we should be storing in a single directory.
141 if (array_key_exists('singledirectory', $configuration)) {
702651c7 142 $this->singledirectory = (bool)$configuration['singledirectory'];
08aaa637
SH
143 } else {
144 // Default: No, we will use multiple directories.
702651c7 145 $this->singledirectory = false;
08aaa637 146 }
62704f33
SH
147 }
148
149 /**
150 * Returns true if this store instance is ready to be used.
151 * @return bool
152 */
153 public function is_ready() {
154 return ($this->path !== null);
155 }
156
157 /**
158 * Returns true once this instance has been initialised.
159 *
160 * @return bool
161 */
162 public function is_initialised() {
163 return true;
164 }
165
166 /**
167 * Returns the supported features as a combined int.
168 *
169 * @param array $configuration
170 * @return int
171 */
172 public static function get_supported_features(array $configuration = array()) {
173 $supported = self::SUPPORTS_DATA_GUARANTEE +
174 self::SUPPORTS_NATIVE_TTL;
175 return $supported;
176 }
177
178 /**
179 * Returns the supported modes as a combined int.
180 *
181 * @param array $configuration
182 * @return int
183 */
184 public static function get_supported_modes(array $configuration = array()) {
185 return self::MODE_APPLICATION + self::MODE_SESSION;
186 }
187
188 /**
189 * Returns true if the store requirements are met.
190 *
191 * @return bool
192 */
193 public static function are_requirements_met() {
194 return true;
195 }
196
197 /**
198 * Returns true if the given mode is supported by this store.
199 *
200 * @param int $mode One of cache_store::MODE_*
201 * @return bool
202 */
203 public static function is_supported_mode($mode) {
204 return ($mode === self::MODE_APPLICATION || $mode === self::MODE_SESSION);
205 }
206
207 /**
208 * Returns true if the store instance supports multiple identifiers.
209 *
210 * @return bool
211 */
212 public function supports_multiple_indentifiers() {
213 return false;
214 }
215
216 /**
217 * Returns true if the store instance guarantees data.
218 *
219 * @return bool
220 */
221 public function supports_data_guarantee() {
222 return true;
223 }
224
225 /**
226 * Returns true if the store instance supports native ttl.
227 *
228 * @return bool
229 */
230 public function supports_native_ttl() {
231 return true;
232 }
233
234 /**
235 * Initialises the cache.
236 *
237 * Once this has been done the cache is all set to be used.
238 *
239 * @param cache_definition $definition
240 */
241 public function initialise(cache_definition $definition) {
242 $this->definition = $definition;
243 $hash = preg_replace('#[^a-zA-Z0-9]+#', '_', $this->definition->get_id());
244 $this->path .= '/'.$hash;
245 make_writable_directory($this->path);
246 if ($this->prescan && $definition->get_mode() !== self::MODE_REQUEST) {
247 $this->prescan = false;
248 }
249 if ($this->prescan) {
08aaa637
SH
250 $this->prescan_keys();
251 }
252 }
253
254 /**
255 * Pre-scan the cache to see which keys are present.
256 */
257 protected function prescan_keys() {
258 foreach (glob($this->glob_keys_pattern(), GLOB_MARK | GLOB_NOSORT) as $filename) {
259 $this->keys[basename($filename)] = filemtime($filename);
260 }
261 }
262
263 /**
264 * Gets a pattern suitable for use with glob to find all keys in the cache.
265 * @return string The pattern.
266 */
267 protected function glob_keys_pattern() {
268 if ($this->singledirectory) {
269 return $this->path . '/*.cache';
270 } else {
702651c7 271 return $this->path . '/*/*.cache';
08aaa637
SH
272 }
273 }
274
275 /**
276 * Returns the file path to use for the given key.
277 *
278 * @param string $key The key to generate a file path for.
279 * @param bool $create If set to the true the directory structure the key requires will be created.
280 * @return string The full path to the file that stores a particular cache key.
281 */
282 protected function file_path_for_key($key, $create = false) {
283 if ($this->singledirectory) {
284 // Its a single directory, easy, just the store instances path + the file name.
285 return $this->path . '/' . $key . '.cache';
286 } else {
702651c7
SH
287 // We are using a single subdirectory to achieve 1 level.
288 $subdir = substr($key, 0, 3);
289 $dir = $this->path . '/' . $subdir;
08aaa637
SH
290 if ($create) {
291 // Create the directory. This function does it recursivily!
292 make_writable_directory($dir);
62704f33 293 }
08aaa637 294 return $dir . '/' . $key . '.cache';
62704f33
SH
295 }
296 }
297
298 /**
299 * Retrieves an item from the cache store given its key.
300 *
301 * @param string $key The key to retrieve
302 * @return mixed The data that was associated with the key, or false if the key did not exist.
303 */
304 public function get($key) {
305 $filename = $key.'.cache';
08aaa637 306 $file = $this->file_path_for_key($key);
62704f33
SH
307 $ttl = $this->definition->get_ttl();
308 if ($ttl) {
309 $maxtime = cache::now() - $ttl;
310 }
311 $readfile = false;
312 if ($this->prescan && array_key_exists($key, $this->keys)) {
313 if (!$ttl || $this->keys[$filename] >= $maxtime && file_exists($file)) {
314 $readfile = true;
315 } else {
316 $this->delete($key);
317 }
318 } else if (file_exists($file) && (!$ttl || filemtime($file) >= $maxtime)) {
319 $readfile = true;
320 }
321 if (!$readfile) {
322 return false;
323 }
170f821b 324 // Check the filesize first, likely not needed but important none the less.
62704f33
SH
325 $filesize = filesize($file);
326 if (!$filesize) {
327 return false;
328 }
329 // Open ensuring the file for writing, truncating it and setting the pointer to the start.
330 if (!$handle = fopen($file, 'rb')) {
331 return false;
332 }
333 // Lock it up!
170f821b 334 // We don't care if this succeeds or not, on some systems it will, on some it won't, meah either way.
62704f33
SH
335 flock($handle, LOCK_SH);
336 // HACK ALERT
337 // There is a problem when reading from the file during PHPUNIT tests. For one reason or another the filesize is not correct
338 // Doesn't happen during normal operation, just during unit tests.
170f821b 339 // Read it.
62704f33 340 $data = fread($handle, $filesize+128);
170f821b 341 // Unlock it.
62704f33
SH
342 flock($handle, LOCK_UN);
343 // Return it unserialised.
344 return $this->prep_data_after_read($data);
345 }
346
347 /**
348 * Retrieves several items from the cache store in a single transaction.
349 *
350 * If not all of the items are available in the cache then the data value for those that are missing will be set to false.
351 *
352 * @param array $keys The array of keys to retrieve
353 * @return array An array of items from the cache. There will be an item for each key, those that were not in the store will
354 * be set to false.
355 */
356 public function get_many($keys) {
357 $result = array();
358 foreach ($keys as $key) {
359 $result[$key] = $this->get($key);
360 }
361 return $result;
362 }
170f821b 363
62704f33
SH
364 /**
365 * Deletes an item from the cache store.
366 *
367 * @param string $key The key to delete.
368 * @return bool Returns true if the operation was a success, false otherwise.
369 */
370 public function delete($key) {
371 $filename = $key.'.cache';
08aaa637 372 $file = $this->file_path_for_key($key);
62704f33
SH
373 $result = @unlink($file);
374 unset($this->keys[$filename]);
375 return $result;
376 }
377
378 /**
379 * Deletes several keys from the cache in a single action.
380 *
381 * @param array $keys The keys to delete
382 * @return int The number of items successfully deleted.
383 */
384 public function delete_many(array $keys) {
385 $count = 0;
386 foreach ($keys as $key) {
387 if ($this->delete($key)) {
388 $count++;
389 }
390 }
391 return $count;
392 }
393
394 /**
395 * Sets an item in the cache given its key and data value.
396 *
397 * @param string $key The key to use.
398 * @param mixed $data The data to set.
399 * @return bool True if the operation was a success false otherwise.
400 */
401 public function set($key, $data) {
402 $this->ensure_path_exists();
403 $filename = $key.'.cache';
08aaa637 404 $file = $this->file_path_for_key($key, true);
62704f33
SH
405 $result = $this->write_file($file, $this->prep_data_before_save($data));
406 if (!$result) {
407 // Couldn't write the file.
408 return false;
409 }
170f821b 410 // Record the key if required.
62704f33
SH
411 if ($this->prescan) {
412 $this->keys[$filename] = cache::now() + 1;
413 }
170f821b 414 // Return true.. it all worked **miracles**.
62704f33
SH
415 return true;
416 }
417
418 /**
419 * Prepares data to be stored in a file.
420 *
421 * @param mixed $data
422 * @return string
423 */
424 protected function prep_data_before_save($data) {
425 return serialize($data);
426 }
427
428 /**
429 * Prepares the data it has been read from the cache. Undoing what was done in prep_data_before_save.
430 *
431 * @param string $data
432 * @return mixed
433 * @throws coding_exception
434 */
435 protected function prep_data_after_read($data) {
436 $result = @unserialize($data);
437 if ($result === false) {
438 throw new coding_exception('Failed to unserialise data from file. Either failed to read, or failed to write.');
439 }
440 return $result;
441 }
442
443 /**
444 * Sets many items in the cache in a single transaction.
445 *
446 * @param array $keyvaluearray An array of key value pairs. Each item in the array will be an associative array with two
447 * keys, 'key' and 'value'.
448 * @return int The number of items successfully set. It is up to the developer to check this matches the number of items
449 * sent ... if they care that is.
450 */
451 public function set_many(array $keyvaluearray) {
452 $count = 0;
453 foreach ($keyvaluearray as $pair) {
454 if ($this->set($pair['key'], $pair['value'])) {
455 $count++;
456 }
457 }
458 return $count;
459 }
460
461 /**
462 * Checks if the store has a record for the given key and returns true if so.
463 *
464 * @param string $key
465 * @return bool
466 */
467 public function has($key) {
468 $filename = $key.'.cache';
62704f33
SH
469 $maxtime = cache::now() - $this->definition->get_ttl();
470 if ($this->prescan) {
471 return array_key_exists($filename, $this->keys) && $this->keys[$filename] >= $maxtime;
472 }
08aaa637 473 $file = $this->file_path_for_key($key);
62704f33
SH
474 return (file_exists($file) && ($this->definition->get_ttl() == 0 || filemtime($file) >= $maxtime));
475 }
476
477 /**
478 * Returns true if the store contains records for all of the given keys.
479 *
480 * @param array $keys
481 * @return bool
482 */
483 public function has_all(array $keys) {
484 foreach ($keys as $key) {
485 if (!$this->has($key)) {
486 return false;
487 }
488 }
489 return true;
490 }
491
492 /**
493 * Returns true if the store contains records for any of the given keys.
494 *
495 * @param array $keys
496 * @return bool
497 */
498 public function has_any(array $keys) {
499 foreach ($keys as $key) {
500 if ($this->has($key)) {
501 return true;
502 }
503 }
504 return false;
505 }
170f821b 506
62704f33
SH
507 /**
508 * Purges the cache deleting all items within it.
509 *
510 * @return boolean True on success. False otherwise.
511 */
512 public function purge() {
08aaa637 513 foreach (glob($this->glob_keys_pattern(), GLOB_MARK | GLOB_NOSORT) as $filename) {
62704f33
SH
514 @unlink($filename);
515 }
516 $this->keys = array();
517 return true;
518 }
519
d4d2f27c
MS
520 /**
521 * Given the data from the add instance form this function creates a configuration array.
522 *
523 * @param stdClass $data
524 * @return array
525 */
526 public static function config_get_configuration_array($data) {
527 $config = array();
528
d4d2f27c
MS
529 if (isset($data->path)) {
530 $config['path'] = $data->path;
531 }
532 if (isset($data->autocreate)) {
533 $config['autocreate'] = $data->autocreate;
534 }
49c497ff
SH
535 if (isset($data->singledirectory)) {
536 $config['singledirectory'] = $data->singledirectory;
537 }
d4d2f27c
MS
538 if (isset($data->prescan)) {
539 $config['prescan'] = $data->prescan;
540 }
541
542 return $config;
543 }
544
62704f33
SH
545 /**
546 * Checks to make sure that the path for the file cache exists.
547 *
548 * @return bool
549 * @throws coding_exception
550 */
551 protected function ensure_path_exists() {
552 if (!is_writable($this->path)) {
553 if ($this->custompath && !$this->autocreate) {
170f821b 554 throw new coding_exception('File store path does not exist. It must exist and be writable by the web server.');
62704f33
SH
555 }
556 if (!make_writable_directory($this->path, false)) {
557 throw new coding_exception('File store path does not exist and can not be created.');
558 }
559 }
560 return true;
561 }
562
563 /**
564 * Returns true if the user can add an instance of the store plugin.
565 *
566 * @return bool
567 */
568 public static function can_add_instance() {
569 return true;
570 }
571
572 /**
573 * Performs any necessary clean up when the store instance is being deleted.
574 *
575 * 1. Purges the cache directory.
576 * 2. Deletes the directory we created for this cache instances data.
577 */
578 public function cleanup() {
579 $this->purge();
580 @rmdir($this->path);
581 }
582
583 /**
584 * Generates an instance of the cache store that can be used for testing.
585 *
586 * Returns an instance of the cache store, or false if one cannot be created.
587 *
588 * @param cache_definition $definition
6fec1820 589 * @return cachestore_file
62704f33
SH
590 */
591 public static function initialise_test_instance(cache_definition $definition) {
592 $name = 'File test';
6fec1820
SH
593 $path = make_cache_directory('cachestore_file_test');
594 $cache = new cachestore_file($name, array('path' => $path));
62704f33
SH
595 $cache->initialise($definition);
596 return $cache;
597 }
598
599 /**
600 * Writes your madness to a file.
601 *
602 * There are several things going on in this function to try to ensure what we don't end up with partial writes etc.
603 * 1. Files for writing are opened with the mode xb, the file must be created and can not already exist.
34c84c72 604 * 2. Renaming, data is written to a temporary file, where it can be verified using md5 and is then renamed.
62704f33
SH
605 *
606 * @param string $file Absolute file path
607 * @param string $content The content to write.
608 * @return bool
609 */
610 protected function write_file($file, $content) {
611 // Generate a temp file that is going to be unique. We'll rename it at the end to the desired file name.
612 // in this way we avoid partial writes.
613 $path = dirname($file);
614 while (true) {
615 $tempfile = $path.'/'.uniqid(sesskey().'.', true) . '.temp';
616 if (!file_exists($tempfile)) {
617 break;
618 }
619 }
620
62704f33
SH
621 // Open the file with mode=x. This acts to create and open the file for writing only.
622 // If the file already exists this will return false.
623 // We also force binary.
624 $handle = @fopen($tempfile, 'xb+');
625 if ($handle === false) {
626 // File already exists... lock already exists, return false.
627 return false;
628 }
62704f33
SH
629 fwrite($handle, $content);
630 fflush($handle);
631 // Close the handle, we're done.
632 fclose($handle);
633
62704f33
SH
634 if (md5_file($tempfile) !== md5($content)) {
635 // The md5 of the content of the file must match the md5 of the content given to be written.
636 @unlink($tempfile);
637 return false;
638 }
639
640 // Finally rename the temp file to the desired file, returning the true|false result.
641 $result = rename($tempfile, $file);
642 if (!$result) {
643 // Failed to rename, don't leave files lying around.
644 @unlink($tempfile);
645 }
646 return $result;
647 }
34c84c72
SH
648
649 /**
650 * Returns the name of this instance.
651 * @return string
652 */
653 public function my_name() {
654 return $this->name;
655 }
62704f33 656}