Issues (1686)

sources/ElkArte/Cache/Cache.php (8 issues)

1
<?php
2
3
/**
4
 * This file contains functions that deal with getting and setting cache values.
5
 *
6
 * @package   ElkArte Forum
7
 * @copyright ElkArte Forum contributors
8
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
9
 *
10
 * This file contains code covered by:
11
 * copyright: 2011 Simple Machines (http://www.simplemachines.org)
12
 *
13
 * @version 2.0 dev
14
 *
15
 */
16
17
namespace ElkArte\Cache;
18
19
use ElkArte\Cache\CacheMethod\AbstractCacheMethod;
20
use ElkArte\Debug;
21
use ElkArte\Helper\FileFunctions;
22
use ElkArte\Helper\Util;
23
24
/**
25
 * Class Cache - Methods that deal with getting and setting cache values.
26
 */
27
class Cache
28
{
29
	/** @var object Holds our static instance of the class */
30
	protected static $_instance;
31
32
	/** @var array Array of options for the methods (if needed) */
33
	protected $_options = [];
34
35
	/** @var bool If the cache is enabled or not. */
36
	protected $enabled = false;
37
38
	/** @var int The caching level */
39
	protected $level = 0;
40
41
	/** @var string The prefix to append to the cache key */
42
	protected $_key_prefix;
43
44
	/** @var string The accelerator in use */
45
	protected $_accelerator;
46
47
	/** @var string[] Cached keys */
48
	protected $_cached_keys = [];
49
50
	/** @var AbstractCacheMethod|null The caching engine object */
51
	protected $_cache_obj;
52
53
	/**
54
	 * Initialize the class, defines the options and the caching method to use
55
	 *
56
	 * @param int $level The level of caching
57
	 * @param string $accelerator The accelerator used
58
	 * @param array $options Any setting necessary to the caching engine
59
	 */
60
	public function __construct($level, $accelerator, $options)
61
	{
62
		$this->_options = $options;
63
64
		// Default to file based, so we can slow everything down :P
65
		if (empty($accelerator))
66
		{
67
			$accelerator = 'filebased';
68
		}
69
		$this->_accelerator = ucfirst($accelerator);
70
71
		$this->setLevel($level);
72
		if ($level > 0)
73
		{
74
			$this->enable(true);
75
			$this->level = $level;
76
		}
77
	}
78
79
	/**
80
	 * Enable or disable caching
81
	 *
82
	 * @param bool $enable
83
	 *
84
	 * @return $this
85
	 */
86
	public function enable($enable)
87
	{
88
		// Enable it if we can
89
		if (!$this->enabled && $this->_cache_obj === null)
90 1
		{
91
			$this->_init();
92 1
		}
93
94
		$this->enabled = (bool) $enable;
95 1
96
		return $this;
97 1
	}
98
99
	/**
100 1
	 * Initialize a cache class and call its initialization method
101
	 */
102 1
	protected function _init()
103
	{
104
		$cache_class = '\\ElkArte\\Cache\\CacheMethod\\' . $this->_accelerator;
105
106
		if (class_exists($cache_class))
107
		{
108
			$this->_cache_obj = new $cache_class($this->_options);
109 1
			$this->enabled = $this->_cache_obj->isAvailable();
110
		}
111 1
		else
112
		{
113
			$this->_cache_obj = null;
114
			$this->enabled = false;
115
		}
116
117
		$this->_build_prefix();
118
	}
119
120
	/**
121
	 * Set $_key_prefix to a "unique" value based on timestamp of a file
122
	 */
123
	protected function _build_prefix()
124
	{
125
		global $boardurl;
126
127 3
		if (!FileFunctions::instance()->fileExists(CACHEDIR . '/index.php'))
128
		{
129
			touch(CACHEDIR . '/index.php');
130 3
		}
131
132 1
		$this->_key_prefix = md5($boardurl . filemtime(CACHEDIR . '/index.php')) . '-ELK-';
133
	}
134
135 3
	/**
136
	 * Check if caching is enabled
137 3
	 *
138
	 * @return bool
139
	 */
140
	public function isEnabled()
141
	{
142
		return $this->enabled;
143 1
	}
144
145 1
	/**
146
	 * Return the current cache_obj
147 1
	 *
148
	 * @return AbstractCacheMethod|null
149
	 */
150
	public function getCacheEngine()
151
	{
152
		return $this->_cache_obj;
153
	}
154 1
155
	/**
156 1
	 * Return the cache accelerator in use
157
	 *
158
	 * @return string
159 1
	 */
160 1
	public function getAccelerator()
161
	{
162
		return $this->_accelerator;
163
	}
164
165 1
	/**
166
	 * Find and return the instance of the Cache class if it exists,
167 1
	 * otherwise start a new instance
168
	 */
169 1
	public static function instance()
170
	{
171
		if (self::$_instance === null)
172
		{
173 1
			global $cache_accelerator, $cache_enable, $cache_uid, $cache_password, $cache_servers;
174 1
175
			$options = [
176
				'servers' => empty($cache_servers) ? [] : explode(',', $cache_servers),
177
				'cache_uid' => empty($cache_uid) ? '' : $cache_uid,
178
				'cache_password' => empty($cache_password) ? '' : $cache_password,
179
			];
180
181 277
			self::$_instance = new Cache($cache_enable, $cache_accelerator, $options);
182
		}
183 277
184
		return self::$_instance;
185
	}
186
187
	/**
188
	 * Just before forgetting about the cache, let's save the existing keys.
189
	 */
190 277
	public function __destruct()
191
	{
192 277
		$cached = $this->get('_cached_keys');
193
		if (!is_array($cached))
194 1
		{
195
			$cached = [];
196 1
		}
197 1
198
		$_cached_keys = array_unique(array_merge($this->_cached_keys, $cached));
199
		$this->put('_cached_keys', $_cached_keys);
200
	}
201
202
	/**
203
	 * Gets the value from the cache specified by key, so long as it is not older than ttl seconds.
204 1
	 *
205
	 * - It may often "miss", so shouldn't be depended on.
206
	 * - It supports the same as \ElkArte\Cache\Cache::put().
207 277
	 *
208
	 * @param string $key
209
	 * @param int $ttl = 120
210
	 *
211
	 * @return null|mixed if it was a hit
212
	 */
213
	public function get($key, $ttl = 120)
214
	{
215
		global $db_show_debug;
216
217
		if (!$this->isEnabled())
218
		{
219
			return null;
220
		}
221
222
		if ($db_show_debug === true)
223
		{
224
			$cache_hit = [
225
				'k' => $key,
226
				'd' => 'get'
227
			];
228
			$st = microtime(true);
229
		}
230
231
		$key = $this->_key($key);
232
		$value = $this->_cache_obj->get($key, $ttl);
0 ignored issues
show
The method get() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

232
		/** @scrutinizer ignore-call */ 
233
  $value = $this->_cache_obj->get($key, $ttl);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
233
234
		if ($db_show_debug === true)
235
		{
236 259
			$cache_hit['t'] = microtime(true) - $st;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $st does not seem to be defined for all execution paths leading up to this point.
Loading history...
237
			$cache_hit['s'] = isset($value) ? strlen($value) : 0;
0 ignored issues
show
$value of type array is incompatible with the type string expected by parameter $string of strlen(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

237
			$cache_hit['s'] = isset($value) ? strlen(/** @scrutinizer ignore-type */ $value) : 0;
Loading history...
238 259
			Debug::instance()->cache($cache_hit);
239
		}
240 259
241
		call_integration_hook('cache_get_data', [$key, $ttl, $value]);
242 257
243
		return empty($value) ? null : Util::unserialize($value);
0 ignored issues
show
$value of type array is incompatible with the type string expected by parameter $string of ElkArte\Helper\Util::unserialize(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

243
		return empty($value) ? null : Util::unserialize(/** @scrutinizer ignore-type */ $value);
Loading history...
244
	}
245 2
246
	/**
247
	 * Get the key for the cache.
248
	 *
249
	 * @param string $key
250
	 *
251
	 * @return string
252
	 */
253
	protected function _key($key)
254 2
	{
255 2
		return $this->_key_prefix . $this->_cache_obj->fixkey($key);
256
	}
257 2
258
	/**
259
	 * Puts value in the cache under key for ttl seconds.
260
	 *
261
	 * - It may "miss" so shouldn't be depended on
262
	 * - Uses the cache engine chosen in the ACP and saved in settings.php
263
	 * - It supports:
264 2
	 *   - Memcache: https://www.php.net/memcache
265
	 *   - MemcacheD: https://www.php.net/memcached
266 2
	 *   - APCu: https://us3.php.net/manual/en/book.apcu.php
267
	 *   - Zend: https://help.zend.com/zend/current/content/data_cache_component.htm
268
	 *   - Redis: https://redis.io/learn/develop/php
269
	 *
270
	 * @param string $key
271
	 * @param string|int|array|null $value
272
	 * @param int $ttl = 120
273
	 */
274
	public function put($key, $value, $ttl = 120)
275
	{
276 2
		global $db_show_debug;
277
278 2
		if (!$this->isEnabled())
279
		{
280
			return;
281
		}
282
283
		// If we are showing debug information we have some data to collect
284
		if ($db_show_debug === true)
285
		{
286
			$cache_hit = [
287
				'k' => $key,
288
				'd' => 'put',
289
				's' => $value === null ? 0 : strlen(serialize($value))
290
			];
291
			$st = microtime(true);
292
		}
293
294
		$this->_cached_keys[] = $key;
295
		$key = $this->_key($key);
296
		$value = $value === null ? null : serialize($value);
297
298 261
		$this->_cache_obj->put($key, $value, $ttl);
299
300 261
		call_integration_hook('cache_put_data', [$key, $value, $ttl]);
301
302 261
		// Show the debug cache hit information
303
		if ($db_show_debug === true)
304 259
		{
305
			$cache_hit['t'] = microtime(true) - $st;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $st does not seem to be defined for all execution paths leading up to this point.
Loading history...
306
			Debug::instance()->cache($cache_hit);
307
		}
308 2
	}
309
310
	/**
311
	 * Try to retrieve a cache entry. On failure, call the appropriate function.
312
	 * This callback is sent as $file to include, and $function to call, with
313
	 * $params parameters.
314
	 *
315
	 * @param string $key cache entry key
316
	 * @param string $file file to include
317
	 * @param string $function function to call
318 2
	 * @param array $params parameters sent to the function
319 2
	 * @param int $level = 1
320 2
	 *
321
	 * @return array
322 2
	 */
323
	public function quick_get($key, $file, $function, $params, $level = 1)
324 2
	{
325
		call_integration_hook('pre_cache_quick_get', [&$key, &$file, &$function, &$params, &$level]);
326
327 2
		/* Refresh the cache if either:
328
			1. Caching is disabled.
329
			2. The cache level isn't high enough.
330
			3. The item has not been cached or the cached item expired.
331
			4. The cached item has a custom expiration condition evaluating to true.
332 2
			5. The expire time set in the cache item has passed (needed for Zend).
333
		*/
334
		$cache_block = $this->get($key, 3600);
335
		if ($this->level < $level
336
			|| !is_array($cache_block)
337
			|| !$this->isEnabled()
338
			|| (!empty($cache_block['refresh_eval']) && eval($cache_block['refresh_eval']))
0 ignored issues
show
The use of eval() is discouraged.
Loading history...
339
			|| (!empty($cache_block['expires']) && $cache_block['expires'] < time()))
340
		{
341
			require_once(SOURCEDIR . '/' . $file);
342
			$cache_block = call_user_func_array($function, $params);
343
344
			if ($this->level >= $level)
345
			{
346
				$this->put($key, $cache_block, $cache_block['expires'] - time());
347
			}
348
		}
349
350
		// Some cached data may need a freshening up after retrieval.
351
		if (!empty($cache_block['post_retri_eval']))
352
		{
353
			eval($cache_block['post_retri_eval']);
0 ignored issues
show
The use of eval() is discouraged.
Loading history...
354
		}
355
356
		call_integration_hook('post_cache_quick_get', [$cache_block]);
357
358
		return $cache_block['data'];
359
	}
360
361
	/**
362
	 * Same as $this->get but sets $var to the result and return if it was a hit
363
	 *
364
	 * @param mixed $var The variable to be assigned the result
365
	 * @param string $key
366
	 * @param int $ttl
367
	 *
368
	 * @return null|bool if it was a hit
369
	 */
370
	public function getVar(&$var, $key, $ttl = 120)
371
	{
372
		$var = $this->get($key, $ttl);
373
374
		return !$this->isMiss();
375
	}
376
377
	/**
378
	 * @return bool If the result of the last get was a miss
379
	 */
380
	public function isMiss()
381
	{
382
		return $this->isEnabled() ? $this->_cache_obj->isMiss() : true;
383
	}
384
385
	/**
386
	 * Empty out the cache in use as best it can
387
	 *
388
	 * It may only remove the files of a certain type (if the $type parameter is given)
389
	 * Type can be user, data or left blank
390 259
	 *  - user clears out user data
391
	 *  - data clears out system / opcode data
392 259
	 *  - If no type is specified will perform a complete cache clearing
393
	 * For cache engines that do not distinguish on types, a full cache flush will be done
394 259
	 *
395
	 * @param string $type = ''
396
	 */
397
	public function clean($type = '')
398
	{
399
		if (!$this->isEnabled())
400
		{
401 259
			return;
402
		}
403 259
404
		$this->_cache_obj->clean($type);
405
406
		// Invalidate cache, to be sure!
407
		// ... as long as CACHEDIR/index.php can be modified, anyway.
408
		@touch(CACHEDIR . '/index.php');
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for touch(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

408
		/** @scrutinizer ignore-unhandled */ @touch(CACHEDIR . '/index.php');

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
409
410
		// Give addons a way to trigger cache cleaning.
411
		call_integration_hook('integrate_clean_cache');
412
413
		clearstatcache();
414
	}
415
416
	/**
417
	 * Retrieves the current level.
418 2
	 *
419
	 * @return int The current level.
420 2
	 */
421
	public function getLevel()
422 2
	{
423
		return $this->level;
424
	}
425
426
	/**
427
	 * Set the caching level. Setting it to <= 0 disables caching
428
	 *
429
	 * @param int $level
430
	 *
431
	 * @return $this
432
	 */
433
	public function setLevel($level)
434
	{
435
		$this->level = (int) $level;
436
437
		if ($this->level <= 0)
438
		{
439
			$this->enable(false);
440 2
		}
441
442 2
		return $this;
443
	}
444
445
	/**
446
	 * Checks if the system level is set to a value strictly higher than the
447
	 * required level of the cache request.
448
	 *
449
	 * @param int $level
450
	 *
451
	 * @return bool
452 3
	 */
453
	public function levelHigherThan($level)
454 3
	{
455
		return $this->isEnabled() && $this->level > $level;
456 3
	}
457
458 1
	/**
459
	 * Checks if the system level is set to a value strictly lower than the
460
	 * required level of the cache request.
461 3
	 * Returns true also if the cache is disabled (it's lower than any level).
462
	 *
463
	 * @param int $level
464
	 *
465
	 * @return bool
466
	 */
467
	public function levelLowerThan($level)
468
	{
469
		if (!$this->isEnabled())
470
		{
471
			return true;
472 251
		}
473
474 251
		return $this->level < $level;
475
	}
476
477
	/**
478
	 * @param $key
479
	 */
480
	public function remove($key)
481
	{
482
		if (!$this->isEnabled())
483
		{
484
			return;
485
		}
486 6
487
		$key = $this->_key($key);
488 6
		$this->_cache_obj->remove($key);
489
	}
490
491
	/**
492
	 * Removes one or multiple keys from the cache.
493
	 *
494 51
	 * Supports the preg_match syntax.
495
	 *
496 51
	 * @param string|string[] $keys_match The regulat expression/s to match
497
	 *                        the key to remove from the cache.
498 51
	 * @param string $delimiter The delimiter used by preg_match.
499
	 * @param string $modifiers Any modifier required by the regexp.
500
	 */
501
	public function removeKeys($keys_match, $delimiter = '~', $modifiers = '')
502
	{
503
		if (!$this->isEnabled())
504
		{
505
			return;
506
		}
507
508
		$to_remove = (array) $keys_match;
509
		$pattern = $delimiter . implode('|', $to_remove) . $delimiter . $modifiers;
510
511
		foreach ($this->_cached_keys as $cached_key)
512
		{
513
			if (preg_match($pattern, $cached_key) === 1)
514
			{
515
				$this->_cache_obj->remove($cached_key);
516
			}
517
		}
518
	}
519
}
520