Issues (29)

Handlers/File.php (2 issues)

1
<?php
2
3
/**
4
 * This file is part of Blitz PHP framework.
5
 *
6
 * (c) 2022 Dimitri Sitchet Tomkeu <[email protected]>
7
 *
8
 * For the full copyright and license information, please view
9
 * the LICENSE file that was distributed with this source code.
10
 */
11
12
namespace BlitzPHP\Cache\Handlers;
13
14
use BlitzPHP\Cache\InvalidArgumentException;
15
use CallbackFilterIterator;
16
use DateInterval;
17
use Exception;
18
use FilesystemIterator;
19
use LogicException;
20
use RecursiveDirectoryIterator;
21
use RecursiveIteratorIterator;
22
use SplFileInfo;
23
use SplFileObject;
24
25
class File extends BaseHandler
26
{
27
    /**
28
     * Instance de la classe SplFileObject
29
     *
30
     * @var SplFileObject|null
31
     */
32
    protected $_File;
33
34
    /**
35
     * La configuration par défaut utilisée sauf si elle est remplacée par la configuration d'exécution
36
     *
37
     * - `duration` Spécifiez combien de temps durent les éléments de cette configuration de cache.
38
     * - `groups` Liste des groupes ou 'tags' associés à chaque clé stockée dans cette configuration.
39
     * 			pratique pour supprimer un groupe complet du cache.
40
     * - `lock` Utilisé par FileCache. Les fichiers doivent-ils être verrouillés avant d'y écrire ?
41
     * - `mask` Le masque utilisé pour les fichiers créés
42
     * - `path` Chemin d'accès où les fichiers cache doivent être enregistrés. Par défaut, le répertoire temporaire du système.
43
     * - `prefix` Préfixé à toutes les entrées. Bon pour quand vous avez besoin de partager un keyspace
44
     * 			avec une autre configuration de cache ou une autre application. cache::gc d'être appelé automatiquement.
45
     * - `serialize` Les objets du cache doivent-ils être sérialisés en premier.
46
     *
47
     * @var array<string, mixed>
48
     */
49
    protected array $_defaultConfig = [
50
		'duration'  => 3600,
51
		'groups'    => [],
52
		'lock'      => true,
53
		'mask'      => 0o664,
54
		'dirMask'   => 0770,
55
		'path'      => null,
56
		'prefix'    => 'blitz_',
57
		'serialize' => true,
58
    ];
59
60
    /**
61
     * Vrai sauf si FileEngine :: __active(); échoue
62
     */
63
    protected bool $_init = true;
64
65
    /**
66
     * {@inheritDoc}
67
     */
68
    public function init(array $config = []): bool
69
    {
70
        parent::init($config);
71
72
        if ($this->_config['path'] === null) {
73
            $this->_config['path'] = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'blitz-php' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
74
        }
75
        if (substr($this->_config['path'], -1) !== DIRECTORY_SEPARATOR) {
76
            $this->_config['path'] .= DIRECTORY_SEPARATOR;
77
        }
78
        if ($this->_groupPrefix) {
79
            $this->_groupPrefix = str_replace('_', DIRECTORY_SEPARATOR, $this->_groupPrefix);
80
        }
81
82
        return $this->_active();
83
    }
84
85
    /**
86
     * {@inheritDoc}
87
     */
88
    public function set(string $key, mixed $value, DateInterval|int|null $ttl = null): bool
89
    {
90
        if ($value === '' || ! $this->_init) {
91
            return false;
92
        }
93
94
        $key = $this->_key($key);
95
96
        if ($this->_setKey($key, true) === false) {
97
            return false;
98
        }
99
100
        if (! empty($this->_config['serialize'])) {
101
            $value = serialize($value);
102
        }
103
104
        $expires  = time() + $this->duration($ttl);
105
        $contents = implode('', [$expires, PHP_EOL, $value, PHP_EOL]);
106
107
        if ($this->_config['lock']) {
108
            $this->_File->flock(LOCK_EX);
109
        }
110
111
        $this->_File->rewind();
112
        $success = $this->_File->ftruncate(0)
113
            && $this->_File->fwrite($contents)
114
            && $this->_File->fflush();
115
116
        if ($this->_config['lock']) {
117
            $this->_File->flock(LOCK_UN);
118
        }
119
        unset($this->_File);
120
121
        return $success;
122
    }
123
124
    /**
125
     * {@inheritDoc}
126
     */
127
    public function get(string $key, mixed $default = null): mixed
128
    {
129
        $key = $this->_key($key);
130
131
        if (! $this->_init || $this->_setKey($key) === false) {
132
            return $default;
133
        }
134
135
        if ($this->_config['lock']) {
136
            $this->_File->flock(LOCK_SH);
137
        }
138
139
        $this->_File->rewind();
140
        $time      = time();
141
        $cachetime = (int) $this->_File->current();
142
143
        if ($cachetime < $time) {
144
            if ($this->_config['lock']) {
145
                $this->_File->flock(LOCK_UN);
146
            }
147
148
            return $default;
149
        }
150
151
        $data = '';
152
        $this->_File->next();
153
154
        while ($this->_File->valid()) {
155
            $data .= $this->_File->current();
156
            $this->_File->next();
157
        }
158
159
        if ($this->_config['lock']) {
160
            $this->_File->flock(LOCK_UN);
161
        }
162
163
        $data = trim($data);
164
165
        if ($data !== '' && ! empty($this->_config['serialize'])) {
166
            $data = unserialize($data);
167
        }
168
169
        return $data;
170
    }
171
172
    /**
173
     * {@inheritDoc}
174
     */
175
    public function delete(string $key): bool
176
    {
177
        $key = $this->_key($key);
178
179
        if ($this->_setKey($key) === false || ! $this->_init) {
180
            return false;
181
        }
182
183
        $path        = $this->_File->getRealPath();
184
        $this->_File = null;
185
186
        if ($path === false) {
187
            return false;
188
        }
189
190
        // phpcs:disable
191
        return @unlink($path);
192
        // phpcs:enable
193
    }
194
195
    /**
196
     * {@inheritDoc}
197
     */
198
    public function clear(): bool
199
    {
200
        if (! $this->_init) {
201
            return false;
202
        }
203
        unset($this->_File);
204
205
        $this->_clearDirectory($this->_config['path']);
206
207
        $directory = new RecursiveDirectoryIterator(
208
            $this->_config['path'],
209
            FilesystemIterator::SKIP_DOTS
210
        );
211
        $contents = new RecursiveIteratorIterator(
212
            $directory,
213
            RecursiveIteratorIterator::SELF_FIRST
214
        );
215
        $cleared = [];
216
217
        /** @var SplFileInfo $fileInfo */
218
        foreach ($contents as $fileInfo) {
219
            if ($fileInfo->isFile()) {
220
                unset($fileInfo);
221
222
                continue;
223
            }
224
225
            $realPath = $fileInfo->getRealPath();
226
            if (! $realPath) {
227
                unset($fileInfo);
228
229
                continue;
230
            }
231
232
            $path = $realPath . DIRECTORY_SEPARATOR;
233
            if (! in_array($path, $cleared, true)) {
234
                $this->_clearDirectory($path);
235
                $cleared[] = $path;
236
            }
237
238
            // les itérateurs internes possibles doivent également être désactivés pour que les verrous sur les parents soient libérés
239
            unset($fileInfo);
240
        }
241
242
        // la désactivation des itérateurs aide à libérer les verrous possibles dans certains environnements,
243
        // ce qui pourrait sinon faire échouer `rmdir()`
244
        unset($directory, $contents);
245
246
        return true;
247
    }
248
249
    /**
250
     * Utilisé pour effacer un répertoire de fichiers correspondants.
251
     */
252
    protected function _clearDirectory(string $path): void
253
    {
254
        if (! is_dir($path)) {
255
            return;
256
        }
257
258
        $dir = dir($path);
259
        if (! $dir) {
260
            return;
261
        }
262
263
        $prefixLength = strlen($this->_config['prefix']);
264
265
        while (($entry = $dir->read()) !== false) {
266
            if (substr($entry, 0, $prefixLength) !== $this->_config['prefix']) {
267
                continue;
268
            }
269
270
            try {
271
                $file = new SplFileObject($path . $entry, 'r');
272
            } catch (Exception $e) {
273
                continue;
274
            }
275
276
            if ($file->isFile()) {
277
                $filePath = $file->getRealPath();
278
                unset($file);
279
280
                // phpcs:disable
281
                @unlink($filePath);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). 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

281
                /** @scrutinizer ignore-unhandled */ @unlink($filePath);

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...
282
                // phpcs:enable
283
            }
284
        }
285
286
        $dir->close();
287
    }
