Passed
Pull Request — master (#1596)
by Michael
09:08
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
    private 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
     * Resolve engine name from parameter or current configuration
101
     *
102
     * @param string|null $engine Engine name or null to use current
103
     * @return string|null Engine name or null if cannot be resolved
104
     */
105
    private function resolveEngineName($engine)
106
    {
107
        if ($engine !== null && is_string($engine) && $engine !== '') {
108
            return $engine;
109
        }
110
111
        if (
112
            is_string($this->name) &&
113
            $this->name !== '' &&
114
            isset($this->configs[$this->name]['engine'])
115
        ) {
116
            return $this->configs[$this->name]['engine'];
117
        }
118
119
        return null;
120
    }
121
122
    /**
123
     * Tries to find and include a file for a cache engine
124
     *
125
     * @param  string $name Name of the engine
126
     * @return bool True if the engine was successfully loaded, false on failure
127
     * @access private
128
     */
129
    private function loadEngine($name)
130
    {
131
        if (!class_exists('XoopsCache' . ucfirst($name))) {
132
            if (file_exists($file = __DIR__ . '/' . strtolower($name) . '.php')) {
133
                include $file;
134
            } else {
135
                trigger_error('File :' . $file . ' not found in file : ' . __FILE__ . ' at line: ' . __LINE__, E_USER_WARNING);
136
                return false;
137
            }
138
        }
139
140
        return true;
141
    }
142
143
    /**
144
     * Set the cache configuration to use
145
     *
146
     * @param  string|array $name     Name of the configuration
147
     * @param  array  $settings Optional associative array of settings passed to the engine
148
     * @return array|false  (engine, settings) on success, false on failure
149
     * @access public
150
     */
151
    public function config($name = 'default', $settings = [])
152
    {
153
        $_this = XoopsCache::getInstance();
154
155
        if (!is_array($settings)) {
0 ignored issues
show
introduced by
The condition is_array($settings) is always true.
Loading history...
156
            $settings = [];
157
        }
158
159
        if (is_array($name)) {
160
            $config = $name;
161
162
            if (isset($config['name']) && is_string($config['name'])) {
163
                $name = $config['name'];
164
            } else {
165
                $name = 'default';
166
            }
167
168
            if (isset($config['settings']) && is_array($config['settings'])) {
169
                $settings = $config['settings'];
170
            }
171
        }
172
173
        if (!is_string($name) || $name === '') {
0 ignored issues
show
introduced by
The condition is_string($name) is always true.
Loading history...
174
            $name = 'default';
175
        }
176
177
        if (isset($_this->configs[$name])) {
178
            $settings = array_merge($_this->configs[$name], $settings);
179
        } elseif (!empty($settings)) {
180
            $_this->configs[$name] = $settings;
181
        } elseif (
182
            $_this->configs !== null
183
            && is_string($_this->name)
184
            && $_this->name !== ''
185
            && isset($_this->configs[$_this->name])
186
        ) {
187
            $name     = $_this->name;
188
            $settings = $_this->configs[$_this->name];
189
        } else {
190
            $name = 'default';
191
            if (!empty($_this->configs['default'])) {
192
                $settings = $_this->configs['default'];
193
            } else {
194
                $settings = [
195
                    'engine' => 'file',
196
                ];
197
            }
198
        }
199
200
        $engine = !empty($settings['engine']) ? $settings['engine'] : 'file';
201
202
        if ($name !== $_this->name) {
203
            if ($_this->engine($engine, $settings) === false) {
204
                trigger_error("Cache Engine {$engine} is not set", E_USER_WARNING);
205
206
                return false;
207
            }
208
            $_this->name           = $name;
209
            $_this->configs[$name] = $_this->settings($engine);
210
        }
211
212
        $settings = $_this->configs[$name];
213
214
        return compact('engine', 'settings');
215
    }
216
217
    /**
218
     * Set the cache engine to use or modify settings for one instance
219
     *
220
     * @param  string $name     Name of the engine (without 'Engine')
221
     * @param  array  $settings Optional associative array of settings passed to the engine
222
     * @return bool True on success, false on failure
223
     * @access public
224
     */
225
    public function engine($name = 'file', $settings = []): bool
226
    {
227
        if (!is_string($name) || $name === '') {
0 ignored issues
show
introduced by
The condition is_string($name) is always true.
Loading history...
228
            return false;
229
        }
230
        if (!is_array($settings)) {
0 ignored issues
show
introduced by
The condition is_array($settings) is always true.
Loading history...
231
            $settings = [];
232
        }
233
234
        $cacheClass = 'XoopsCache' . ucfirst($name);
235
        $_this      = XoopsCache::getInstance();
236
237
        if (!isset($_this->engine[$name])) {
238
            if ($_this->loadEngine($name) === false) {
239
                trigger_error("Cache Engine {$name} is not loaded", E_USER_WARNING);
240
                return false;
241
            }
242
243
            $_this->engine[$name] = new $cacheClass();
244
245
            // Validate that the engine implements the required interface
246
            if (!$_this->engine[$name] instanceof XoopsCacheEngineInterface) {
247
                $_this->engine[$name] = null;
248
                trigger_error(
249
                    "Cache engine {$name} must implement XoopsCacheEngineInterface",
250
                    E_USER_WARNING
251
                );
252
                return false;
253
            }
254
        }
255
256
        if ($_this->engine[$name]->init($settings)) {
257
            // Safely check probability before using it
258
            $probability = $_this->engine[$name]->settings['probability'] ?? 0;
259
            if ($probability > 0 && time() % $probability === 0) {
260
                $_this->engine[$name]->gc();
261
            }
262
            return true;
263
        }
264
265
        $_this->engine[$name] = null;
266
        trigger_error("Cache Engine {$name} is not initialized", E_USER_WARNING);
267
268
        return false;
269
    }
270
271
    /**
272
     * Garbage collection
273
     *
274
     * Permanently remove all expired and deleted data
275
     *
276
     * @return bool True on success, false on failure
277
     * @access public
278
     */
279
    public function gc(): bool
280
    {
281
        $_this  = XoopsCache::getInstance();
282
        $config = $_this->resolveEngineConfig(null);
283
284
        if ($config === false) {
285
            return false;
286
        }
287
288
        $engine = $config['engine'];
289
290
// Engine configuration is valid, but runtime initialization may have failed
291
        if (!$_this->isInitialized($engine)) {
292
            trigger_error("Cache engine {$engine} not initialized for garbage collection", E_USER_WARNING);
293
            return false;
294
        }
295
296
        $_this->engine[$engine]->gc();
297
298
        return true;
299
    }
300
301
    /**
302
     * Write data for key into cache
303
     *
304
     * @param  string $key       Identifier for the data
305
     * @param  mixed  $value     Data to be cached - anything except a resource
306
     * @param  mixed  $duration  Optional - string configuration name OR how long to cache the data, either in seconds or a
307
     *                           string that can be parsed by the strtotime() function OR array('config' => 'default', 'duration' => '3600')
308
     * @return bool True if the data was successfully cached, false on failure
309
     * @access public
310
     */
311
    public static function write($key, $value, $duration = null): bool
312
    {
313
        $key    = substr(md5(XOOPS_URL), 0, 8) . '_' . $key;
314
        $_this  = XoopsCache::getInstance();
315
316
        $config = null;
317
318
        if (is_array($duration)) {
319
            if (isset($duration['config']) && is_string($duration['config'])) {
320
                $config = $duration['config'];
321
            }
322
            if (isset($duration['duration'])) {
323
                $duration = $duration['duration'];
324
            } else {
325
                $duration = null;
326
            }
327
        } elseif (
328
            is_string($duration)
329
            && $duration !== ''
330
            && isset($_this->configs[$duration])
331
        ) {
332
            $config   = $duration;
333
            $duration = null;
334
        }
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
            trigger_error('Cache write not initialized: ' . $engine);
346
347
            return false;
348
        }
349
350
        if (!$key = $_this->key($key)) {
351
            return false;
352
        }
353
354
        if (is_resource($value)) {
355
            return false;
356
        }
357
358
        if (!$duration) {
359
            $duration = $settings['duration'];
360
        }
361
        $duration = is_numeric($duration) ? (int) $duration : strtotime($duration) - time();
362
363
        if ($duration < 1) {
364
            return false;
365
        }
366
        $_this->engine[$engine]->init($settings);
367
        $success = $_this->engine[$engine]->write($key, $value, $duration);
368
369
        return $success;
370
    }
