Passed
Push — main ( afae0a...eb4a74 )
by Dimitri
09:41 queued 03:53
created

Publisher   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 461
Duplicated Lines 0 %

Test Coverage

Coverage 93.98%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 122
c 2
b 0
f 0
dl 0
loc 461
ccs 78
cts 83
cp 0.9398
rs 6
wmc 55

22 Methods

Rating   Name   Duplication   Size   Complexity  
A addLineAfter() 0 15 2
A addLineBefore() 0 15 2
A getPublished() 0 3 1
A getSource() 0 3 1
A publish() 0 8 3
A __construct() 0 20 3
A safeCopyFile() 0 28 6
A addUri() 0 9 1
A addPath() 0 5 1
B discover() 0 32 8
A copy() 0 16 3
A wipe() 0 5 1
A __destruct() 0 6 2
A merge() 0 25 3
A getDestination() 0 3 1
A replace() 0 11 1
A wipeDirectory() 0 14 4
A getErrors() 0 3 1
A getScratch() 0 10 3
A addUris() 0 7 2
A addPaths() 0 7 2
A verifyAllowed() 0 6 4

How to fix   Complexity   

Complex Class

Complex classes like Publisher 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 Publisher, 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\Publisher;
13
14
use BlitzPHP\Container\Services;
15
use BlitzPHP\Exceptions\PublisherException;
16
use BlitzPHP\Filesystem\Files\FileCollection;
17
use BlitzPHP\Http\Uri;
18
use RuntimeException;
19
use Throwable;
20
21
/**
22
 * Les éditeurs lisent les chemins d'accès aux fichiers à partir de diverses sources et copient les fichiers vers différentes destinations.
23
 * Cette classe sert à la fois de base pour les directives de publication individuelles et de mode de découverte pour lesdites instances.
24
 * Dans cette classe, un "fichier" est un chemin complet vers un fichier vérifié tandis qu'un "chemin" est relatif à sa source ou à sa destination et peut indiquer soit un fichier, soit un répertoire dont l'existence n'est pas confirmée.
25
 *
26
 * Les échecs de classe lancent l'exception PublisherException,
27
 * mais certaines méthodes sous-jacentes peuvent percoler différentes exceptions,
28
 * comme FileException, FileNotFoundException ou InvalidArgumentException.
29
 *
30
 * Les opérations d'écriture intercepteront toutes les erreurs dans le fichier spécifique
31
 * Propriété $errors pour minimiser l'impact des opérations par lots partielles.
32
 *
33
 * @credit <a href="http://codeigniter.com">CodeIgniter 4 - \CodeIgniter\Publisher\Publisher</a>
34
 */
