Passed
Pull Request — master (#282)
by Théo
02:30
created

Configuration::getRecommendations()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the box project.
7
 *
8
 * (c) Kevin Herrera <[email protected]>
9
 *     Théo Fidry <[email protected]>
10
 *
11
 * This source file is subject to the MIT license that is bundled
12
 * with this source code in the file LICENSE.
13
 */
14
15
namespace KevinGH\Box;
16
17
use Assert\Assertion;
18
use Closure;
19
use DateTimeImmutable;
20
use DateTimeZone;
21
use Herrera\Annotations\Tokenizer;
22
use Herrera\Box\Compactor\Php as LegacyPhp;
23
use Humbug\PhpScoper\Configuration as PhpScoperConfiguration;
24
use Humbug\PhpScoper\Console\ApplicationFactory;
25
use Humbug\PhpScoper\Scoper;
26
use InvalidArgumentException;
27
use KevinGH\Box\Compactor\Php;
28
use KevinGH\Box\Compactor\PhpScoper as PhpScoperCompactor;
29
use KevinGH\Box\Composer\ComposerConfiguration;
30
use KevinGH\Box\Json\Json;
31
use KevinGH\Box\PhpScoper\SimpleScoper;
32
use Phar;
33
use RuntimeException;
34
use Seld\JsonLint\ParsingException;
35
use SplFileInfo;
36
use stdClass;
37
use Symfony\Component\Finder\Finder;
38
use Symfony\Component\Process\Process;
39
use const E_USER_DEPRECATED;
40
use function array_column;
41
use function array_diff;
42
use function array_filter;
43
use function array_key_exists;
44
use function array_keys;
45
use function array_map;
46
use function array_merge;
47
use function array_unique;
48
use function constant;
49
use function defined;
50
use function dirname;
51
use function file_exists;
52
use function in_array;
53
use function intval;
54
use function is_array;
55
use function is_bool;
56
use function is_file;
57
use function is_link;
58
use function is_object;
59
use function is_readable;
60
use function is_string;
61
use function iter\map;
62
use function iter\toArray;
63
use function iter\values;
64
use function KevinGH\Box\FileSystem\canonicalize;
65
use function KevinGH\Box\FileSystem\file_contents;
66
use function KevinGH\Box\FileSystem\is_absolute_path;
67
use function KevinGH\Box\FileSystem\longest_common_base_path;
68
use function KevinGH\Box\FileSystem\make_path_absolute;
69
use function KevinGH\Box\FileSystem\make_path_relative;
70
use function preg_match;
71
use function sprintf;
72
use function strtoupper;
73
use function substr;
74
use function trigger_error;
75
use function uniqid;
76
77
/**
78
 * @private
79
 */
80
final class Configuration
81
{
82
    private const DEFAULT_ALIAS = 'test.phar';
83
    private const DEFAULT_MAIN_SCRIPT = 'index.php';
84
    private const DEFAULT_DATETIME_FORMAT = 'Y-m-d H:i:s';
85
    private const DEFAULT_REPLACEMENT_SIGIL = '@';
86
    private const DEFAULT_SHEBANG = '#!/usr/bin/env php';
87
    private const DEFAULT_BANNER = <<<'BANNER'
88
Generated by Humbug Box.
89
90
@link https://github.com/humbug/box
91
BANNER;
92
    private const FILES_SETTINGS = [
93
        'directories',
94
        'finder',
95
    ];
96
    private const PHP_SCOPER_CONFIG = 'scoper.inc.php';
97
    private const DEFAULT_SIGNING_ALGORITHM = Phar::SHA1;
98
99
    private $file;
100
    private $fileMode;
101
    private $alias;
102
    private $basePath;
103
    private $composerJson;
104
    private $composerLock;
105
    private $files;
106
    private $binaryFiles;
107
    private $autodiscoveredFiles;
108
    private $dumpAutoload;
109
    private $excludeComposerFiles;
110
    private $compactors;
111
    private $compressionAlgorithm;
112
    private $mainScriptPath;
113
    private $mainScriptContents;
114
    private $map;
115
    private $fileMapper;
116
    private $metadata;
117
    private $tmpOutputPath;
118
    private $outputPath;
119
    private $privateKeyPassphrase;
120
    private $privateKeyPath;
121
    private $promptForPrivateKey;
122
    private $processedReplacements;
123
    private $shebang;
124
    private $signingAlgorithm;
125
    private $stubBannerContents;
126
    private $stubBannerPath;
127
    private $stubPath;
128
    private $isInterceptFileFuncs;
129
    private $isStubGenerated;
130
    private $checkRequirements;
131
    private $warnings;
132
    private $recommendations;
133
134
    public static function create(?string $file, stdClass $raw): self
135
    {
136
        $logger = new ConfigurationLogger();
137
138
        $alias = self::retrieveAlias($raw);
139
140
        $basePath = self::retrieveBasePath($file, $raw);
141
142
        $composerFiles = self::retrieveComposerFiles($basePath);
143
144
        $mainScriptPath = self::retrieveMainScriptPath($raw, $basePath, $composerFiles[0][1]);
145
        $mainScriptContents = self::retrieveMainScriptContents($mainScriptPath);
146
147
        [$tmpOutputPath, $outputPath] = self::retrieveOutputPath($raw, $basePath, $mainScriptPath);
148
149
        $composerJson = $composerFiles[0];
150
        $composerLock = $composerFiles[1];
151
152
        $devPackages = ComposerConfiguration::retrieveDevPackages($basePath, $composerJson[1], $composerLock[1]);
153
154
        /**
155
         * @var string[]
156
         * @var Closure  $blacklistFilter
157
         */
158
        [$excludedPaths, $blacklistFilter] = self::retrieveBlacklistFilter($raw, $basePath, $tmpOutputPath, $outputPath, $mainScriptPath);
159
160
        $autodiscoverFiles = self::autodiscoverFiles($file, $raw);
161
        $forceFilesAutodiscovery = self::retrieveForceFilesAutodiscovery($raw);
162
163
        $filesAggregate = self::collectFiles(
164
            $raw,
165
            $basePath,
166
            $mainScriptPath,
167
            $blacklistFilter,
168
            $excludedPaths,
169
            $devPackages,
170
            $composerFiles,
171
            $composerJson,
172
            $autodiscoverFiles,
173
            $forceFilesAutodiscovery
174
        );
175
        $binaryFilesAggregate = self::collectBinaryFiles(
176
            $raw,
177
            $basePath,
178
            $mainScriptPath,
179
            $blacklistFilter,
180
            $excludedPaths,
181
            $devPackages
182
        );
183
184
        $dumpAutoload = self::retrieveDumpAutoload($raw, null !== $composerJson[0], $logger);
185
186
        $excludeComposerFiles = self::retrieveExcludeComposerFiles($raw);
187
188
        $compactors = self::retrieveCompactors($raw, $basePath);
189
        $compressionAlgorithm = self::retrieveCompressionAlgorithm($raw);
190
191
        $fileMode = self::retrieveFileMode($raw);
192
193
        $map = self::retrieveMap($raw);
194
        $fileMapper = new MapFile($basePath, $map);
195
196
        $metadata = self::retrieveMetadata($raw);
197
198
        $signingAlgorithm = self::retrieveSigningAlgorithm($raw);
199
        $promptForPrivateKey = self::retrievePromptForPrivateKey($raw, $signingAlgorithm, $logger);
200
        $privateKeyPath = self::retrievePrivateKeyPath($raw, $basePath, $signingAlgorithm, $logger);
201
        $privateKeyPassphrase = self::retrievePrivateKeyPassphrase($raw, $signingAlgorithm, $logger);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $privateKeyPassphrase is correct as self::retrievePrivateKey...ningAlgorithm, $logger) targeting KevinGH\Box\Configuratio...ePrivateKeyPassphrase() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
202
203
        $replacements = self::retrieveReplacements($raw, $file, $logger);
0 ignored issues
show
Bug introduced by
$logger of type KevinGH\Box\ConfigurationLogger is incompatible with the type array expected by parameter $messages of KevinGH\Box\Configuration::retrieveReplacements(). ( Ignorable by Annotation )

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

203
        $replacements = self::retrieveReplacements($raw, $file, /** @scrutinizer ignore-type */ $logger);
Loading history...
204
205
        $shebang = self::retrieveShebang($raw);
206
207
        $stubBannerContents = self::retrieveStubBannerContents($raw);
208
        $stubBannerPath = self::retrieveStubBannerPath($raw, $basePath);
209
210
        if (null !== $stubBannerPath) {
211
            $stubBannerContents = file_contents($stubBannerPath);
212
        }
213
214
        $stubBannerContents = self::normalizeStubBannerContents($stubBannerContents);
215
216
        $stubPath = self::retrieveStubPath($raw, $basePath);
217
218
        $isInterceptFileFuncs = self::retrieveIsInterceptFileFuncs($raw);
219
        $isStubGenerated = self::retrieveIsStubGenerated($raw, $stubPath);
220
221
        $checkRequirements = self::retrieveCheckRequirements(
222
            $raw,
223
            null !== $composerJson[0],
224
            null !== $composerLock[0],
225
            $logger
226
        );
227
228
        return new self(
229
            $file,
230
            $alias,
231
            $basePath,
232
            $composerJson,
233
            $composerLock,
234
            $filesAggregate,
235
            $binaryFilesAggregate,
236
            $autodiscoverFiles || $forceFilesAutodiscovery,
237
            $dumpAutoload,
238
            $excludeComposerFiles,
239
            $compactors,
240
            $compressionAlgorithm,
241
            $fileMode,
242
            $mainScriptPath,
243
            $mainScriptContents,
244
            $fileMapper,
245
            $metadata,
246
            $tmpOutputPath,
247
            $outputPath,
248
            $privateKeyPassphrase,
249
            $privateKeyPath,
250
            $promptForPrivateKey,
251
            $replacements,
252
            $shebang,
253
            $signingAlgorithm,
254
            $stubBannerContents,
255
            $stubBannerPath,
256
            $stubPath,
257
            $isInterceptFileFuncs,
258
            $isStubGenerated,
259
            $checkRequirements,
260
            $logger->getWarnings(),
261
            $logger->getRecommendations()
262
        );
263
    }
264
265
    /**
266
     * @param null|string   $file
267
     * @param null|string   $alias
268
     * @param string        $basePath             Utility to private the base path used and be able to retrieve a
269
     *                                            path relative to it (the base path)
270
     * @param array         $composerJson         The first element is the path to the `composer.json` file as a
271
     *                                            string and the second element its decoded contents as an
272
     *                                            associative array.
273
     * @param array         $composerLock         The first element is the path to the `composer.lock` file as a
274
     *                                            string and the second element its decoded contents as an
275
     *                                            associative array.
276
     * @param SplFileInfo[] $files                List of files
277
     * @param SplFileInfo[] $binaryFiles          List of binary files
278
     * @param bool          $dumpAutoload         Whether or not the Composer autoloader should be dumped
279
     * @param bool          $excludeComposerFiles Whether or not the Composer files composer.json, composer.lock and
280
     *                                            installed.json should be removed from the PHAR
281
     * @param Compactor[]   $compactors           List of file contents compactors
282
     * @param null|int      $compressionAlgorithm Compression algorithm constant value. See the \Phar class constants
283
     * @param null|int      $fileMode             File mode in octal form
284
     * @param string        $mainScriptPath       The main script file path
285
     * @param string        $mainScriptContents   The processed content of the main script file
286
     * @param MapFile       $fileMapper           Utility to map the files from outside and inside the PHAR
287
     * @param mixed         $metadata             The PHAR Metadata
288
     * @param bool          $promptForPrivateKey  If the user should be prompted for the private key passphrase
289
     * @param scalar[]      $replacements         The processed list of replacement placeholders and their values
290
     * @param null|string   $shebang              The shebang line
291
     * @param int           $signingAlgorithm     The PHAR siging algorithm. See \Phar constants
292
     * @param null|string   $stubBannerContents   The stub banner comment
293
     * @param null|string   $stubBannerPath       The path to the stub banner comment file
294
     * @param null|string   $stubPath             The PHAR stub file path
295
     * @param bool          $isInterceptFileFuncs Whether or not Phar::interceptFileFuncs() should be used
296
     * @param bool          $isStubGenerated      Whether or not if the PHAR stub should be generated
297
     * @param bool          $checkRequirements    Whether the PHAR will check the application requirements before
298
     *                                            running
299
     * @param string[]      $warnings
300
     * @param string[]      $recommendations
301
     */
302
    private function __construct(
303
        ?string $file,
304
        string $alias,
305
        string $basePath,
306
        array $composerJson,
307
        array $composerLock,
308
        array $files,
309
        array $binaryFiles,
310
        bool $autodiscoveredFiles,
311
        bool $dumpAutoload,
312
        bool $excludeComposerFiles,
313
        array $compactors,
314
        ?int $compressionAlgorithm,
315
        ?int $fileMode,
316
        ?string $mainScriptPath,
317
        ?string $mainScriptContents,
318
        MapFile $fileMapper,
319
        $metadata,
320
        string $tmpOutputPath,
321
        string $outputPath,
322
        ?string $privateKeyPassphrase,
323
        ?string $privateKeyPath,
324
        bool $promptForPrivateKey,
325
        array $replacements,
326
        ?string $shebang,
327
        int $signingAlgorithm,
328
        ?string $stubBannerContents,
329
        ?string $stubBannerPath,
330
        ?string $stubPath,
331
        bool $isInterceptFileFuncs,
332
        bool $isStubGenerated,
333
        bool $checkRequirements,
334
        array $warnings,
335
        array $recommendations
336
    ) {
337
        Assertion::nullOrInArray(
338
            $compressionAlgorithm,
339
            get_phar_compression_algorithms(),
340
            sprintf(
341
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
342
                implode('", "', array_keys(get_phar_compression_algorithms()))
343
            )
344
        );
345
346
        if (null === $mainScriptPath) {
347
            Assertion::null($mainScriptContents);
348
        } else {
349
            Assertion::notNull($mainScriptContents);
350
        }
351
352
        $this->file = $file;
353
        $this->alias = $alias;
354
        $this->basePath = $basePath;
355
        $this->composerJson = $composerJson;
356
        $this->composerLock = $composerLock;
357
        $this->files = $files;
358
        $this->binaryFiles = $binaryFiles;
359
        $this->autodiscoveredFiles = $autodiscoveredFiles;
360
        $this->dumpAutoload = $dumpAutoload;
361
        $this->excludeComposerFiles = $excludeComposerFiles;
362
        $this->compactors = $compactors;
363
        $this->compressionAlgorithm = $compressionAlgorithm;
364
        $this->fileMode = $fileMode;
365
        $this->mainScriptPath = $mainScriptPath;
366
        $this->mainScriptContents = $mainScriptContents;
367
        $this->fileMapper = $fileMapper;
368
        $this->metadata = $metadata;
369
        $this->tmpOutputPath = $tmpOutputPath;
370
        $this->outputPath = $outputPath;
371
        $this->privateKeyPassphrase = $privateKeyPassphrase;
372
        $this->privateKeyPath = $privateKeyPath;
373
        $this->promptForPrivateKey = $promptForPrivateKey;
374
        $this->processedReplacements = $replacements;
375
        $this->shebang = $shebang;
376
        $this->signingAlgorithm = $signingAlgorithm;
377
        $this->stubBannerContents = $stubBannerContents;
378
        $this->stubBannerPath = $stubBannerPath;
379
        $this->stubPath = $stubPath;
380
        $this->isInterceptFileFuncs = $isInterceptFileFuncs;
381
        $this->isStubGenerated = $isStubGenerated;
382
        $this->checkRequirements = $checkRequirements;
383
        $this->warnings = $warnings;
384
        $this->recommendations = $recommendations;
385
    }
386
387
    public function getConfigurationFile(): ?string
388
    {
389
        return $this->file;
390
    }
391
392
    public function getAlias(): string
393
    {
394
        return $this->alias;
395
    }
396
397
    public function getBasePath(): string
398
    {
399
        return $this->basePath;
400
    }
401
402
    public function getComposerJson(): ?string
403
    {
404
        return $this->composerJson[0];
405
    }
406
407
    public function getDecodedComposerJsonContents(): ?array
408
    {
409
        return $this->composerJson[1];
410
    }
411
412
    public function getComposerLock(): ?string
413
    {
414
        return $this->composerLock[0];
415
    }
416
417
    public function getDecodedComposerLockContents(): ?array
418
    {
419
        return $this->composerLock[1];
420
    }
421
422
    /**
423
     * @return string[]
424
     */
425
    public function getFiles(): array
426
    {
427
        return $this->files;
428
    }
429
430
    /**
431
     * @return string[]
432
     */
433
    public function getBinaryFiles(): array
434
    {
435
        return $this->binaryFiles;
436
    }
437
438
    public function hasAutodiscoveredFiles(): bool
439
    {
440
        return $this->autodiscoveredFiles;
441
    }
442
443
    public function dumpAutoload(): bool
444
    {
445
        return $this->dumpAutoload;
446
    }
447
448
    public function excludeComposerFiles(): bool
449
    {
450
        return $this->excludeComposerFiles;
451
    }
452
453
    /**
454
     * @return Compactor[] the list of compactors
455
     */
456
    public function getCompactors(): array
457
    {
458
        return $this->compactors;
459
    }
460
461
    public function getCompressionAlgorithm(): ?int
462
    {
463
        return $this->compressionAlgorithm;
464
    }
465
466
    public function getFileMode(): ?int
467
    {
468
        return $this->fileMode;
469
    }
470
471
    public function hasMainScript(): bool
472
    {
473
        return null !== $this->mainScriptPath;
474
    }
475
476
    public function getMainScriptPath(): string
477
    {
478
        Assertion::notNull(
479
            $this->mainScriptPath,
480
            'Cannot retrieve the main script path: no main script configured.'
481
        );
482
483
        return $this->mainScriptPath;
484
    }
485
486
    public function getMainScriptContents(): string
487
    {
488
        Assertion::notNull(
489
            $this->mainScriptPath,
490
            'Cannot retrieve the main script contents: no main script configured.'
491
        );
492
493
        return $this->mainScriptContents;
494
    }
495
496
    public function checkRequirements(): bool
497
    {
498
        return $this->checkRequirements;
499
    }
500
501
    public function getTmpOutputPath(): string
502
    {
503
        return $this->tmpOutputPath;
504
    }
505
506
    public function getOutputPath(): string
507
    {
508
        return $this->outputPath;
509
    }
510
511
    public function getFileMapper(): MapFile
512
    {
513
        return $this->fileMapper;
514
    }
515
516
    /**
517
     * @return mixed
518
     */
519
    public function getMetadata()
520
    {
521
        return $this->metadata;
522
    }
523
524
    public function getPrivateKeyPassphrase(): ?string
525
    {
526
        return $this->privateKeyPassphrase;
527
    }
528
529
    public function getPrivateKeyPath(): ?string
530
    {
531
        return $this->privateKeyPath;
532
    }
533
534
    /**
535
     * @deprecated Use promptForPrivateKey() instead
536
     */
537
    public function isPrivateKeyPrompt(): bool
538
    {
539
        return $this->promptForPrivateKey;
540
    }
541
542
    public function promptForPrivateKey(): bool
543
    {
544
        return $this->promptForPrivateKey;
545
    }
546
547
    /**
548
     * @return scalar[]
549
     */
550
    public function getReplacements(): array
551
    {
552
        return $this->processedReplacements;
553
    }
554
555
    public function getShebang(): ?string
556
    {
557
        return $this->shebang;
558
    }
559
560
    public function getSigningAlgorithm(): int
561
    {
562
        return $this->signingAlgorithm;
563
    }
564
565
    public function getStubBannerContents(): ?string
566
    {
567
        return $this->stubBannerContents;
568
    }
569
570
    public function getStubBannerPath(): ?string
571
    {
572
        return $this->stubBannerPath;
573
    }
574
575
    public function getStubPath(): ?string
576
    {
577
        return $this->stubPath;
578
    }
579
580
    public function isInterceptFileFuncs(): bool
581
    {
582
        return $this->isInterceptFileFuncs;
583
    }
584
585
    public function isStubGenerated(): bool
586
    {
587
        return $this->isStubGenerated;
588
    }
589
590
    /**
591
     * @return string[]
592
     */
593
    public function getWarnings(): array
594
    {
595
        return $this->warnings;
596
    }
597
598
    /**
599
     * @return string[]
600
     */
601
    public function getRecommendations(): array
602
    {
603
        return $this->recommendations;
604
    }
605
606
    private static function retrieveAlias(stdClass $raw): string
607
    {
608
        if (false === isset($raw->alias)) {
609
            return uniqid('box-auto-generated-alias-', false).'.phar';
610
        }
611
612
        $alias = trim($raw->alias);
613
614
        Assertion::notEmpty($alias, 'A PHAR alias cannot be empty when provided.');
615
616
        return $alias;
617
    }
618
619
    private static function retrieveBasePath(?string $file, stdClass $raw): string
620
    {
621
        if (null === $file) {
622
            return getcwd();
623
        }
624
625
        if (false === isset($raw->{'base-path'})) {
626
            return realpath(dirname($file));
627
        }
628
629
        $basePath = trim($raw->{'base-path'});
630
631
        Assertion::directory(
632
            $basePath,
633
            'The base path "%s" is not a directory or does not exist.'
634
        );
635
636
        return realpath($basePath);
637
    }
638
639
    private static function autodiscoverFiles(?string $file, stdClass $raw): bool
640
    {
641
        if (null === $file) {
642
            return true;
643
        }
644
645
        // TODO: config should be casted into an array: it is easier to do and we need an array in several places now
646
        $rawConfig = (array) $raw;
647
648
        return self::FILES_SETTINGS === array_diff(self::FILES_SETTINGS, array_keys($rawConfig));
649
    }
650
651
    private static function retrieveForceFilesAutodiscovery(stdClass $raw): bool
652
    {
653
        return $raw->{'auto-discovery'} ?? false;
654
    }
655
656
    private static function retrieveBlacklistFilter(stdClass $raw, string $basePath, ?string ...$excludedPaths): array
657
    {
658
        $blacklist = self::retrieveBlacklist($raw, $basePath, ...$excludedPaths);
659
660
        $blacklistFilter = function (SplFileInfo $file) use ($blacklist): ?bool {
661
            if ($file->isLink()) {
662
                return false;
663
            }
664
665
            if (false === $file->getRealPath()) {
666
                return false;
667
            }
668
669
            if (in_array($file->getRealPath(), $blacklist, true)) {
670
                return false;
671
            }
672
673
            return null;
674
        };
675
676
        return [$blacklist, $blacklistFilter];
677
    }
678
679
    /**
680
     * @param stdClass        $raw
681
     * @param string          $basePath
682
     * @param null[]|string[] $excludedPaths
683
     *
684
     * @return string[]
685
     */
686
    private static function retrieveBlacklist(stdClass $raw, string $basePath, ?string ...$excludedPaths): array
687
    {
688
        /** @var string[] $blacklist */
689
        $blacklist = array_merge(
690
            array_filter($excludedPaths),
691
            $raw->blacklist ?? []
692
        );
693
694
        $normalizedBlacklist = [];
695
696
        foreach ($blacklist as $file) {
697
            $normalizedBlacklist[] = self::normalizePath($file, $basePath);
698
            $normalizedBlacklist[] = canonicalize(make_path_relative(trim($file), $basePath));
699
        }
700
701
        return array_unique($normalizedBlacklist);
702
    }
703
704
    /**
705
     * @param string[] $excludedPaths
706
     * @param string[] $devPackages
707
     *
708
     * @return SplFileInfo[]
709
     */
710
    private static function collectFiles(
711
        stdClass $raw,
712
        string $basePath,
713
        ?string $mainScriptPath,
714
        Closure $blacklistFilter,
715
        array $excludedPaths,
716
        array $devPackages,
717
        array $composerFiles,
718
        array $composerJson,
719
        bool $autodiscoverFiles,
720
        bool $forceFilesAutodiscovery
721
    ): array {
722
        $files = [self::retrieveFiles($raw, 'files', $basePath, $composerFiles, $mainScriptPath)];
723
724
        if ($autodiscoverFiles || $forceFilesAutodiscovery) {
725
            [$filesToAppend, $directories] = self::retrieveAllDirectoriesToInclude(
726
                $basePath,
727
                $composerJson[1],
728
                $devPackages,
729
                array_filter(
730
                    array_column($composerFiles, 0)
731
                ),
732
                $excludedPaths
733
            );
734
735
            $files[] = $filesToAppend;
736
737
            $files[] = self::retrieveAllFiles(
738
                $basePath,
739
                $directories,
740
                $mainScriptPath,
741
                $blacklistFilter,
742
                $excludedPaths,
743
                $devPackages
744
            );
745
        }
746
747
        if (false === $autodiscoverFiles) {
748
            $files[] = self::retrieveDirectories($raw, 'directories', $basePath, $blacklistFilter, $excludedPaths);
749
750
            $filesFromFinders = self::retrieveFilesFromFinders($raw, 'finder', $basePath, $blacklistFilter, $devPackages);
751
752
            foreach ($filesFromFinders as $filesFromFinder) {
753
                // Avoid an array_merge here as it can be quite expansive at this stage depending of the number of files
754
                $files[] = $filesFromFinder;
755
            }
756
        }
757
758
        return self::retrieveFilesAggregate(...$files);
759
    }
760
761
    /**
762
     * @param string[] $excludedPaths
763
     * @param string[] $devPackages
764
     *
765
     * @return SplFileInfo[]
766
     */
767
    private static function collectBinaryFiles(
768
        stdClass $raw,
769
        string $basePath,
770
        ?string $mainScriptPath,
771
        Closure $blacklistFilter,
772
        array $excludedPaths,
773
        array $devPackages
774
    ): array {
775
        $binaryFiles = self::retrieveFiles($raw, 'files-bin', $basePath, [], $mainScriptPath);
776
        $binaryDirectories = self::retrieveDirectories($raw, 'directories-bin', $basePath, $blacklistFilter, $excludedPaths);
777
        $binaryFilesFromFinders = self::retrieveFilesFromFinders($raw, 'finder-bin', $basePath, $blacklistFilter, $devPackages);
778
779
        return self::retrieveFilesAggregate($binaryFiles, $binaryDirectories, ...$binaryFilesFromFinders);
780
    }
781
782
    /**
783
     * @return SplFileInfo[]
784
     */
785
    private static function retrieveFiles(
786
        stdClass $raw,
787
        string $key,
788
        string $basePath,
789
        array $composerFiles,
790
        ?string $mainScriptPath
791
    ): array {
792
        $files = [];
793
794
        if (isset($composerFiles[0][0])) {
795
            $files[] = $composerFiles[0][0];
796
        }
797
798
        if (isset($composerFiles[1][1])) {
799
            $files[] = $composerFiles[1][0];
800
        }
801
802
        if (false === isset($raw->{$key})) {
803
            return $files;
804
        }
805
806
        $files = array_merge((array) $raw->{$key}, $files);
807
808
        Assertion::allString($files);
809
810
        $normalizePath = function (string $file) use ($basePath, $key, $mainScriptPath): ?SplFileInfo {
811
            $file = self::normalizePath($file, $basePath);
812
813
            if (is_link($file)) {
814
                // TODO: add this to baberlei/assert
815
                throw new InvalidArgumentException(
816
                    sprintf(
817
                        'Cannot add the link "%s": links are not supported.',
818
                        $file
819
                    )
820
                );
821
            }
822
823
            Assertion::file(
824
                $file,
825
                sprintf(
826
                    '"%s" must contain a list of existing files. Could not find "%%s".',
827
                    $key
828
                )
829
            );
830
831
            return $mainScriptPath === $file ? null : new SplFileInfo($file);
832
        };
833
834
        return array_filter(array_map($normalizePath, $files));
835
    }
836
837
    /**
838
     * @param string   $key           Config property name
839
     * @param string[] $excludedPaths
840
     *
841
     * @return iterable|SplFileInfo[]
842
     */
843
    private static function retrieveDirectories(
844
        stdClass $raw,
845
        string $key,
846
        string $basePath,
847
        Closure $blacklistFilter,
848
        array $excludedPaths
849
    ): iterable {
850
        $directories = self::retrieveDirectoryPaths($raw, $key, $basePath);
851
852
        if ([] !== $directories) {
853
            $finder = Finder::create()
854
                ->files()
855
                ->filter($blacklistFilter)
856
                ->ignoreVCS(true)
857
                ->in($directories)
858
            ;
859
860
            foreach ($excludedPaths as $excludedPath) {
861
                $finder->notPath($excludedPath);
862
            }
863
864
            return $finder;
865
        }
866
867
        return [];
868
    }
869
870
    /**
871
     * @param string[] $devPackages
872
     *
873
     * @return iterable[]|SplFileInfo[][]
874
     */
875
    private static function retrieveFilesFromFinders(
876
        stdClass $raw,
877
        string $key,
878
        string $basePath,
879
        Closure $blacklistFilter,
880
        array $devPackages
881
    ): array {
882
        if (isset($raw->{$key})) {
883
            return self::processFinders($raw->{$key}, $basePath, $blacklistFilter, $devPackages);
884
        }
885
886
        return [];
887
    }
888
889
    /**
890
     * @param iterable[]|SplFileInfo[][] $fileIterators
891
     *
892
     * @return SplFileInfo[]
893
     */
894
    private static function retrieveFilesAggregate(iterable ...$fileIterators): array
895
    {
896
        $files = [];
897
898
        foreach ($fileIterators as $fileIterator) {
899
            foreach ($fileIterator as $file) {
900
                $files[(string) $file] = $file;
901
            }
902
        }
903
904
        return array_values($files);
905
    }
906
907
    /**
908
     * @param string[] $devPackages
909
     *
910
     * @return Finder[]|SplFileInfo[][]
911
     */
912
    private static function processFinders(
913
        array $findersConfig,
914
        string $basePath,
915
        Closure $blacklistFilter,
916
        array $devPackages
917
    ): array {
918
        $processFinderConfig = function (stdClass $config) use ($basePath, $blacklistFilter, $devPackages) {
919
            return self::processFinder($config, $basePath, $blacklistFilter, $devPackages);
920
        };
921
922
        return array_map($processFinderConfig, $findersConfig);
923
    }
924
925
    /**
926
     * @param string[] $devPackages
927
     *
928
     * @return Finder|SplFileInfo[]
929
     */
930
    private static function processFinder(
931
        stdClass $config,
932
        string $basePath,
933
        Closure $blacklistFilter,
934
        array $devPackages
935
    ): Finder {
936
        $finder = Finder::create()
937
            ->files()
938
            ->filter($blacklistFilter)
939
            ->filter(
940
                function (SplFileInfo $fileInfo) use ($devPackages): bool {
941
                    foreach ($devPackages as $devPackage) {
942
                        if ($devPackage === longest_common_base_path([$devPackage, $fileInfo->getRealPath()])) {
943
                            // File belongs to the dev package
944
                            return false;
945
                        }
946
                    }
947
948
                    return true;
949
                }
950
            )
951
            ->ignoreVCS(true)
952
        ;
953
954
        $normalizedConfig = (function (array $config, Finder $finder): array {
955
            $normalizedConfig = [];
956
957
            foreach ($config as $method => $arguments) {
958
                $method = trim($method);
959
                $arguments = (array) $arguments;
960
961
                Assertion::methodExists(
962
                    $method,
963
                    $finder,
964
                    'The method "Finder::%s" does not exist.'
965
                );
966
967
                $normalizedConfig[$method] = $arguments;
968
            }
969
970
            krsort($normalizedConfig);
971
972
            return $normalizedConfig;
973
        })((array) $config, $finder);
974
975
        $createNormalizedDirectories = function (string $directory) use ($basePath): ?string {
976
            $directory = self::normalizePath($directory, $basePath);
977
978
            if (is_link($directory)) {
979
                // TODO: add this to baberlei/assert
980
                throw new InvalidArgumentException(
981
                    sprintf(
982
                        'Cannot append the link "%s" to the Finder: links are not supported.',
983
                        $directory
984
                    )
985
                );
986
            }
987
988
            Assertion::directory($directory);
989
990
            return $directory;
991
        };
992
993
        $normalizeFileOrDirectory = function (string &$fileOrDirectory) use ($basePath, $blacklistFilter): void {
994
            $fileOrDirectory = self::normalizePath($fileOrDirectory, $basePath);
995
996
            if (is_link($fileOrDirectory)) {
997
                // TODO: add this to baberlei/assert
998
                throw new InvalidArgumentException(
999
                    sprintf(
1000
                        'Cannot append the link "%s" to the Finder: links are not supported.',
1001
                        $fileOrDirectory
1002
                    )
1003
                );
1004
            }
1005
1006
            // TODO: add this to baberlei/assert
1007
            if (false === file_exists($fileOrDirectory)) {
1008
                throw new InvalidArgumentException(
1009
                    sprintf(
1010
                        'Path "%s" was expected to be a file or directory. It may be a symlink (which are unsupported).',
1011
                        $fileOrDirectory
1012
                    )
1013
                );
1014
            }
1015
1016
            // TODO: add fileExists (as file or directory) to Assert
1017
            if (false === is_file($fileOrDirectory)) {
1018
                Assertion::directory($fileOrDirectory);
1019
            } else {
1020
                Assertion::file($fileOrDirectory);
1021
            }
1022
1023
            if (false === $blacklistFilter(new SplFileInfo($fileOrDirectory))) {
1024
                $fileOrDirectory = null;
1025
            }
1026
        };
1027
1028
        foreach ($normalizedConfig as $method => $arguments) {
1029
            if ('in' === $method) {
1030
                $normalizedConfig[$method] = $arguments = array_map($createNormalizedDirectories, $arguments);
1031
            }
1032
1033
            if ('exclude' === $method) {
1034
                $arguments = array_unique(array_map('trim', $arguments));
1035
            }
1036
1037
            if ('append' === $method) {
1038
                array_walk($arguments, $normalizeFileOrDirectory);
1039
1040
                $arguments = [array_filter($arguments)];
1041
            }
1042
1043
            foreach ($arguments as $argument) {
1044
                $finder->$method($argument);
1045
            }
1046
        }
1047
1048
        return $finder;
1049
    }
1050
1051
    /**
1052
     * @param string[] $devPackages
1053
     * @param string[] $filesToAppend
1054
     *
1055
     * @return string[][]
1056
     */
1057
    private static function retrieveAllDirectoriesToInclude(
1058
        string $basePath,
1059
        ?array $decodedJsonContents,
1060
        array $devPackages,
1061
        array $filesToAppend,
1062
        array $excludedPaths
1063
    ): array {
1064
        $toString = function ($file): string {
1065
            // @param string|SplFileInfo $file
1066
            return (string) $file;
1067
        };
1068
1069
        if (null !== $decodedJsonContents && array_key_exists('vendor-dir', $decodedJsonContents)) {
1070
            $vendorDir = self::normalizePath($decodedJsonContents['vendor-dir'], $basePath);
1071
        } else {
1072
            $vendorDir = self::normalizePath('vendor', $basePath);
1073
        }
1074
1075
        if (file_exists($vendorDir)) {
1076
            // The installed.json file is necessary for dumping the autoload correctly. Note however that it will not exists if no
1077
            // dependencies are included in the `composer.json`
1078
            $installedJsonFiles = self::normalizePath($vendorDir.'/composer/installed.json', $basePath);
1079
1080
            if (file_exists($installedJsonFiles)) {
1081
                $filesToAppend[] = $installedJsonFiles;
1082
            }
1083
1084
            $vendorPackages = toArray(values(map(
1085
                $toString,
1086
                Finder::create()
1087
                    ->in($vendorDir)
1088
                    ->directories()
1089
                    ->depth(1)
1090
                    ->ignoreUnreadableDirs()
1091
                    ->filter(
1092
                        function (SplFileInfo $fileInfo): ?bool {
1093
                            if ($fileInfo->isLink()) {
1094
                                return false;
1095
                            }
1096
1097
                            return null;
1098
                        }
1099
                    )
1100
            )));
1101
1102
            $vendorPackages = array_diff($vendorPackages, $devPackages);
1103
1104
            if (null === $decodedJsonContents || false === array_key_exists('autoload', $decodedJsonContents)) {
1105
                $files = toArray(values(map(
1106
                    $toString,
1107
                    Finder::create()
1108
                        ->in($basePath)
1109
                        ->files()
1110
                        ->depth(0)
1111
                )));
1112
1113
                $directories = toArray(values(map(
1114
                    $toString,
1115
                    Finder::create()
1116
                        ->in($basePath)
1117
                        ->notPath('vendor')
1118
                        ->directories()
1119
                        ->depth(0)
1120
                )));
1121
1122
                return [
1123
                    array_merge($files, $filesToAppend),
1124
                    array_merge($directories, $vendorPackages),
1125
                ];
1126
            }
1127
1128
            $paths = $vendorPackages;
1129
        } else {
1130
            $paths = [];
1131
        }
1132
1133
        $autoload = $decodedJsonContents['autoload'] ?? [];
1134
1135
        if (array_key_exists('psr-4', $autoload)) {
1136
            foreach ($autoload['psr-4'] as $path) {
1137
                /** @var string|string[] $path */
1138
                $composerPaths = (array) $path;
1139
1140
                foreach ($composerPaths as $composerPath) {
1141
                    $paths[] = '' !== trim($composerPath) ? $composerPath : $basePath;
1142
                }
1143
            }
1144
        }
1145
1146
        if (array_key_exists('psr-0', $autoload)) {
1147
            foreach ($autoload['psr-0'] as $path) {
1148
                /** @var string|string[] $path */
1149
                $composerPaths = (array) $path;
1150
1151
                foreach ($composerPaths as $composerPath) {
1152
                    if ('' !== trim($composerPath)) {
1153
                        $paths[] = $composerPath;
1154
                    }
1155
                }
1156
            }
1157
        }
1158
1159
        if (array_key_exists('classmap', $autoload)) {
1160
            foreach ($autoload['classmap'] as $path) {
1161
                // @var string $path
1162
                $paths[] = $path;
1163
            }
1164
        }
1165
1166
        $normalizePath = function (string $path) use ($basePath): string {
1167
            return is_absolute_path($path)
1168
                ? canonicalize($path)
1169
                : self::normalizePath(trim($path, '/ '), $basePath)
1170
            ;
1171
        };
1172
1173
        if (array_key_exists('files', $autoload)) {
1174
            foreach ($autoload['files'] as $path) {
1175
                // @var string $path
1176
                $path = $normalizePath($path);
1177
1178
                Assertion::file($path);
1179
                Assertion::false(is_link($path), 'Cannot add the link "'.$path.'": links are not supported.');
1180
1181
                $filesToAppend[] = $path;
1182
            }
1183
        }
1184
1185
        $files = $filesToAppend;
1186
        $directories = [];
1187
1188
        foreach ($paths as $path) {
1189
            $path = $normalizePath($path);
1190
1191
            Assertion::true(file_exists($path), 'File or directory "'.$path.'" was expected to exist.');
1192
            Assertion::false(is_link($path), 'Cannot add the link "'.$path.'": links are not supported.');
1193
1194
            if (is_file($path)) {
1195
                $files[] = $path;
1196
            } else {
1197
                $directories[] = $path;
1198
            }
1199
        }
1200
1201
        [$files, $directories] = [
1202
            array_unique($files),
1203
            array_unique($directories),
1204
        ];
1205
1206
        return [
1207
            array_diff($files, $excludedPaths),
1208
            array_diff($directories, $excludedPaths),
1209
        ];
1210
    }
1211
1212
    /**
1213
     * @param string[] $files
1214
     * @param string[] $directories
1215
     * @param string[] $excludedPaths
1216
     * @param string[] $devPackages
1217
     *
1218
     * @return SplFileInfo[]
1219
     */
1220
    private static function retrieveAllFiles(
1221
        string $basePath,
1222
        array $directories,
1223
        ?string $mainScriptPath,
1224
        Closure $blacklistFilter,
1225
        array $excludedPaths,
1226
        array $devPackages
1227
    ): iterable {
1228
        if ([] === $directories) {
1229
            return [];
1230
        }
1231
1232
        $relativeDevPackages = array_map(
1233
            function (string $packagePath) use ($basePath): string {
1234
                return make_path_relative($packagePath, $basePath);
1235
            },
1236
            $devPackages
1237
        );
1238
1239
        $finder = Finder::create()
1240
            ->files()
1241
            ->filter($blacklistFilter)
1242
            ->exclude($relativeDevPackages)
1243
            ->ignoreVCS(true)
1244
            ->ignoreDotFiles(true)
1245
            // Remove build files
1246
            ->notName('composer.json')
1247
            ->notName('composer.lock')
1248
            ->notName('Makefile')
1249
            ->notName('Vagrantfile')
1250
            ->notName('phpstan*.neon*')
1251
            ->notName('infection*.json*')
1252
            ->notName('humbug*.json*')
1253
            ->notName('easy-coding-standard.neon*')
1254
            ->notName('phpbench.json*')
1255
            ->notName('phpcs.xml*')
1256
            ->notName('psalm.xml*')
1257
            ->notName('scoper.inc*')
1258
            ->notName('box*.json*')
1259
            ->notName('phpdoc*.xml*')
1260
            ->notName('codecov.yml*')
1261
            ->notName('Dockerfile')
1262
            ->exclude('build')
1263
            ->exclude('dist')
1264
            ->exclude('example')
1265
            ->exclude('examples')
1266
            // Remove documentation
1267
            ->notName('*.md')
1268
            ->notName('*.rst')
1269
            ->notName('/^readme(\..*+)?$/i')
1270
            ->notName('/^upgrade(\..*+)?$/i')
1271
            ->notName('/^contributing(\..*+)?$/i')
1272
            ->notName('/^changelog(\..*+)?$/i')
1273
            ->notName('/^authors?(\..*+)?$/i')
1274
            ->notName('/^conduct(\..*+)?$/i')
1275
            ->notName('/^todo(\..*+)?$/i')
1276
            ->exclude('doc')
1277
            ->exclude('docs')
1278
            ->exclude('documentation')
1279
            // Remove backup files
1280
            ->notName('*~')
1281
            ->notName('*.back')
1282
            ->notName('*.swp')
1283
            // Remove tests
1284
            ->notName('*Test.php')
1285
            ->exclude('test')
1286
            ->exclude('Test')
1287
            ->exclude('tests')
1288
            ->exclude('Tests')
1289
            ->notName('/phpunit.*\.xml(.dist)?/')
1290
            ->notName('/behat.*\.yml(.dist)?/')
1291
            ->exclude('spec')
1292
            ->exclude('specs')
1293
            ->exclude('features')
1294
            // Remove CI config
1295
            ->exclude('travis')
1296
            ->notName('travis.yml')
1297
            ->notName('appveyor.yml')
1298
            ->notName('build.xml*')
1299
        ;
1300
1301
        if (null !== $mainScriptPath) {
1302
            $finder->notPath(make_path_relative($mainScriptPath, $basePath));
1303
        }
1304
1305
        $finder->in($directories);
1306
1307
        $excludedPaths = array_unique(
1308
            array_filter(
1309
                array_map(
1310
                    function (string $path) use ($basePath): string {
1311
                        return make_path_relative($path, $basePath);
1312
                    },
1313
                    $excludedPaths
1314
                ),
1315
                function (string $path): bool {
1316
                    return '..' !== substr($path, 0, 2);
1317
                }
1318
            )
1319
        );
1320
1321
        foreach ($excludedPaths as $excludedPath) {
1322
            $finder->notPath($excludedPath);
1323
        }
1324
1325
        return $finder;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $finder returns the type Symfony\Component\Finder\Finder which is incompatible with the documented return type SplFileInfo[].
Loading history...
1326
    }
1327
1328
    /**
1329
     * @param stdClass $raw
1330
     * @param string   $key      Config property name
1331
     * @param string   $basePath
1332
     *
1333
     * @return string[]
1334
     */
1335
    private static function retrieveDirectoryPaths(stdClass $raw, string $key, string $basePath): array
1336
    {
1337
        if (false === isset($raw->{$key})) {
1338
            return [];
1339
        }
1340
1341
        $directories = $raw->{$key};
1342
1343
        $normalizeDirectory = function (string $directory) use ($basePath, $key): string {
1344
            $directory = self::normalizePath($directory, $basePath);
1345
1346
            if (is_link($directory)) {
1347
                // TODO: add this to baberlei/assert
1348
                throw new InvalidArgumentException(
1349
                    sprintf(
1350
                        'Cannot add the link "%s": links are not supported.',
1351
                        $directory
1352
                    )
1353
                );
1354
            }
1355
1356
            Assertion::directory(
1357
                $directory,
1358
                sprintf(
1359
                    '"%s" must contain a list of existing directories. Could not find "%%s".',
1360
                    $key
1361
                )
1362
            );
1363
1364
            return $directory;
1365
        };
1366
1367
        return array_map($normalizeDirectory, $directories);
1368
    }
1369
1370
    private static function normalizePath(string $file, string $basePath): string
1371
    {
1372
        return make_path_absolute(trim($file), $basePath);
1373
    }
1374
1375
    private static function retrieveDumpAutoload(stdClass $raw, bool $composerJson, ConfigurationLogger $logger): bool
1376
    {
1377
        $raw = (array) $raw;
1378
1379
        if (false === array_key_exists('dump-autoload', $raw)) {
1380
            return $composerJson;
1381
        }
1382
1383
        $dumpAutoload = $raw['dump-autoload'];
1384
1385
        if ($dumpAutoload) {
1386
            $logger->addRecommendation(
1387
                'The "dump-autoload" setting has been set but is unnecessary since its value is the default value.'
1388
            );
1389
        }
1390
1391
        if (false === $composerJson && $dumpAutoload) {
1392
            $logger->addWarning(
1393
                'The "dump-autoload" setting has been set but has been ignored because the composer.json file necessary'
1394
                .' for it could not be found'
1395
            );
1396
1397
            return false;
1398
        }
1399
1400
        return $composerJson && false !== $dumpAutoload;
1401
    }
1402
1403
    private static function retrieveExcludeComposerFiles(stdClass $raw): bool
1404
    {
1405
        return $raw->{'exclude-composer-files'} ?? true;
1406
    }
1407
1408
    /**
1409
     * @return Compactor[]
1410
     */
1411
    private static function retrieveCompactors(stdClass $raw, string $basePath): array
1412
    {
1413
        if (false === isset($raw->compactors)) {
1414
            return [];
1415
        }
1416
1417
        $compactorClasses = array_unique((array) $raw->compactors);
1418
1419
        return array_map(
1420
            function (string $class) use ($raw, $basePath): Compactor {
1421
                Assertion::classExists($class, 'The compactor class "%s" does not exist.');
1422
                Assertion::implementsInterface($class, Compactor::class, 'The class "%s" is not a compactor class.');
1423
1424
                if (Php::class === $class || LegacyPhp::class === $class) {
1425
                    return self::createPhpCompactor($raw);
1426
                }
1427
1428
                if (PhpScoperCompactor::class === $class) {
1429
                    $phpScoperConfig = self::retrievePhpScoperConfig($raw, $basePath);
1430
1431
                    $prefix = null === $phpScoperConfig->getPrefix()
1432
                        ? uniqid('_HumbugBox', false)
1433
                        : $phpScoperConfig->getPrefix()
1434
                    ;
1435
1436
                    return new PhpScoperCompactor(
1437
                        new SimpleScoper(
1438
                            (new class() extends ApplicationFactory {
1439
                                public static function createScoper(): Scoper
1440
                                {
1441
                                    return parent::createScoper();
1442
                                }
1443
                            })::createScoper(),
1444
                            $prefix,
1445
                            $phpScoperConfig->getWhitelist(),
1446
                            $phpScoperConfig->getPatchers()
1447
                        )
1448
                    );
1449
                }
1450
1451
                return new $class();
1452
            },
1453
            $compactorClasses
1454
        );
1455
    }
1456
1457
    private static function retrieveCompressionAlgorithm(stdClass $raw): ?int
1458
    {
1459
        if (false === isset($raw->compression)) {
1460
            return null;
1461
        }
1462
1463
        $knownAlgorithmNames = array_keys(get_phar_compression_algorithms());
1464
1465
        Assertion::inArray(
1466
            $raw->compression,
1467
            $knownAlgorithmNames,
1468
            sprintf(
1469
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
1470
                implode('", "', $knownAlgorithmNames)
1471
            )
1472
        );
1473
1474
        $value = get_phar_compression_algorithms()[$raw->compression];
1475
1476
        // Phar::NONE is not valid for compressFiles()
1477
        if (Phar::NONE === $value) {
1478
            return null;
1479
        }
1480
1481
        return $value;
1482
    }
1483
1484
    private static function retrieveFileMode(stdClass $raw): ?int
1485
    {
1486
        if (isset($raw->chmod)) {
1487
            return intval($raw->chmod, 8);
1488
        }
1489
1490
        return intval(0755, 8);
1491
    }
1492
1493
    private static function retrieveMainScriptPath(stdClass $raw, string $basePath, ?array $decodedJsonContents): ?string
1494
    {
1495
        if (isset($raw->main)) {
1496
            $main = $raw->main;
1497
        } else {
1498
            if (null === $decodedJsonContents
1499
                || false === array_key_exists('bin', $decodedJsonContents)
1500
                || false === $main = current((array) $decodedJsonContents['bin'])
1501
            ) {
1502
                $main = self::DEFAULT_MAIN_SCRIPT;
1503
            }
1504
        }
1505
1506
        if (is_bool($main)) {
1507
            Assertion::false(
1508
                $main,
1509
                'Cannot "enable" a main script: either disable it with `false` or give the main script file path.'
1510
            );
1511
1512
            return null;
1513
        }
1514
1515
        return self::normalizePath($main, $basePath);
1516
    }
1517
1518
    private static function retrieveMainScriptContents(?string $mainScriptPath): ?string
1519
    {
1520
        if (null === $mainScriptPath) {
1521
            return null;
1522
        }
1523
1524
        $contents = file_contents($mainScriptPath);
1525
1526
        // Remove the shebang line: the shebang line in a PHAR should be located in the stub file which is the real
1527
        // PHAR entry point file.
1528
        return preg_replace('/^#!.*\s*/', '', $contents);
1529
    }
1530
1531
    /**
1532
     * @return string|null[][]
1533
     */
1534
    private static function retrieveComposerFiles(string $basePath): array
1535
    {
1536
        $retrieveFileAndContents = function (string $file): array {
1537
            $json = new Json();
1538
1539
            if (false === file_exists($file) || false === is_file($file) || false === is_readable($file)) {
1540
                return [null, null];
1541
            }
1542
1543
            try {
1544
                $contents = $json->decodeFile($file, true);
1545
            } catch (ParsingException $exception) {
1546
                throw new InvalidArgumentException(
1547
                    sprintf(
1548
                        'Expected the file "%s" to be a valid composer.json file but an error has been found: %s',
1549
                        $file,
1550
                        $exception->getMessage()
1551
                    ),
1552
                    0,
1553
                    $exception
1554
                );
1555
            }
1556
1557
            return [$file, $contents];
1558
        };
1559
1560
        [$composerJson, $composerJsonContents] = $retrieveFileAndContents(canonicalize($basePath.'/composer.json'));
1561
        [$composerLock, $composerLockContents] = $retrieveFileAndContents(canonicalize($basePath.'/composer.lock'));
1562
1563
        return [
1564
            [$composerJson, $composerJsonContents],
1565
            [$composerLock, $composerLockContents],
1566
        ];
1567
    }
1568
1569
    /**
1570
     * @return string[][]
1571
     */
1572
    private static function retrieveMap(stdClass $raw): array
1573
    {
1574
        if (false === isset($raw->map)) {
1575
            return [];
1576
        }
1577
1578
        $map = [];
1579
1580
        foreach ((array) $raw->map as $item) {
1581
            $processed = [];
1582
1583
            foreach ($item as $match => $replace) {
1584
                $processed[canonicalize(trim($match))] = canonicalize(trim($replace));
1585
            }
1586
1587
            if (isset($processed['_empty_'])) {
1588
                $processed[''] = $processed['_empty_'];
1589
1590
                unset($processed['_empty_']);
1591
            }
1592
1593
            $map[] = $processed;
1594
        }
1595
1596
        return $map;
1597
    }
1598
1599
    /**
1600
     * @return mixed
1601
     */
1602
    private static function retrieveMetadata(stdClass $raw)
1603
    {
1604
        if (isset($raw->metadata)) {
1605
            if (is_object($raw->metadata)) {
1606
                return (array) $raw->metadata;
1607
            }
1608
1609
            return $raw->metadata;
1610
        }
1611
1612
        return null;
1613
    }
1614
1615
    /**
1616
     * @return string[] The first element is the temporary output path and the second the real one
1617
     */
1618
    private static function retrieveOutputPath(stdClass $raw, string $basePath, ?string $mainScriptPath): array
1619
    {
1620
        if (isset($raw->output)) {
1621
            $path = $raw->output;
1622
        } else {
1623
            if (null !== $mainScriptPath
1624
                && 1 === preg_match('/^(?<main>.*?)(?:\.[\p{L}\d]+)?$/', $mainScriptPath, $matches)
1625
            ) {
1626
                $path = $matches['main'].'.phar';
1627
            } else {
1628
                // Last resort, should not happen
1629
                $path = self::DEFAULT_ALIAS;
1630
            }
1631
        }
1632
1633
        $tmp = $real = self::normalizePath($path, $basePath);
1634
1635
        if ('.phar' !== substr($real, -5)) {
1636
            $tmp .= '.phar';
1637
        }
1638
1639
        return [$tmp, $real];
1640
    }
1641
1642
    private static function retrievePrivateKeyPath(
1643
        stdClass $raw,
1644
        string $basePath,
1645
        int $signingAlgorithm,
1646
        ConfigurationLogger $logger
1647
    ): ?string {
1648
        $raw = (array) $raw;
1649
1650
        if (array_key_exists('key', $raw) && Phar::OPENSSL !== $signingAlgorithm) {
1651
            if (null === $raw['key']) {
1652
                $logger->addRecommendation(
1653
                    'The setting "key" has been set but is unnecessary since the signing algorithm is not "OPENSSL".'
1654
                );
1655
            } else {
1656
                $logger->addWarning(
1657
                    'The setting "key" has been set but is ignored since the signing algorithm is not "OPENSSL".'
1658
                );
1659
            }
1660
1661
            return null;
1662
        }
1663
1664
        if (!isset($raw['key'])) {
1665
            Assertion::true(
1666
                Phar::OPENSSL !== $signingAlgorithm,
1667
                'Expected to have a private key for OpenSSL signing but none have been provided.'
1668
            );
1669
1670
            return null;
1671
        }
1672
1673
        $path = self::normalizePath($raw['key'], $basePath);
1674
1675
        Assertion::file($path);
1676
1677
        return $path;
1678
    }
1679
1680
    private static function retrievePrivateKeyPassphrase(
1681
        stdClass $raw,
1682
        int $algorithm,
1683
        ConfigurationLogger $logger
1684
    ): ?string {
1685
        $raw = (array) $raw;
1686
1687
        if (array_key_exists('key-pass', $raw) && Phar::OPENSSL !== $algorithm) {
1688
            if (false === $raw['key-pass'] || null === $raw['key-pass']) {
1689
                $logger->addRecommendation(
1690
                    'The setting "key-pass" has been set but is unnecessary since the signing algorithm is not '
1691
                    .'"OPENSSL".'
1692
                );
1693
            } else {
1694
                $logger->addWarning(
1695
                    'The setting "key-pass" has been set but ignored the signing algorithm is not "OPENSSL".'
1696
                );
1697
            }
1698
1699
            return null;
1700
        }
1701
1702
        return null;
1703
    }
1704
1705
    /**
1706
     * @return scalar[]
1707
     */
1708
    private static function retrieveReplacements(stdClass $raw, ?string $file, array &$messages): array
1709
    {
1710
        if (null === $file) {
1711
            return [];
1712
        }
1713
1714
        $replacements = isset($raw->replacements) ? (array) $raw->replacements : [];
1715
1716
        if (null !== ($git = self::retrievePrettyGitPlaceholder($raw))) {
1717
            $replacements[$git] = self::retrievePrettyGitTag($file);
1718
        }
1719
1720
        if (null !== ($git = self::retrieveGitHashPlaceholder($raw))) {
1721
            $replacements[$git] = self::retrieveGitHash($file);
1722
        }
1723
1724
        if (null !== ($git = self::retrieveGitShortHashPlaceholder($raw))) {
1725
            $replacements[$git] = self::retrieveGitHash($file, true);
1726
        }
1727
1728
        if (null !== ($git = self::retrieveGitTagPlaceholder($raw))) {
1729
            $replacements[$git] = self::retrieveGitTag($file);
1730
        }
1731
1732
        if (null !== ($git = self::retrieveGitVersionPlaceholder($raw))) {
1733
            $replacements[$git] = self::retrieveGitVersion($file);
1734
        }
1735
1736
        $datetimeFormat = self::retrieveDatetimeFormat($raw, $messages);
0 ignored issues
show
Bug introduced by
$messages of type array is incompatible with the type KevinGH\Box\ConfigurationLogger expected by parameter $logger of KevinGH\Box\Configuratio...etrieveDatetimeFormat(). ( Ignorable by Annotation )

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

1736
        $datetimeFormat = self::retrieveDatetimeFormat($raw, /** @scrutinizer ignore-type */ $messages);
Loading history...
1737
1738
        if (null !== ($date = self::retrieveDatetimeNowPlaceHolder($raw))) {
1739
            $replacements[$date] = self::retrieveDatetimeNow(
1740
                $datetimeFormat
1741
            );
1742
        }
1743
1744
        $sigil = self::retrieveReplacementSigil($raw);
1745
1746
        foreach ($replacements as $key => $value) {
1747
            unset($replacements[$key]);
1748
            $replacements[$sigil.$key.$sigil] = $value;
1749
        }
1750
1751
        return $replacements;
1752
    }
1753
1754
    private static function retrievePrettyGitPlaceholder(stdClass $raw): ?string
1755
    {
1756
        return isset($raw->{'git'}) ? $raw->{'git'} : null;
1757
    }
1758
1759
    private static function retrieveGitHashPlaceholder(stdClass $raw): ?string
1760
    {
1761
        return isset($raw->{'git-commit'}) ? $raw->{'git-commit'} : null;
1762
    }
1763
1764
    /**
1765
     * @param string $file
1766
     * @param bool   $short Use the short version
1767
     *
1768
     * @return string the commit hash
1769
     */
1770
    private static function retrieveGitHash(string $file, bool $short = false): string
1771
    {
1772
        return self::runGitCommand(
1773
            sprintf(
1774
                'git log --pretty="%s" -n1 HEAD',
1775
                $short ? '%h' : '%H'
1776
            ),
1777
            $file
1778
        );
1779
    }
1780
1781
    private static function retrieveGitShortHashPlaceholder(stdClass $raw): ?string
1782
    {
1783
        return isset($raw->{'git-commit-short'}) ? $raw->{'git-commit-short'} : null;
1784
    }
1785
1786
    private static function retrieveGitTagPlaceholder(stdClass $raw): ?string
1787
    {
1788
        return isset($raw->{'git-tag'}) ? $raw->{'git-tag'} : null;
1789
    }
1790
1791
    private static function retrieveGitTag(string $file): string
1792
    {
1793
        return self::runGitCommand('git describe --tags HEAD', $file);
1794
    }
1795
1796
    private static function retrievePrettyGitTag(string $file): string
1797
    {
1798
        $version = self::retrieveGitTag($file);
1799
1800
        if (preg_match('/^(?<tag>.+)-\d+-g(?<hash>[a-f0-9]{7})$/', $version, $matches)) {
1801
            return sprintf('%s@%s', $matches['tag'], $matches['hash']);
1802
        }
1803
1804
        return $version;
1805
    }
1806
1807
    private static function retrieveGitVersionPlaceholder(stdClass $raw): ?string
1808
    {
1809
        return isset($raw->{'git-version'}) ? $raw->{'git-version'} : null;
1810
    }
1811
1812
    private static function retrieveGitVersion(string $file): ?string
1813
    {
1814
        try {
1815
            return self::retrieveGitTag($file);
1816
        } catch (RuntimeException $exception) {
1817
            try {
1818
                return self::retrieveGitHash($file, true);
1819
            } catch (RuntimeException $exception) {
1820
                throw new RuntimeException(
1821
                    sprintf(
1822
                        'The tag or commit hash could not be retrieved from "%s": %s',
1823
                        dirname($file),
1824
                        $exception->getMessage()
1825
                    ),
1826
                    0,
1827
                    $exception
1828
                );
1829
            }
1830
        }
1831
    }
1832
1833
    private static function retrieveDatetimeNowPlaceHolder(stdClass $raw): ?string
1834
    {
1835
        return isset($raw->{'datetime'}) ? $raw->{'datetime'} : null;
1836
    }
1837
1838
    private static function retrieveDatetimeNow(string $format): string
1839
    {
1840
        $now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
1841
1842
        return $now->format($format);
1843
    }
1844
1845
    private static function retrieveDatetimeFormat(stdClass $raw, ConfigurationLogger $logger): string
1846
    {
1847
        if (isset($raw->{'datetime-format'})) {
1848
            $format = $raw->{'datetime-format'};
1849
        } elseif (isset($raw->{'datetime_format'})) {
1850
            @trigger_error(
1851
                'The setting "datetime_format" is deprecated, use "datetime-format" instead.',
1852
                E_USER_DEPRECATED
1853
            );
1854
            $logger->addWarning('The setting "datetime_format" is deprecated, use "datetime-format" instead.');
1855
1856
            $format = $raw->{'datetime_format'};
1857
        }
1858
1859
        if (isset($format)) {
1860
            $formattedDate = (new DateTimeImmutable())->format($format);
1861
1862
            Assertion::false(
1863
                false === $formattedDate || $formattedDate === $format,
1864
                sprintf(
1865
                    'Expected the datetime format to be a valid format: "%s" is not',
1866
                    $format
1867
                )
1868
            );
1869
1870
            return $format;
1871
        }
1872
1873
        return self::DEFAULT_DATETIME_FORMAT;
1874
    }
1875
1876
    private static function retrieveReplacementSigil(stdClass $raw): string
1877
    {
1878
        return isset($raw->{'replacement-sigil'}) ? $raw->{'replacement-sigil'} : self::DEFAULT_REPLACEMENT_SIGIL;
1879
    }
1880
1881
    private static function retrieveShebang(stdClass $raw): ?string
1882
    {
1883
        if (false === array_key_exists('shebang', (array) $raw)) {
1884
            return self::DEFAULT_SHEBANG;
1885
        }
1886
1887
        $shebang = $raw->shebang;
1888
1889
        if (false === $shebang) {
1890
            return null;
1891
        }
1892
1893
        if (null === $shebang) {
1894
            $shebang = self::DEFAULT_SHEBANG;
1895
        }
1896
1897
        Assertion::string($shebang, 'Expected shebang to be either a string, false or null, found true');
1898
1899
        $shebang = trim($shebang);
1900
1901
        Assertion::notEmpty($shebang, 'The shebang should not be empty.');
1902
        Assertion::true(
1903
            '#!' === substr($shebang, 0, 2),
1904
            sprintf(
1905
                'The shebang line must start with "#!". Got "%s" instead',
1906
                $shebang
1907
            )
1908
        );
1909
1910
        return $shebang;
1911
    }
1912
1913
    private static function retrieveSigningAlgorithm(stdClass $raw): int
1914
    {
1915
        if (false === isset($raw->algorithm)) {
1916
            return self::DEFAULT_SIGNING_ALGORITHM;
1917
        }
1918
1919
        $algorithm = strtoupper($raw->algorithm);
1920
1921
        Assertion::inArray($algorithm, array_keys(get_phar_signing_algorithms()));
1922
1923
        Assertion::true(
1924
            defined('Phar::'.$algorithm),
1925
            sprintf(
1926
                'The signing algorithm "%s" is not supported by your current PHAR version.',
1927
                $algorithm
1928
            )
1929
        );
1930
1931
        return constant('Phar::'.$algorithm);
1932
    }
1933
1934
    private static function retrieveStubBannerContents(stdClass $raw): ?string
1935
    {
1936
        if (false === array_key_exists('banner', (array) $raw) || null === $raw->banner) {
1937
            return self::DEFAULT_BANNER;
1938
        }
1939
1940
        $banner = $raw->banner;
1941
1942
        if (false === $banner) {
1943
            return null;
1944
        }
1945
1946
        Assertion::true(is_string($banner) || is_array($banner), 'The banner cannot accept true as a value');
1947
1948
        if (is_array($banner)) {
1949
            $banner = implode("\n", $banner);
1950
        }
1951
1952
        return $banner;
1953
    }
1954
1955
    private static function retrieveStubBannerPath(stdClass $raw, string $basePath): ?string
1956
    {
1957
        if (false === isset($raw->{'banner-file'})) {
1958
            return null;
1959
        }
1960
1961
        $bannerFile = make_path_absolute($raw->{'banner-file'}, $basePath);
1962
1963
        Assertion::file($bannerFile);
1964
1965
        return $bannerFile;
1966
    }
1967
1968
    private static function normalizeStubBannerContents(?string $contents): ?string
1969
    {
1970
        if (null === $contents) {
1971
            return null;
1972
        }
1973
1974
        $banner = explode("\n", $contents);
1975
        $banner = array_map('trim', $banner);
1976
1977
        return implode("\n", $banner);
1978
    }
1979
1980
    private static function retrieveStubPath(stdClass $raw, string $basePath): ?string
1981
    {
1982
        if (isset($raw->stub) && is_string($raw->stub)) {
1983
            $stubPath = make_path_absolute($raw->stub, $basePath);
1984
1985
            Assertion::file($stubPath);
1986
1987
            return $stubPath;
1988
        }
1989
1990
        return null;
1991
    }
1992
1993
    private static function retrieveIsInterceptFileFuncs(stdClass $raw): bool
1994
    {
1995
        if (isset($raw->intercept)) {
1996
            return $raw->intercept;
1997
        }
1998
1999
        return false;
2000
    }
2001
2002
    private static function retrievePromptForPrivateKey(
2003
        stdClass $raw,
2004
        int $signingAlgorithm,
2005
        ConfigurationLogger $logger
2006
    ): bool {
2007
        if (isset($raw->{'key-pass'}) && true === $raw->{'key-pass'}) {
2008
            if (Phar::OPENSSL !== $signingAlgorithm) {
2009
                $logger->addWarning(
2010
                    'A prompt for password for the private key has been requested but ignored since the signing '
2011
                    .'algorithm used is not "OPENSSL.'
2012
                );
2013
2014
                return false;
2015
            }
2016
2017
            return true;
2018
        }
2019
2020
        return false;
2021
    }
2022
2023
    private static function retrieveIsStubGenerated(stdClass $raw, ?string $stubPath): bool
2024
    {
2025
        return null === $stubPath && (false === isset($raw->stub) || false !== $raw->stub);
2026
    }
2027
2028
    private static function retrieveCheckRequirements(
2029
        stdClass $raw,
2030
        bool $hasComposerJson,
2031
        bool $hasComposerLock,
2032
        ConfigurationLogger $logger
2033
    ): bool {
2034
        $raw = (array) $raw;
2035
2036
        if (false === array_key_exists('check-requirements', $raw)) {
2037
            return $hasComposerJson || $hasComposerLock;
2038
        }
2039
2040
        /** @var null|bool $checkRequirements */
2041
        $checkRequirements = $raw['check-requirements'];
2042
2043
        if ($checkRequirements) {
2044
            $logger->addRecommendation(
2045
                'The "check-requirements" setting has been set but is unnecessary since its value is the default value'
2046
            );
2047
        }
2048
2049
        if (null === $checkRequirements) {
2050
            $checkRequirements = true;
2051
        }
2052
2053
        if ($checkRequirements && false === $hasComposerJson && false === $hasComposerLock) {
2054
            $logger->addWarning(
2055
                'The requirement checker could not be used because the composer.json and composer.lock file could not '
2056
                .'be found.'
2057
            );
2058
2059
            return false;
2060
        }
2061
2062
        return $checkRequirements;
2063
    }
2064
2065
    private static function retrievePhpScoperConfig(stdClass $raw, string $basePath): PhpScoperConfiguration
2066
    {
2067
        if (!isset($raw->{'php-scoper'})) {
2068
            $configFilePath = make_path_absolute(self::PHP_SCOPER_CONFIG, $basePath);
2069
2070
            return file_exists($configFilePath)
2071
                ? PhpScoperConfiguration::load($configFilePath)
2072
                : PhpScoperConfiguration::load()
2073
             ;
2074
        }
2075
2076
        $configFile = $raw->{'php-scoper'};
2077
2078
        Assertion::string($configFile);
2079
2080
        $configFilePath = make_path_absolute($configFile, $basePath);
2081
2082
        Assertion::file($configFilePath);
2083
        Assertion::readable($configFilePath);
2084
2085
        return PhpScoperConfiguration::load($configFilePath);
2086
    }
2087
2088
    /**
2089
     * Runs a Git command on the repository.
2090
     *
2091
     * @param string $command the command
2092
     *
2093
     * @return string the trimmed output from the command
2094
     */
2095
    private static function runGitCommand(string $command, string $file): string
2096
    {
2097
        $path = dirname($file);
2098
2099
        $process = new Process($command, $path);
2100
2101
        if (0 === $process->run()) {
2102
            return trim($process->getOutput());
2103
        }
2104
2105
        throw new RuntimeException(
2106
            sprintf(
2107
                'The tag or commit hash could not be retrieved from "%s": %s',
2108
                $path,
2109
                $process->getErrorOutput()
2110
            )
2111
        );
2112
    }
2113
2114
    private static function createPhpCompactor(stdClass $raw): Compactor
2115
    {
2116
        // TODO: false === not set; check & add test/doc
2117
        $tokenizer = new Tokenizer();
2118
2119
        if (false === empty($raw->annotations) && isset($raw->annotations->ignore)) {
2120
            $tokenizer->ignore(
2121
                (array) $raw->annotations->ignore
2122
            );
2123
        }
2124
2125
        return new Php($tokenizer);
2126
    }
2127
}
2128