371
372
    /**
373
     * Read a key from the cache
374
     *
375
     * @param  string $key    Identifier for the data
376
     * @param  string|array|null $config name of the configuration to use
377
     * @return mixed The cached data, or false if:
378
     *               - Configuration is invalid
379
     *               - Data doesn't exist
380
     *               - Data has expired
381
     *               - Error occurred during retrieval
382
     * @access public
383
     */
384
    public static function read($key, $config = null)
385
    {
386
        $key    = substr(md5(XOOPS_URL), 0, 8) . '_' . $key;
387
        $_this  = XoopsCache::getInstance();
388
389
        $config = $_this->resolveEngineConfig($config);
390
        if ($config === false) {
391
            return false;
392
        }
393
394
        $engine   = $config['engine'];
395
        $settings = $config['settings'];
396
397
        if (!$_this->isInitialized($engine)) {
398
            return false;
399
        }
400
        if (!$key = $_this->key($key)) {
401
            return false;
402
        }
403
        $_this->engine[$engine]->init($settings);
404
        $success = $_this->engine[$engine]->read($key);
405
406
        return $success;
407
    }
408
409
    /**
410
     * Delete a key from the cache
411
     *
412
     * @param  string $key    Identifier for the data
413
     * @param string|null $config name of the configuration to use
414
     * @return boolean True if the value was successfully deleted, false if it didn't exist or couldn't be removed
415
     * @access public
416
     */