35
class Publisher extends FileCollection
36
{
37
    /**
38
     * Tableau des éditeurs découverts.
39
     *
40
     * @var array<string, list<self>|null>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<string, list<self>|null> at position 4 could not be parsed: Expected '>' at position 4, but found 'list'.
Loading history...
41
     */
42
    private static array $discovered = [];
43
44
    /**
45
     * Répertoire à utiliser pour les méthodes nécessitant un stockage temporaire.
46
     * Créé à la volée selon les besoins.
47
     */
48
    private ?string $scratch = null;
49
50
    /**
51
     * Exceptions pour des fichiers spécifiques de la dernière opération d'écriture.
52
     *
53
     * @var array<string, Throwable>
54
     */
55
    private array $errors = [];
56
57
    /**
58
     * Liste des fichiers publiés traitant la dernière opération d'écriture.
59
     *
60
     * @var list<string>
0 ignored issues
show
Bug introduced by
The type BlitzPHP\Publisher\list was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
61
     */
62
    private array $published = [];
63
64
    /**
65
     * Liste des répertoires autorisés et leur regex de fichiers autorisés.
66
     * Les restrictions sont intentionnellement privées pour éviter qu'elles ne soient dépassées.
67
     *
68
     * @var array<string,string>
69
     */
70
    private readonly array $restrictions;
71
72
    private readonly ContentReplacer $replacer;
73
74
    /**
75
     * Chemin de base à utiliser pour la source.
76
     */
77
    protected string $source = ROOTPATH;
78
79
    /**
80
     * Chemin de base à utiliser pour la destination.
81
     */
82
    protected string $destination = WEBROOT;
0 ignored issues
show
Bug introduced by
The constant BlitzPHP\Publisher\WEBROOT was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
83
84
    // --------------------------------------------------------------------
85
    // Méthodes d'assistance
86
    // --------------------------------------------------------------------
87
88
    /**
89
     * Découvre et renvoie tous les éditeurs dans le répertoire d'espace de noms spécifié.
90
     *
91
     * @return list<self>
92
     */
93
    final public static function discover(string $directory = 'Publishers', string $namespace = ''): array
94
    {
95 4
        $key = implode('.', [$directory, $namespace]);
96
97
        if (isset(self::$discovered[$key])) {
98 4
            return self::$discovered[$key];
99
        }
100
101 4
        self::$discovered[$key] = [];
102
103 4
        $locator = Services::locator();
104
105
        $files = $namespace === ''
106
            ? $locator->listFiles($directory)
107 2
            : $locator->listNamespaceFiles($namespace, $directory);
108
109
        if ([] === $files) {
110 2
            return [];
0 ignored issues
show
Bug Best Practice introduced by
The expression return array() returns the type array which is incompatible with the documented return type BlitzPHP\Publisher\list.
Loading history...
111
        }
112
113
        // Boucle sur chaque fichier en vérifiant s'il s'agit d'un Publisher
114
        foreach (array_unique($files) as $file) {
115 4
            $className = $locator->findQualifiedNameFromPath($file);
116
117
            if ($className !== false && class_exists($className) && is_a($className, self::class, true)) {
118 4
                self::$discovered[$key][] = Services::factory($className);
119
            }
120
        }
121
122 4
        sort(self::$discovered[$key]);
123
124 4
        return self::$discovered[$key];
125
    }
126
127
    /**
128
     * Supprime un répertoire et tous ses fichiers et sous-répertoires.
129
     */
130
    private static function wipeDirectory(string $directory): void
131
    {
132
        if (is_dir($directory)) {
133
            // Essayez plusieurs fois en cas de mèches persistantes
134 6
            $attempts = 10;
135
136
            while ((bool) $attempts && ! delete_files($directory, true, false, true)) {
137
                // @codeCoverageIgnoreStart
138
                $attempts--;
139
                usleep(100000); // .1s
140
                // @codeCoverageIgnoreEnd
141
            }
142
143 6
            @rmdir($directory);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for rmdir(). 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

143
            /** @scrutinizer ignore-unhandled */ @rmdir($directory);

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...
144
        }
145
    }
146
147
    /**
148
     * Charge l'assistant et vérifie les répertoires source et destination.
149
     */
150
    public function __construct(?string $source = null, ?string $destination = null)
151
    {
152 12
        helper('filesystem');
153
154 12
        $this->source      = self::resolveDirectory($source ?? $this->source);
155 12
        $this->destination = self::resolveDirectory($destination ?? $this->destination);
156
157 12
        $this->replacer = new ContentReplacer();
0 ignored issues
show
Bug introduced by
The property replacer is declared read-only in BlitzPHP\Publisher\Publisher.
Loading history...
158
159
        // Les restrictions ne sont intentionnellement pas injectées pour empêcher le dépassement
160 12
        $this->restrictions = config('publisher.restrictions');
0 ignored issues
show
Bug introduced by
The property restrictions is declared read-only in BlitzPHP\Publisher\Publisher.
Loading history...
Documentation Bug introduced by
It seems like config('publisher.restrictions') can also be of type BlitzPHP\Config\Config. However, the property $restrictions is declared as type array<string,string>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
161
162
        // Assurez-vous que la destination est autorisée
163
        foreach (array_keys($this->restrictions) as $directory) {
164
            if (str_starts_with($this->destination, $directory)) {
165 12
                return;
166
            }
167
        }
168
169 2
        throw PublisherException::destinationNotAllowed($this->destination);
170
    }
171
172
    /**
173
     * Nettoie tous les fichiers temporaires dans l'espace de travail.
174
     */
175
    public function __destruct()
176
    {
177
        if (isset($this->scratch)) {
178 6
            self::wipeDirectory($this->scratch);
0 ignored issues
show
Bug introduced by
It seems like $this->scratch can also be of type null; however, parameter $directory of BlitzPHP\Publisher\Publisher::wipeDirectory() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

178
            self::wipeDirectory(/** @scrutinizer ignore-type */ $this->scratch);
Loading history...
179
180 6
            $this->scratch = null;
181
        }
182
    }
183
184
    /**
185
     * Lit les fichiers à partir des sources et les copie vers leurs destinations.
186
     * Cette méthode devrait être réimplémentée par les classes filles destinées à la découverte.
187
     *
188
     * @throws RuntimeException
189
     */
190
    public function publish(): bool
191
    {
192
        // Protection contre une mauvaise utilisation accidentelle
193
        if ($this->source === ROOTPATH && $this->destination === WEBROOT) {
0 ignored issues
show
Bug introduced by
The constant BlitzPHP\Publisher\WEBROOT was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
194 2
            throw new RuntimeException('Les classes enfants de Publisher doivent fournir leur propre méthode de publication ou une source et une destination.');
195
        }
196
197 2
        return $this->addPath('/')->merge(true);
198
    }
199
200
    // --------------------------------------------------------------------
201
    // Accesseurs de propriété
202
    // --------------------------------------------------------------------
203
204
    /**
205
     * Renvoie le répertoire source.
206
     */
207
    final public function getSource(): string
208
    {
209 2
        return $this->source;
210
    }
211
212
    /**
213
     * Renvoie le répertoire de destination.
214
     */
215
    final public function getDestination(): string
216
    {
217 4
        return $this->destination;
218
    }
219
220
    /**
221
     * Renvoie l'espace de travail temporaire, en le créant si nécessaire.
222
     */
223
    final public function getScratch(): string
224
    {
225
        if ($this->scratch === null) {
226 6
            $this->scratch = rtrim(sys_get_temp_dir(), DS) . DS . bin2hex(random_bytes(6)) . DS;
227 6
            mkdir($this->scratch, 0o700);
0 ignored issues
show
Bug introduced by
$this->scratch of type null is incompatible with the type string expected by parameter $directory of mkdir(). ( Ignorable by Annotation )

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

227
            mkdir(/** @scrutinizer ignore-type */ $this->scratch, 0o700);
Loading history...
228
            $this->scratch = realpath($this->scratch) ? realpath($this->scratch) . DS
0 ignored issues
show
Bug introduced by
$this->scratch of type null is incompatible with the type string expected by parameter $path of realpath(). ( Ignorable by Annotation )

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

228
            $this->scratch = realpath(/** @scrutinizer ignore-type */ $this->scratch) ? realpath($this->scratch) . DS
Loading history...
229
                : $this->scratch;
230
        }
231
232 6
        return $this->scratch;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->scratch could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
233
    }
234
235
    /**
236
     * Renvoie les erreurs de la dernière opération d'écriture, le cas échéant.
237
     *
238
     * @return array<string,Throwable>
239
     */
240
    final public function getErrors(): array
241
    {
242 8
        return $this->errors;
243
    }
244
245
    /**
246
     * Renvoie les fichiers publiés par la dernière opération d'écriture.
247
     *
248
     * @return list<string>
249
     */
250
    final public function getPublished(): array
251
    {
252 4
        return $this->published;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->published returns the type array which is incompatible with the documented return type BlitzPHP\Publisher\list.
Loading history...
253
    }
254
255
    // --------------------------------------------------------------------
256
    // Gestionnaires supplémentaires
257
    // --------------------------------------------------------------------
258
259
    /**
260
     * Vérifie et ajoute des chemins à la liste.
261
     *
262
     * @param list<string> $paths
263
     */
264
    final public function addPaths(array $paths, bool $recursive = true): static
265
    {
266
        foreach ($paths as $path) {
267 2
            $this->addPath($path, $recursive);
268
        }
269
270 2
        return $this;
271
    }
272
273
    /**
274
     * Ajoute un chemin unique à la liste de fichiers.
275
     */
276
    final public function addPath(string $path, bool $recursive = true): static
277
    {
278 6
        $this->add($this->source . $path, $recursive);
279
280 6
        return $this;
281
    }
282
283
    /**
284
     * Télécharge et met en scène des fichiers à partir d'un tableau d'URI.
285
     *
286
     * @param list<string> $uris
287
     */
288
    final public function addUris(array $uris): static
289
    {
290
        foreach ($uris as $uri) {
291 2
            $this->addUri($uri);
292
        }
293
294 2
        return $this;
295
    }
296
297
    /**
298
     * Télécharge un fichier à partir de l'URI et l'ajoute à la liste des fichiers.
299
     *
300
     * @param string $uri Parce que HTTP\URI est stringable, il sera toujours accepté
301
     */
302
    final public function addUri(string $uri): static
303
    {
304
        // Trouvez un bon nom de fichier (en utilisant des requêtes et des fragments de bandes d'URI)
305 2
        $file = $this->getScratch() . basename((new Uri($uri))->getPath());
306
307
        // Obtenez le contenu et écrivez-le dans l'espace de travail
308 2
        write_file($file, service('httpclient')->get($uri)->body());
309
310 2
        return $this->addFile($file);
311
    }
312
313
    // --------------------------------------------------------------------
314
    // Méthodes d'écriture
315
    // --------------------------------------------------------------------
316
317
    /**
318
     * Supprime la destination et tous ses fichiers et dossiers.
319
     */
320
    final public function wipe(): static
321
    {
322 2
        self::wipeDirectory($this->destination);
323
324 2
        return $this;
325
    }
326
327
    /**
328
     * Copie tous les fichiers dans la destination, ne crée pas de structure de répertoire.
329
     *
330
     * @param bool $replace S'il faut écraser les fichiers existants.
331
     *
332
     * @return bool Si tous les fichiers ont été copiés avec succès
333
     */
334
    final public function copy(bool $replace = true): bool
335
    {
336 4
        $this->errors = $this->published = [];
0 ignored issues
show
Documentation Bug introduced by
It seems like array() of type array is incompatible with the declared type BlitzPHP\Publisher\list of property $published.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
337
338
        foreach ($this->get() as $file) {
339 4
            $to = $this->destination . basename($file);
340
341
            try {
342 4
                $this->safeCopyFile($file, $to, $replace);
343 2
                $this->published[] = $to;
344
            } catch (Throwable $e) {
345 4
                $this->errors[$file] = $e;
346
            }
347
        }
348
349 4
        return $this->errors === [];
350
    }
351
352
    /**
353
     * Fusionne tous les fichiers dans la destination.
354
     * Crée une structure de répertoires en miroir uniquement pour les fichiers de la source.
355
     *
356
     * @param bool $replace Indique s'il faut écraser les fichiers existants.
357
     *
358
     * @return bool Si tous les fichiers ont été copiés avec succès
359
     */
360
    final public function merge(bool $replace = true): bool
361
    {
362 4
        $this->errors = $this->published = [];
0 ignored issues
show
Documentation Bug introduced by
It seems like array() of type array is incompatible with the declared type BlitzPHP\Publisher\list of property $published.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
363
364
        // Obtenez les fichiers de la source pour un traitement spécial
365 4
        $sourced = self::filterFiles($this->get(), $this->source);
366
367
        // Obtenez les fichiers de la source pour un traitement spécial
368 4
        $this->files = array_diff($this->files, $sourced);
369 4
        $this->copy($replace);
370
371
        // Copiez chaque fichier source vers sa destination relative
372
        foreach ($sourced as $file) {
373
            // Résoudre le chemin de destination
374 2
            $to = $this->destination . substr($file, strlen($this->source));
375
376
            try {
377 2
                $this->safeCopyFile($file, $to, $replace);
378 2
                $this->published[] = $to;
379
            } catch (Throwable $e) {
380 2
                $this->errors[$file] = $e;
381
            }
382
        }
383
384 4
        return $this->errors === [];
385
    }
386
387
    /**
388
     * Remplacer le contenu
389
     *
390
     * @param array $replaces [search => replace]
391
     */
392
    public function replace(string $file, array $replaces): bool
393
    {
394 2
        $this->verifyAllowed($file, $file);
395
396 2
        $content = file_get_contents($file);
397
398 2
        $newContent = $this->replacer->replace($content, $replaces);
399
400 2
        $return = file_put_contents($file, $newContent);
401
402 2
        return $return !== false;
403
    }
404
405
    /**
406
     * Ajouter une ligne après la ligne avec la chaîne
407
     *
408
     * @param string $after Chaîne à rechercher.
409
     */
410
    public function addLineAfter(string $file, string $line, string $after): bool
411
    {
412 2
        $this->verifyAllowed($file, $file);
413
414 2
        $content = file_get_contents($file);
415
416 2
        $result = $this->replacer->addAfter($content, $line, $after);
417
418
        if ($result !== null) {
419 2
            $return = file_put_contents($file, $result);
420
421 2
            return $return !== false;
422
        }
423
424
        return false;
425
    }
426
427
    /**
428
     * Ajouter une ligne avant la ligne avec la chaîne
429
     *
430
     * @param string $before String à rechercher.
431
     */
432
    public function addLineBefore(string $file, string $line, string $before): bool
433
    {
434 2
        $this->verifyAllowed($file, $file);
435
436 2
        $content = file_get_contents($file);
437
438 2
        $result = $this->replacer->addBefore($content, $line, $before);
439
440
        if ($result !== null) {
441 2
            $return = file_put_contents($file, $result);
442
443 2
            return $return !== false;
444
        }
445
446
        return false;
447
    }
448
449
    /**
450
     * Vérifiez qu'il s'agit d'un fichier autorisé pour sa destination
451
     */
452
    private function verifyAllowed(string $from, string $to)
453
    {
454
        // Vérifiez qu'il s'agit d'un fichier autorisé pour sa destination
455
        foreach ($this->restrictions as $directory => $pattern) {
456
            if (str_starts_with($to, $directory) && self::matchFiles([$to], $pattern) === []) {
457 2
                throw PublisherException::fileNotAllowed($from, $directory, $pattern);
458
            }
459
        }
460
    }
461
462
    /**
463
     * Copie un fichier avec création de répertoire et reconnaissance de fichier identique.
464
     * Permet intentionnellement des erreurs.
465
     *
466
     * @throws PublisherException Pour les collisions et les violations de restriction
467
     */
468
    private function safeCopyFile(string $from, string $to, bool $replace): void
469
    {
470
        // Vérifiez qu'il s'agit d'un fichier autorisé pour sa destination
471 4
        $this->verifyAllowed($from, $to);
472
473
        // Rechercher un fichier existant
474
        if (file_exists($to)) {
475
            // S'il n'est pas remplacé ou si les fichiers sont identiques, envisagez de réussir
476
            if (! $replace || same_file($from, $to)) {
477 2
                return;
478
            }
479
480
            // S'il s'agit d'un répertoire, n'essayez pas de le supprimer
481
            if (is_dir($to)) {
482 2
                throw PublisherException::collision($from, $to);
483
            }
484
485
            // Essayez de supprimer autre chose
486 2
            unlink($to);
487
        }
488
489
        // Assurez-vous que le répertoire existe
490
        if (! is_dir($directory = pathinfo($to, PATHINFO_DIRNAME))) {
0 ignored issues
show
Bug introduced by
It seems like $directory = pathinfo($t...isher\PATHINFO_DIRNAME) can also be of type array; however, parameter $filename of is_dir() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

490
        if (! is_dir(/** @scrutinizer ignore-type */ $directory = pathinfo($to, PATHINFO_DIRNAME))) {
Loading history...
491 2
            mkdir($directory, 0o775, true);
0 ignored issues
show
Bug introduced by
It seems like $directory can also be of type array; however, parameter $directory of mkdir() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

491
            mkdir(/** @scrutinizer ignore-type */ $directory, 0o775, true);
Loading history...
492
        }
493
494
        // Autoriser copy() à générer des erreurs
495 2
        copy($from, $to);
496
    }
497
}
498