288
289
    /**
290
     * Pas implementé
291
     *
292
     * @throws LogicException
293
     */
294
    public function decrement(string $key, int $offset = 1)
295
    {
296
        throw new LogicException('Les fichiers ne peuvent pas être décrémentés de manière atomique.');
297
    }
298
299
    /**
300
     * Pas implementé
301
     *
302
     * @throws LogicException
303
     */
304
    public function increment(string $key, int $offset = 1)
305
    {
306
        throw new LogicException('Les fichiers ne peuvent pas être incrémentés de manière atomique.');
307
    }
308
309
    /**
310
     * {@inheritDoc}
311
     */
312
    public function info()
313
    {
314
        return $this->getDirFileInfo($this->_config['path']);
315
    }
316
317
    /**
318
     * Définit la clé de cache actuelle que cette classe gère et crée un SplFileObject inscriptible
319
     * pour le fichier cache auquel la clé fait référence.
320
     *
321
     * @param bool $createKey Whether the key should be created if it doesn't exists, or not
322
     *
323
     * @return bool true if the cache key could be set, false otherwise
324
     */
325
    protected function _setKey(string $key, bool $createKey = false): bool
326
    {
327
        $groups = null;
328
        if ($this->_groupPrefix) {
329
            $groups = vsprintf($this->_groupPrefix, $this->groups());
330
        }
331
        $dir = $this->_config['path'] . $groups;
332
333
        if (! is_dir($dir)) {
334
            mkdir($dir, $this->_config['dirMask'], true);
335
        }
336
337
        $path = new SplFileInfo($dir . $key);
338
339
        if (! $createKey && ! $path->isFile()) {
340
            return false;
341
        }
342
        if (
343
            empty($this->_File)
344
            || $this->_File->getBasename() !== $key
345
            || $this->_File->valid() === false
346
        ) {
347
            $exists = is_file($path->getPathname());
348
349
            try {
350
                $this->_File = $path->openFile('c+');
351
            } catch (Exception $e) {
352
                trigger_error($e->getMessage(), E_USER_WARNING);
353
354
                return false;
355
            }
356
            unset($path);
357
358
            if (! $exists && ! chmod($this->_File->getPathname(), (int) $this->_config['mask'])) {
359
                trigger_error(sprintf(
360
                    'Impossible d\'appliquer le masque d\'autorisation "%s" sur le fichier cache "%s"',
361
                    $this->_File->getPathname(),
362
                    $this->_config['mask']
363
                ), E_USER_WARNING);
364
            }
365
        }
366
367
        return true;
368
    }