417
    public static function delete($key, $config = null): bool
418
    {
419
        $key   = substr(md5(XOOPS_URL), 0, 8) . '_' . $key;
420
        $_this = XoopsCache::getInstance();
421
422
        $config = $_this->resolveEngineConfig($config);
423
        if ($config === false) {
424
            return false;
425
        }
426
427
        $engine   = $config['engine'];
428
        $settings = $config['settings'];
429
430
        if (!$_this->isInitialized($engine)) {
431
            return false;
432
        }
433
434
        if (!$key = $_this->key($key)) {
435
            return false;
436
        }
437
438
        $_this->engine[$engine]->init($settings);
439
        $success = $_this->engine[$engine]->delete($key);
440
441
        return $success;
442
    }
443
444
    /**
445
     * Delete all keys from the cache
446
     *
447
     * @param  boolean $check  if true will check expiration, otherwise delete all
448
     * @param string|null $config name of the configuration to use
449
     * @return boolean True if the cache was successfully cleared, false otherwise
450
     * @access public
451
     */
452
    public function clear($check = false, $config = null): bool
453
    {
454
        $_this = XoopsCache::getInstance();
455
456
        $config = $_this->resolveEngineConfig($config);
457
        if ($config === false) {
458
            return false;
459
        }
460
461
        $engine   = $config['engine'];
462
        $settings = $config['settings'];
463
464
        if (!$_this->isInitialized($engine)) {
465
            return false;
466
        }
467
468
        $success = $_this->engine[$engine]->clear($check);
469
        $_this->engine[$engine]->init($settings);
470
471
        return $success;
472
    }
473
474
475
    /**
476
     * Check if Cache has initialized a working storage engine
477
     *
478
     * @param  string|null $engine Name of the engine
479
     * @return bool
480
     * @access public
481
     */
482
    public function isInitialized($engine = null)
483
    {
484
        $_this = XoopsCache::getInstance();
485
        $engine = $_this->resolveEngineName($engine);
486
487
        return is_string($engine)
488
            && isset($_this->engine[$engine])
489
            && $_this->engine[$engine] instanceof XoopsCacheEngineInterface;
490
    }
491
492
    /**
493
     * Return the settings for current cache engine
494
     *
495
     * @param  string|null $engine Name of the engine
496
     * @return array<string, mixed> list of settings for this engine
497
     * @access public
498
     */
499
    public function settings($engine = null): array
500
    {
501
        $_this = XoopsCache::getInstance();
502
        $engine = $_this->resolveEngineName($engine);
503
504
        if (
505
            is_string($engine) &&
506
            isset($_this->engine[$engine]) &&
507
            $_this->engine[$engine] instanceof XoopsCacheEngineInterface
508
        ) {
509
            return $_this->engine[$engine]->settings();
510
        }
511
512
        return [];
513
    }
514
515
    /**
516
     * generates a safe key
517
     *
518
     * @param  string $key the key passed over
519
     * @return mixed  string $key or false
520
     * @access private
521
     */
522
    public function key($key)
