File   F
last analyzed

Complexity

Total Complexity 89

Size/Duplication

Total Lines 544
Duplicated Lines 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
eloc 234
c 1
b 1
f 0
dl 0
loc 544
rs 2
wmc 89

15 Methods

Rating   Name   Duplication   Size   Complexity  
B _clearDirectory() 0 35 7
A decrement() 0 3 1
A increment() 0 3 1
B get() 0 46 10
A init() 0 15 4
B set() 0 36 9
A delete() 0 18 4
B getDirFileInfo() 0 28 10
C getFileInfo() 0 49 12
B _setKey() 0 43 11
A _active() 0 21 6
A info() 0 3 1
A clearGroup() 0 45 5
A _key() 0 12 2
B clear() 0 49 6

How to fix   Complexity   

Complex Class

Complex classes like File often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use File, and based on these observations, apply Extract Interface, too.

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
        'path'      => null,
55
        'prefix'    => 'blitz_',
56
        'serialize' => true,
57
    ];
58
59
    /**
60
     * Vrai sauf si FileEngine :: __active(); échoue
61
     */
62
    protected bool $_init = true;
63
64
    /**
65
     * {@inheritDoc}
66
     */
67
    public function init(array $config = []): bool
68
    {
69
        parent::init($config);
70
71
        if ($this->_config['path'] === null) {
72
            $this->_config['path'] = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'blitz-php' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
73
        }
74
        if (substr($this->_config['path'], -1) !== DIRECTORY_SEPARATOR) {
75
            $this->_config['path'] .= DIRECTORY_SEPARATOR;
76
        }
77
        if ($this->_groupPrefix) {
78
            $this->_groupPrefix = str_replace('_', DIRECTORY_SEPARATOR, $this->_groupPrefix);
79
        }
80
81
        return $this->_active();
82
    }
83
84
    /**
85
     * {@inheritDoc}
86
     */
87
    public function set(string $key, mixed $value, null|DateInterval|int $ttl = null): bool
88
    {
89
        if ($value === '' || ! $this->_init) {
90
            return false;
91
        }
92
93
        $key = $this->_key($key);
94
95
        if ($this->_setKey($key, true) === false) {
96
            return false;
97
        }
98
99
        if (! empty($this->_config['serialize'])) {
100
            $value = serialize($value);
101
        }
102
103
        $expires  = time() + $this->duration($ttl);
104
        $contents = implode('', [$expires, PHP_EOL, $value, PHP_EOL]);
105
106
        if ($this->_config['lock']) {
107
            /** @psalm-suppress PossiblyNullReference */
108
            $this->_File->flock(LOCK_EX);
109
        }
110
111
        /** @psalm-suppress PossiblyNullReference */
112
        $this->_File->rewind();
113
        $success = $this->_File->ftruncate(0)
0 ignored issues
show
Bug introduced by
The method ftruncate() 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

113
        $success = $this->_File->/** @scrutinizer ignore-call */ ftruncate(0)

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...
114
            && $this->_File->fwrite($contents)
115
            && $this->_File->fflush();
116
117
        if ($this->_config['lock']) {
118
            $this->_File->flock(LOCK_UN);
119
        }
120
        $this->_File = null;
121
122
        return $success;
123
    }
124
125
    /**
126
     * {@inheritDoc}
127
     */
128
    public function get(string $key, mixed $default = null): mixed