369
370
    /**
371
     * Déterminer si le répertoire de cache est accessible en écriture
372
     */
373
    protected function _active(): bool
374
    {
375
        $dir     = new SplFileInfo($this->_config['path']);
376
        $path    = $dir->getPathname();
377
        $success = true;
378
        if (! is_dir($path)) {
379
            // phpcs:disable
380
            $success = @mkdir($path, $this->_config['dirMask'], true);
381
            // phpcs:enable
382
        }
383
384
        $isWritableDir = ($dir->isDir() && $dir->isWritable());
385
        if (! $success || ($this->_init && ! $isWritableDir)) {
386
            $this->_init = false;
387
            trigger_error(sprintf(
388
                '%s n\'est pas ecrivable',
389
                $this->_config['path']
390
            ), E_USER_WARNING);
391
        }
392
393
        return $success;
394
    }
395
396
    /**
397
     * {@inheritDoc}
398
     */
399
    protected function _key($key): string
400
    {
401
        $key = parent::_key($key);
402
403
        return rawurlencode($key);
404
    }
405
406
    /**
407
     * Supprime récursivement tous les fichiers sous n'importe quel répertoire nommé $group
408
     */
409
    public function clearGroup(string $group): bool
410
    {
411
        unset($this->_File);
412
413
        $prefix = (string) $this->_config['prefix'];
414
415
        $directoryIterator = new RecursiveDirectoryIterator($this->_config['path']);
416
        $contents          = new RecursiveIteratorIterator(
417
            $directoryIterator,
418
            RecursiveIteratorIterator::CHILD_FIRST
419
        );
420
        $filtered = new CallbackFilterIterator(
421
            $contents,
422
            static function (SplFileInfo $current) use ($group, $prefix) {
423
                if (! $current->isFile()) {
424
                    return false;
425
                }
426
427
                $hasPrefix = $prefix === ''
428
                    || str_starts_with($current->getBasename(), $prefix);
429
                if ($hasPrefix === false) {
430
                    return false;
431
                }
432
433
                return str_contains(
434
                    $current->getPathname(),
435
                    DIRECTORY_SEPARATOR . $group . DIRECTORY_SEPARATOR,
436
                );
437
            }
438
        );
439
440
        foreach ($filtered as $object) {
441
			/** @var \SplFileInfo $object */
442
            $path = $object->getPathname();
443
            unset($object);
444
            // phpcs:ignore
445
            @unlink($path);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). 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

445
            /** @scrutinizer ignore-unhandled */ @unlink($path);

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...
446
        }