523
    {
524
        if (empty($key)) {
525
            return false;
526
        }
527
        $key = str_replace(['/', '.'], '_', (string) $key);
528
529
        return $key;
530
    }
531
}
532
533
/**
534
 * Interface for XOOPS cache engine implementations
535
 *
536
 * Defines the contract that all cache engines must implement.
537
 * Third-party cache engines should implement this interface.
538
 *
539
 * @package    core
540
 * @subpackage cache
541
 * @since      2.5.12
542
 * @deprecated 4.0.0 Use Xoops\Core\Cache\EngineInterface instead
543
 */
544
interface XoopsCacheEngineInterface
545
{
546
    public function init($settings = []);
547
    public function gc();
548
    public function write($key, $value, $duration = null);
549
    public function read($key);
550
    public function delete($key);
551
    public function clear($check);
552
    public function settings();
553
}
554
555
/**
556
 * Abstract class for storage engine for caching
557
 *
558
 * @package    core
559
 * @subpackage cache
560
 */
561
abstract class XoopsCacheEngine implements XoopsCacheEngineInterface
0 ignored issues
show
Deprecated Code introduced by
The interface XoopsCacheEngineInterface has been deprecated: 4.0.0 Use Xoops\Core\Cache\EngineInterface instead ( Ignorable by Annotation )

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

561
abstract class XoopsCacheEngine implements /** @scrutinizer ignore-deprecated */ XoopsCacheEngineInterface

This interface has been deprecated. The supplier of the interface has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the interface will be removed and what other interface to use instead.

Loading history...
562
{
563
    /**
564
     * settings of current engine instance
565
     *
566
     * @var array
567
     * @access public
568
     */
569
    public $settings;
570
571
    /**
572
     * Initialize the cache engine
573
     *
574
     * Called automatically by the cache frontend. This method may be called
575
     * multiple times to reconfigure the engine with different settings.
576
     * Implementations should handle repeated initialization gracefully.
577
     *
578
     * @param  array $settings Associative array of parameters for the engine
579
     * @return boolean True if the engine has been successfully initialized, false if not
580
     * @access   public
581
     */
582
    public function init($settings = [])
583
    {
584
        $this->settings = array_merge(
585
            [
586
                'duration'    => 31556926,
587
                'probability' => 100,
588
            ],
589
            $settings,
590
        );
591
592
        return true;
593
    }
594
595
    /**
596
     * Garbage collection
597
     *
598
     * Permanently remove all expired and deleted data
599
     *
600
     * @access public
601
     */
602
    public function gc() {}
603
604
    /**
605
     * Write value for a key into cache
606
     *
607
     * @param  string $key      Identifier for the data
608
     * @param  mixed  $value    Data to be cached
609
     * @param  mixed  $duration How long to cache the data, in seconds
610
     * @return boolean True if the data was successfully cached, false on failure
611
     * @access public
612
     */
613
    public function write($key, $value, $duration = null)
614
    {
615
        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

615
        trigger_error(sprintf(/** @scrutinizer ignore-call */ __('Method write() not implemented in %s', true), get_class($this)), E_USER_ERROR);
Loading history...
616
    }
617
618
    /**
619
     * Read a key from the cache
620
     *
621
     * @param  string $key Identifier for the data
622
     * @return mixed  The cached data, or false if the data doesn't exist, has expired, or if there was an error fetching it
623
     * @access public
624
     */
625
    public function read($key)
626
    {
627
        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

627
        trigger_error(sprintf(/** @scrutinizer ignore-call */ __('Method read() not implemented in %s', true), get_class($this)), E_USER_ERROR);
Loading history...
628
    }
629
630
    /**
631
     * Delete a key from the cache
632
     *
633
     * @param  string $key Identifier for the data
634
     * @return boolean True if the value was successfully deleted, false if it didn't exist or couldn't be removed
635
     * @access public
636
     */
637
    public function delete($key) {}
638
639
    /**
640
     * Delete all keys from the cache
641
     *
642
     * @param  boolean $check if true will check expiration, otherwise delete all
643
     * @return boolean True if the cache was successfully cleared, false otherwise
644
     * @access public
645
     */
646
    public function clear($check) {}
647
648
    /**
649
     * Cache Engine settings
650
     *
651
     * @return array settings
652
     * @access public
653
     */
654
    public function settings()
655
    {
656
        return $this->settings;
657
    }
658
}
659