129
    {
130
        $key = $this->_key($key);
131
132
        if (! $this->_init || $this->_setKey($key) === false) {
133
            return $default;
134
        }
135
136
        if ($this->_config['lock']) {
137
            /** @psalm-suppress PossiblyNullReference */
138
            $this->_File->flock(LOCK_SH);
139
        }
140
141
        /** @psalm-suppress PossiblyNullReference */
142
        $this->_File->rewind();
143
        $time      = time();
144
        $cachetime = (int) $this->_File->current();
145
146
        if ($cachetime < $time) {
147
            if ($this->_config['lock']) {
148
                $this->_File->flock(LOCK_UN);
149
            }
150
151
            return $default;
152
        }
153
154
        $data = '';
155
        $this->_File->next();
156
157
        while ($this->_File->valid()) {
158
            /** @psalm-suppress PossiblyInvalidOperand */
159
            $data .= $this->_File->current();
160
            $this->_File->next();
161
        }
162
163
        if ($this->_config['lock']) {
164
            $this->_File->flock(LOCK_UN);
165
        }
166
167
        $data = trim($data);
168
169
        if ($data !== '' && ! empty($this->_config['serialize'])) {
170
            $data = unserialize($data);
171
        }
172
173
        return $data;
174
    }
175
176
    /**
177
     * {@inheritDoc}
178
     */
179
    public function delete(string $key): bool
180
    {
181
        $key = $this->_key($key);
182
183
        if ($this->_setKey($key) === false || ! $this->_init) {
184
            return false;
185
        }
186
187
        /** @psalm-suppress PossiblyNullReference */
188
        $path        = $this->_File->getRealPath();
189
        $this->_File = null;
190
191
        if ($path === false) {
192
            return false;
193
        }
194
195
        // phpcs:disable
196
        return @unlink($path);
197
        // phpcs:enable
198
    }
199
200
    /**
201
     * {@inheritDoc}
202
     */
203
    public function clear(): bool
204
    {
205
        if (! $this->_init) {
206
            return false;
207
        }
208
        $this->_File = null;
209
210
        $this->_clearDirectory($this->_config['path']);
211
212
        $directory = new RecursiveDirectoryIterator(
213
            $this->_config['path'],
214
            FilesystemIterator::SKIP_DOTS
215
        );
216
        $contents = new RecursiveIteratorIterator(
217
            $directory,
218
            RecursiveIteratorIterator::SELF_FIRST
219
        );
220
        $cleared = [];
221
222
        /** @var SplFileInfo $fileInfo */
223
        foreach ($contents as $fileInfo) {
224
            if ($fileInfo->isFile()) {
225
                unset($fileInfo);
226
227
                continue;
228
            }
229
230
            $realPath = $fileInfo->getRealPath();
231
            if (! $realPath) {
232
                unset($fileInfo);
233
234
                continue;
235
            }
236
237
            $path = $realPath . DIRECTORY_SEPARATOR;
238
            if (! in_array($path, $cleared, true)) {
239
                $this->_clearDirectory($path);
240
                $cleared[] = $path;
241
            }
242
243
            // les itérateurs internes possibles doivent également être désactivés pour que les verrous sur les parents soient libérés
244
            unset($fileInfo);
245
        }
246
247
        // la désactivation des itérateurs aide à libérer les verrous possibles dans certains environnements,
248
        // ce qui pourrait sinon faire échouer `rmdir()`
249
        unset($directory, $contents);
250
251
        return true;
252
    }
253
254
    /**
255
     * Utilisé pour effacer un répertoire de fichiers correspondants.
256
     */
257
    protected function _clearDirectory(string $path): void
258
    {
259
        if (! is_dir($path)) {
260
            return;
261
        }
262
263
        $dir = dir($path);
264
        if (! $dir) {
0 ignored issues
show
introduced by
$dir is of type Directory, thus it always evaluated to true.
Loading history...
265
            return;
266
        }
267
268
        $prefixLength = strlen($this->_config['prefix']);
269
270
        while (($entry = $dir->read()) !== false) {
271
            if (substr($entry, 0, $prefixLength) !== $this->_config['prefix']) {
272
                continue;
273
            }
274
275
            try {
276
                $file = new SplFileObject($path . $entry, 'r');
277
            } catch (Exception $e) {
278
                continue;
279
            }
280
281
            if ($file->isFile()) {
282
                $filePath = $file->getRealPath();
283
                unset($file);
284
285
                // phpcs:disable
286
                @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

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

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