447
448
        // la désactivation des itérateurs permet de libérer d'éventuels verrous dans certains environnements,
449
        // qui pourrait autrement faire échouer `rmdir()`
450
        unset($directoryIterator, $contents, $filtered);
451
452
        return true;
453
    }
454
455
    /**
456
     * Lit le répertoire spécifié et construit un tableau contenant les noms de fichiers,
457
     * taille de fichier, dates et autorisations
458
     *
459
     * Tous les sous-dossiers contenus dans le chemin spécifié sont également lus.
460
     *
461
     * @param string $sourceDir    Chemin d'accès à la source
462
     * @param bool   $topLevelOnly Ne regarder que le répertoire de niveau supérieur spécifié ?
463
     * @param bool   $_recursion   Variable interne pour déterminer l'état de la récursivité - ne pas utiliser dans les appels
464
     *
465
     * @return array|false
466
     */
467
    protected function getDirFileInfo(string $sourceDir, bool $topLevelOnly = true, bool $_recursion = false)
468
    {
469
        static $_filedata = [];
470
        $relativePath     = $sourceDir;
471
472
        if ($fp = @opendir($sourceDir)) {
473
            // réinitialise le tableau et s'assure que $source_dir a une barre oblique à la fin de l'appel initial
474
            if ($_recursion === false) {
475
                $_filedata = [];
476
                $sourceDir = rtrim(realpath($sourceDir) ?: $sourceDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
477
            }
478
479
            // Utilisé pour être foreach (scandir($source_dir, 1) comme $file), mais scandir() n'est tout simplement pas aussi rapide
480
            while (false !== ($file = readdir($fp))) {
481
                if (is_dir($sourceDir . $file) && $file[0] !== '.' && $topLevelOnly === false) {
482
                    $this->getDirFileInfo($sourceDir . $file . DIRECTORY_SEPARATOR, $topLevelOnly, true);
483
                } elseif (! is_dir($sourceDir . $file) && $file[0] !== '.') {
484
                    $_filedata[$file]                  = $this->getFileInfo($sourceDir . $file);
485
                    $_filedata[$file]['relative_path'] = $relativePath;
486
                }
487
            }
488
489
            closedir($fp);
490
491
            return $_filedata;
492
        }
493
494
        return false;
495
    }
496
497
    /**
498
     * Étant donné un fichier et un chemin, renvoie le nom, le chemin, la taille, la date de modification
499
     * Le deuxième paramètre vous permet de déclarer explicitement les informations que vous souhaitez renvoyer
500
     * Les options sont : nom, chemin_serveur, taille, date, lisible, inscriptible, exécutable, fileperms
501
     * Renvoie FALSE si le fichier est introuvable.
502
     *
503
     * @param array|string $returnedValues Tableau ou chaîne d'informations séparées par des virgules renvoyée
504
     *
505
     * @return array|false
506
     */
507
    protected function getFileInfo(string $file, $returnedValues = ['name', 'server_path', 'size', 'date'])
508
    {
509
        if (! is_file($file)) {
510
            return false;
511
        }
512
513
        if (is_string($returnedValues)) {
514
            $returnedValues = explode(',', $returnedValues);
515
        }
516
517
        $fileInfo = [];
518
519
        foreach ($returnedValues as $key) {
520
            switch ($key) {
521
                case 'name':
522
                    $fileInfo['name'] = basename($file);
523
                    break;
524
525
                case 'server_path':
526
                    $fileInfo['server_path'] = $file;
527
                    break;
528
529
                case 'size':
530
                    $fileInfo['size'] = filesize($file);
531
                    break;
532
533
                case 'date':
534
                    $fileInfo['date'] = filemtime($file);
535
                    break;
536
537
                case 'readable':
538
                    $fileInfo['readable'] = is_readable($file);
539
                    break;
540
541
                case 'writable':
542
                    $fileInfo['writable'] = is_writable($file);
543
                    break;
544
545
                case 'executable':
546
                    $fileInfo['executable'] = is_executable($file);
547
                    break;
548
549
                case 'fileperms':
550
                    $fileInfo['fileperms'] = fileperms($file);
551
                    break;
552
            }
553
        }
554
555
        return $fileInfo;
556
    }
557
}
558