Passed
Push — main ( eecacd...dafd8a )
by Dimitri
11:37
created

File::getFileInfo()   C

Complexity

Conditions 12
Paths 21

Size

Total Lines 49
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
eloc 32
nc 21
nop 2
dl 0
loc 49
rs 6.9666
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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'      => 0664,
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, DateInterval|int|null $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
        /** @var SplFileInfo $fileInfo */
222
        foreach ($contents as $fileInfo) {
223
            if ($fileInfo->isFile()) {
224
                unset($fileInfo);
225
226
                continue;
227
            }
228
229
            $realPath = $fileInfo->getRealPath();
230
            if (! $realPath) {
231
                unset($fileInfo);
232
233
                continue;
234
            }
235
236
            $path = $realPath . DIRECTORY_SEPARATOR;
237
            if (! in_array($path, $cleared, true)) {
238
                $this->_clearDirectory($path);
239
                $cleared[] = $path;
240
            }
241
242
            // les itérateurs internes possibles doivent également être désactivés pour que les verrous sur les parents soient libérés
243
            unset($fileInfo);
244
        }
245
246
        // la désactivation des itérateurs aide à libérer les verrous possibles dans certains environnements,
247
        // ce qui pourrait sinon faire échouer `rmdir()`
248
        unset($directory, $contents);
249
250
        return true;
251
    }
252
253
    /**
254
     * Utilisé pour effacer un répertoire de fichiers correspondants.
255
     */
256
    protected function _clearDirectory(string $path): void
257
    {
258
        if (! is_dir($path)) {
259
            return;
260
        }
261
262
        $dir = dir($path);
263
        if (! $dir) {
0 ignored issues
show
introduced by
$dir is of type Directory, thus it always evaluated to true.
Loading history...
264
            return;
265
        }
266
267
        $prefixLength = strlen($this->_config['prefix']);
268
269
        while (($entry = $dir->read()) !== false) {
270
            if (substr($entry, 0, $prefixLength) !== $this->_config['prefix']) {
271
                continue;
272
            }
273
274
            try {
275
                $file = new SplFileObject($path . $entry, 'r');
276
            } catch (Exception $e) {
277
                continue;
278
            }
279
280
            if ($file->isFile()) {
281
                $filePath = $file->getRealPath();
282
                unset($file);
283
284
                // phpcs:disable
285
                @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

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

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