Passed
Pull Request — master (#1596)
by Michael
09:45
created

XoopsCache::resolveEngineConfig()   A

Complexity

Conditions 5
Paths 2

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 8
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 14
rs 9.6111
1
<?php
2
/**
3
 * Cache engine For XOOPS
4
 *
5
 * You may not change or alter any portion of this comment or credits
6
 * of supporting developers from this source code or any supporting source code
7
 * which is considered copyrighted (c) material of the original comment or credit authors.
8
 * This program is distributed in the hope that it will be useful,
9
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11
 *
12
 * @copyright       (c) 2000-2025 XOOPS Project (https://xoops.org)
13
 * @license             GNU GPL 2 (https://www.gnu.org/licenses/gpl-2.0.html)
14
 * @package             class
15
 * @subpackage          cache
16
 * @since               2.3.0
17
 * @author              Taiwen Jiang <[email protected]>
18
 */
19
20
defined('XOOPS_ROOT_PATH') || exit('Restricted access');
21
22
/**
23
 * Caching for CakePHP.
24
 *
25
 * @package    cake
26
 * @subpackage cake.cake.libs
27
 */
28
class XoopsCache
29
{
30
    /**
31
     * Cache engine instances
32
     *
33
     * @var array<string, XoopsCacheEngine|null>
34
     * @access protected
35
     */
36
    protected $engine = [];
37
38
39
    /**
40
     * Cache configuration stack
41
     *
42
     * @var array
43
     * @access private
44
     */
45
    private $configs = [];
46
47
    /**
48
     * Holds name of the current configuration being used
49
     *
50
     * @var string|null
51
     * @access private
52
     */
53
    private $name;
54
55
    /**
56
     * XoopsCache::__construct()
57
     */
58
    public function __construct() {}
59
60
    /**
61
     * Returns a singleton instance
62
     *
63
     * @return object
64
     * @access public
65
     */
66
    public static function getInstance()
67
    {
68
        static $instance;
69
        if (!isset($instance)) {
70
            $class    = self::class;
71
            $instance = new $class();
72
        }
73
74
        return $instance;
75
    }
76
77
    /**
78
     * Resolves and validates engine configuration
79
     *
80
     * @param string|array|null $config Configuration name or array
81
     * @return array{engine: string, settings: array<string, mixed>}|false
82
     */
83
    protected function resolveEngineConfig($config)
84
    {
85
        $config = $this->config($config);
86
87
        if (
88
            !is_array($config) ||
89
            !isset($config['engine'], $config['settings']) ||
90
            !is_string($config['engine']) ||
91
            !is_array($config['settings'])
92
        ) {
93
            return false;
94
        }
95
96
        return $config;
97
    }
98
99
100
101
    /**
102
     * Tries to find and include a file for a cache engine and returns object instance
103
     *
104
     * @param  string $name Name of the engine
105
     * @return mixed $engine object or null
106
     * @access private
107
     */
108
    private function loadEngine($name)
109
    {
110
        if (!class_exists('XoopsCache' . ucfirst($name))) {
111
            if (file_exists($file = __DIR__ . '/' . strtolower($name) . '.php')) {
112
                include $file;
113
            } else {
114
                trigger_error('File :' . $file . ' not found in file : ' . __FILE__ . ' at line: ' . __LINE__, E_USER_WARNING);
115
116
                return false;
117
            }
118
        }
119
120
        return true;
121
    }
122
123
    /**
124
     * Set the cache configuration to use
125
     *
126
     * @param  string|array $name     Name of the configuration
127
     * @param  array  $settings Optional associative array of settings passed to the engine
128
     * @return array|false  (engine, settings) on success, false on failure
129
     * @access public
130
     */
131
    public function config($name = 'default', $settings = [])
132
    {
133
        $_this = XoopsCache::getInstance();
134
135
        if (is_array($name)) {
136
            $config = $name;
137
138
            if (isset($config['name']) && is_string($config['name'])) {
139
                $name = $config['name'];
140
            }
141
142
            if (isset($config['settings']) && is_array($config['settings'])) {
143
                // Preserve legacy behavior: overwrite settings, do NOT merge
144
                $settings = $config['settings'];
145
            }
146
        }
147
148
        if (!is_string($name) || $name === '') {
149
            $name = 'default';
150
        }
151
152
        if (isset($_this->configs[$name])) {
153
            $settings = array_merge($_this->configs[$name], $settings);
154
        } elseif (!empty($settings)) {
155
            $_this->configs[$name] = $settings;
156
        } elseif (
157
            $_this->configs !== null
158
            && is_string($_this->name)
159
            && $_this->name !== ''
160
            && isset($_this->configs[$_this->name])
161
        ) {
162
            $name     = $_this->name;
163
            $settings = $_this->configs[$_this->name];
164
        } else {
165
            $name = 'default';
166
            if (!empty($_this->configs['default'])) {
167
                $settings = $_this->configs['default'];
168
            } else {
169
                $settings = [
170
                    'engine' => 'file',
171
                ];
172
            }
173
        }
174
175
        $engine = !empty($settings['engine']) ? $settings['engine'] : 'file';
176
177
        if ($name !== $_this->name) {
178
            if ($_this->engine($engine, $settings) === false) {
179
                trigger_error("Cache Engine {$engine} is not set", E_USER_WARNING);
180
181
                return false;
182
            }
183
            $_this->name           = $name;
184
            $_this->configs[$name] = $_this->settings($engine);
185
        }
186
187
        $settings = $_this->configs[$name];
188
189
        return compact('engine', 'settings');
190
    }
191
192
    /**
193
     * Set the cache engine to use or modify settings for one instance
194
     *
195
     * @param  string $name     Name of the engine (without 'Engine')
196
     * @param  array  $settings Optional associative array of settings passed to the engine
197
     * @return boolean True on success, false on failure
198
     * @access public
199
     */
200
    public function engine($name = 'file', $settings = [])
201
    {
202
        if (!$name) {
203
            return false;
204
        }
205
206
        $cacheClass = 'XoopsCache' . ucfirst($name);
207
        $_this      = XoopsCache::getInstance();
208
        if (!isset($_this->engine[$name])) {
209
            if ($_this->loadEngine($name) === false) {
210
                trigger_error("Cache Engine {$name} is not loaded", E_USER_WARNING);
211
212
                return false;
213
            }
214
            $_this->engine[$name] = new $cacheClass();
215
        }
216
217
        if ($_this->engine[$name]->init($settings)) {
218
            if (time() % $_this->engine[$name]->settings['probability'] == 0) {
219
                $_this->engine[$name]->gc();
220
            }
221
222
            return true;
223
        }
224
        $_this->engine[$name] = null;
225
        trigger_error("Cache Engine {$name} is not initialized", E_USER_WARNING);
226
227
        return false;
228
    }
229
230
    /**
231
     * Garbage collection
232
     *
233
     * Permanently remove all expired and deleted data
234
     * @return bool True on success, false on failure
235
     * @access public
236
     */
237
    public function gc(): bool
238
    {
239
        $_this  = XoopsCache::getInstance();
240
        $config = $_this->resolveEngineConfig(null);
241
        if ($config === false) {
242
            return false;
243
        }
244
245
        $engine = $config['engine'];
246
247
        $_this->engine[$engine]->gc();
248
249
        return true;
250
    }
251
252
    /**
253
     * Write data for key into cache
254
     *
255
     * @param  string $key       Identifier for the data
256
     * @param  mixed  $value     Data to be cached - anything except a resource
257
     * @param  mixed  $duration  Optional - string configuration name OR how long to cache the data, either in seconds or a
258
     *                           string that can be parsed by the strtotime() function OR array('config' => 'default', 'duration' => '3600')
259
     * @return bool True if the data was successfully cached, false on failure
260
     * @access public
261
     */
262
    public static function write($key, $value, $duration = null): bool
263
    {
264
        $key    = substr(md5(XOOPS_URL), 0, 8) . '_' . $key;
265
        $_this  = XoopsCache::getInstance();
266
267
        $config = null;
268
269
        if (is_array($duration)) {
270
            if (isset($duration['config']) && is_string($duration['config'])) {
271
                $config = $duration['config'];
272
            }
273
            if (isset($duration['duration'])) {
274
                $duration = $duration['duration'];
275
            } else {
276
                $duration = null;
277
            }
278
        } elseif (
279
            is_string($duration)
280
            && $duration !== ''
281
            && isset($_this->configs[$duration])
282
        ) {
283
            $config   = $duration;
284
            $duration = null;
285
        }
286
287
        $config = $_this->resolveEngineConfig($config);
288
        if ($config === false) {
289
            return false;
290
        }
291
292
        $engine   = $config['engine'];
293
        $settings = $config['settings'];
294
295
        if (!$_this->isInitialized($engine)) {
296
            trigger_error('Cache write not initialized: ' . $engine);
297
298
            return false;
299
        }
300
301
        if (!$key = $_this->key($key)) {
302
            return false;
303
        }
304
305
        if (is_resource($value)) {
306
            return false;
307
        }
308
309
        if (!$duration) {
310
            $duration = $settings['duration'];
311
        }
312
        $duration = is_numeric($duration) ? (int) $duration : strtotime($duration) - time();
313
314
        if ($duration < 1) {
315
            return false;
316
        }
317
        $_this->engine[$engine]->init($settings);
318
        $success = $_this->engine[$engine]->write($key, $value, $duration);
319
320
        return $success;
321
    }
322
323
    /**
324
     * Read a key from the cache
325
     *
326
     * @param  string $key    Identifier for the data
327
     * @param  string|array $config name of the configuration to use
328
     * @return mixed  The cached data, or false if the data doesn't exist, has expired, or if there was an error fetching it
329
     * @access public
330
     */
331
    public static function read($key, $config = null)
332
    {
333
        $key    = substr(md5(XOOPS_URL), 0, 8) . '_' . $key;
334
        $_this  = XoopsCache::getInstance();
335
336
        $config = $_this->resolveEngineConfig($config);
337
        if ($config === false) {
338
            return false;
339
        }
340
341
        $engine   = $config['engine'];
342
        $settings = $config['settings'];
343
344
        if (!$_this->isInitialized($engine)) {
345
            return false;
346
        }
347
        if (!$key = $_this->key($key)) {
348
            return false;
349
        }
350
        $_this->engine[$engine]->init($settings);
351
        $success = $_this->engine[$engine]->read($key);
352
353
        return $success;
354
    }
355
356
    /**
357
     * Delete a key from the cache
358
     *
359
     * @param  string $key    Identifier for the data
360
     * @param string|null $config name of the configuration to use
361
     * @return boolean True if the value was successfully deleted, false if it didn't exist or couldn't be removed
362
     * @access public
363
     */
364
    public static function delete($key, $config = null)
365
    {
366
        $key   = substr(md5(XOOPS_URL), 0, 8) . '_' . $key;
367
        $_this = XoopsCache::getInstance();
368
369
        $config = $_this->resolveEngineConfig($config);
370
        if ($config === false) {
371
            return false;
372
        }
373
374
        $engine   = $config['engine'];
375
        $settings = $config['settings'];
376
377
        if (!$_this->isInitialized($engine)) {
378
            return false;
379
        }
380
381
        if (!$key = $_this->key($key)) {
382
            return false;
383
        }
384
385
        $_this->engine[$engine]->init($settings);
386
        $success = $_this->engine[$engine]->delete($key);
387
388
        return $success;
389
    }
390
391
    /**
392
     * Delete all keys from the cache
393
     *
394
     * @param  boolean $check  if true will check expiration, otherwise delete all
395
     * @param string|null $config name of the configuration to use
396
     * @return boolean True if the cache was successfully cleared, false otherwise
397
     * @access public
398
     */
399
    public function clear($check = false, $config = null)
400
    {
401
        $_this = XoopsCache::getInstance();
402
403
        $config = $_this->resolveEngineConfig($config);
404
        if ($config === false) {
405
            return false;
406
        }
407
408
        $engine   = $config['engine'];
409
        $settings = $config['settings'];
410
411
        if (!$_this->isInitialized($engine)) {
412
            return false;
413
        }
414
415
        $success = $_this->engine[$engine]->clear($check);
416
        $_this->engine[$engine]->init($settings);
417
418
        return $success;
419
    }
420
421
422
    /**
423
     * Check if Cache has initialized a working storage engine
424
     *
425
     * @param  string|null $engine Name of the engine
426
     * @return bool
427
     * @internal param string $configs Name of the configuration setting
428
     * @access   public
429
     */
430
    public function isInitialized($engine = null): bool
431
    {
432
        $_this = XoopsCache::getInstance();
433
        if (!$engine && isset($_this->configs[$_this->name]['engine'])) {
434
            $engine = $_this->configs[$_this->name]['engine'];
435
        }
436
437
        return isset($_this->engine[$engine]);
438
    }
439
440
    /**
441
     * Return the settings for current cache engine
442
     *
443
     * @param  string|null $engine Name of the engine
444
     * @return array  list of settings for this engine
445
     * @access public
446
     */
447
    public function settings($engine = null): array
448
    {
449
        $_this = XoopsCache::getInstance();
450
        if (!$engine && isset($_this->configs[$_this->name]['engine'])) {
451
            $engine = $_this->configs[$_this->name]['engine'];
452
        }
453
        if (isset($_this->engine[$engine]) && null !== $_this->engine[$engine]) {
454
            return $_this->engine[$engine]->settings();
455
        }
456
457
        return [];
458
    }
459
460
    /**
461
     * generates a safe key
462
     *
463
     * @param  string $key the key passed over
464
     * @return mixed  string $key or false
465
     * @access private
466
     */
467
    public function key($key)
468
    {
469
        if (empty($key)) {
470
            return false;
471
        }
472
        $key = str_replace(['/', '.'], '_', (string) $key);
473
474
        return $key;
475
    }
476
}
477
478
/**
479
 * Abstract class for storage engine for caching
480
 *
481
 * @package    core
482
 * @subpackage cache
483
 */
484
class XoopsCacheEngine
485
{
486
    /**
487
     * settings of current engine instance
488
     *
489
     * @var array
490
     * @access public
491
     */
492
    public $settings;
493
494
    /**
495
     * Initialize the cache engine
496
     *
497
     * Called automatically by the cache frontend
498
     *
499
     * @param  array $settings Associative array of parameters for the engine
500
     * @return boolean True if the engine has been successfully initialized, false if not
501
     * @access   public
502
     */
503
    public function init($settings = [])
504
    {
505
        $this->settings = array_merge(
506
            [
507
                'duration'    => 31556926,
508
                'probability' => 100,
509
            ],
510
            $settings,
511
        );
512
513
        return true;
514
    }
515
516
    /**
517
     * Garbage collection
518
     *
519
     * Permanently remove all expired and deleted data
520
     *
521
     * @access public
522
     */
523
    public function gc() {}
524
525
    /**
526
     * Write value for a key into cache
527
     *
528
     * @param  string $key      Identifier for the data
529
     * @param  mixed  $value    Data to be cached
530
     * @param  mixed  $duration How long to cache the data, in seconds
531
     * @return boolean True if the data was successfully cached, false on failure
532
     * @access public
533
     */
534
    public function write($key, $value, $duration = null)
535
    {
536
        trigger_error(sprintf(__('Method write() not implemented in %s', true), get_class($this)), E_USER_ERROR);
0 ignored issues
show
Bug introduced by
The function __ was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

536
        trigger_error(sprintf(/** @scrutinizer ignore-call */ __('Method write() not implemented in %s', true), get_class($this)), E_USER_ERROR);
Loading history...
537
    }
538
539
    /**
540
     * Read a key from the cache
541
     *
542
     * @param  string $key Identifier for the data
543
     * @return mixed  The cached data, or false if the data doesn't exist, has expired, or if there was an error fetching it
544
     * @access public
545
     */
546
    public function read($key)
547
    {
548
        trigger_error(sprintf(__('Method read() not implemented in %s', true), get_class($this)), E_USER_ERROR);
0 ignored issues
show
Bug introduced by
The function __ was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

548
        trigger_error(sprintf(/** @scrutinizer ignore-call */ __('Method read() not implemented in %s', true), get_class($this)), E_USER_ERROR);
Loading history...
549
    }
550
551
    /**
552
     * Delete a key from the cache
553
     *
554
     * @param  string $key Identifier for the data
555
     * @return boolean True if the value was successfully deleted, false if it didn't exist or couldn't be removed
556
     * @access public
557
     */
558
    public function delete($key) {}
559
560
    /**
561
     * Delete all keys from the cache
562
     *
563
     * @param  boolean $check if true will check expiration, otherwise delete all
564
     * @return boolean True if the cache was successfully cleared, false otherwise
565
     * @access public
566
     */
567
    public function clear($check) {}
568
569
    /**
570
     * Cache Engine settings
571
     *
572
     * @return array settings
573
     * @access public
574
     */
575
    public function settings()
576
    {
577
        return $this->settings;
578
    }
579
}
580