Passed
Push — master ( 4eeb28...641dd6 )
by Théo
02:24
created

Configuration::retrieveExcludeDevFiles()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 10
dl 0
loc 20
rs 9.9332
c 0
b 0
f 0
cc 4
nc 3
nop 3
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\Box\Compactor\Json as LegacyJson;
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 Humbug\PhpScoper\Scoper\FileWhitelistScoper;
27
use InvalidArgumentException;
28
use KevinGH\Box\Annotation\AnnotationDumper;
29
use KevinGH\Box\Annotation\DocblockAnnotationParser;
30
use KevinGH\Box\Annotation\DocblockParser;
31
use KevinGH\Box\Compactor\Json as JsonCompactor;
32
use KevinGH\Box\Compactor\Php as PhpCompactor;
33
use KevinGH\Box\Compactor\PhpScoper as PhpScoperCompactor;
34
use KevinGH\Box\Composer\ComposerConfiguration;
35
use KevinGH\Box\Json\Json;
36
use KevinGH\Box\PhpScoper\SimpleScoper;
37
use Phar;
38
use RuntimeException;
39
use Seld\JsonLint\ParsingException;
40
use SplFileInfo;
41
use stdClass;
42
use Symfony\Component\Finder\Finder;
43
use Symfony\Component\Process\Process;
44
use const E_USER_DEPRECATED;
45
use function array_column;
46
use function array_diff;
47
use function array_filter;
48
use function array_flip;
49
use function array_key_exists;
50
use function array_keys;
51
use function array_map;
52
use function array_merge;
53
use function array_unique;
54
use function array_values;
55
use function array_walk;
56
use function constant;
57
use function current;
58
use function defined;
59
use function dirname;
60
use function explode;
61
use function file_exists;
62
use function getcwd;
63
use function implode;
64
use function in_array;
65
use function intval;
66
use function is_array;
67
use function is_bool;
68
use function is_file;
69
use function is_link;
70
use function is_object;
71
use function is_readable;
72
use function is_string;
73
use function iter\map;
74
use function iter\toArray;
75
use function iter\values;
76
use function KevinGH\Box\FileSystem\canonicalize;
77
use function KevinGH\Box\FileSystem\file_contents;
78
use function KevinGH\Box\FileSystem\is_absolute_path;
79
use function KevinGH\Box\FileSystem\longest_common_base_path;
80
use function KevinGH\Box\FileSystem\make_path_absolute;
81
use function KevinGH\Box\FileSystem\make_path_relative;
82
use function krsort;
83
use function preg_match;
84
use function preg_replace;
85
use function property_exists;
86
use function realpath;
87
use function sprintf;
88
use function strtoupper;
89
use function substr;
90
use function trigger_error;
91
use function trim;
92
93
/**
94
 * @private
95
 */
96
final class Configuration
97
{
98
    private const DEFAULT_OUTPUT_FALLBACK = 'test.phar';
99
    private const DEFAULT_MAIN_SCRIPT = 'index.php';
100
    private const DEFAULT_DATETIME_FORMAT = 'Y-m-d H:i:s T';
101
    private const DEFAULT_REPLACEMENT_SIGIL = '@';
102
    private const DEFAULT_SHEBANG = '#!/usr/bin/env php';
103
    private const DEFAULT_BANNER = <<<'BANNER'
104
Generated by Humbug Box %s.
105
106
@link https://github.com/humbug/box
107
BANNER;
108
    private const FILES_SETTINGS = [
109
        'directories',
110
        'finder',
111
    ];
112
    private const PHP_SCOPER_CONFIG = 'scoper.inc.php';
113
    private const DEFAULT_SIGNING_ALGORITHM = Phar::SHA1;
114
    private const DEFAULT_ALIAS_PREFIX = 'box-auto-generated-alias-';
115
116
    private const DEFAULT_IGNORED_ANNOTATIONS = [
117
        'abstract',
118
        'access',
119
        'annotation',
120
        'api',
121
        'attribute',
122
        'attributes',
123
        'author',
124
        'category',
125
        'code',
126
        'codecoverageignore',
127
        'codecoverageignoreend',
128
        'codecoverageignorestart',
129
        'copyright',
130
        'deprec',
131
        'deprecated',
132
        'endcode',
133
        'example',
134
        'exception',
135
        'filesource',
136
        'final',
137
        'fixme',
138
        'global',
139
        'ignore',
140
        'ingroup',
141
        'inheritdoc',
142
        'internal',
143
        'license',
144
        'link',
145
        'magic',
146
        'method',
147
        'name',
148
        'override',
149
        'package',
150
        'package_version',
151
        'param',
152
        'private',
153
        'property',
154
        'required',
155
        'return',
156
        'see',
157
        'since',
158
        'static',
159
        'staticvar',
160
        'subpackage',
161
        'suppresswarnings',
162
        'target',
163
        'throw',
164
        'throws',
165
        'todo',
166
        'tutorial',
167
        'usedby',
168
        'uses',
169
        'var',
170
        'version',
171
    ];
172
173
    private const ALGORITHM_KEY = 'algorithm';
174
    private const ALIAS_KEY = 'alias';
175
    private const ANNOTATIONS_KEY = 'annotations';
176
    private const IGNORED_ANNOTATIONS_KEY = 'ignore';
177
    private const AUTO_DISCOVERY_KEY = 'force-autodiscovery';
178
    private const BANNER_KEY = 'banner';
179
    private const BANNER_FILE_KEY = 'banner-file';
180
    private const BASE_PATH_KEY = 'base-path';
181
    private const BLACKLIST_KEY = 'blacklist';
182
    private const CHECK_REQUIREMENTS_KEY = 'check-requirements';
183
    private const CHMOD_KEY = 'chmod';
184
    private const COMPACTORS_KEY = 'compactors';
185
    private const COMPRESSION_KEY = 'compression';
186
    private const DATETIME_KEY = 'datetime';
187
    private const DATETIME_FORMAT_KEY = 'datetime-format';
188
    private const DATETIME_FORMAT_DEPRECATED_KEY = 'datetime_format';
189
    private const DIRECTORIES_KEY = 'directories';
190
    private const DIRECTORIES_BIN_KEY = 'directories-bin';
191
    private const DUMP_AUTOLOAD_KEY = 'dump-autoload';
192
    private const EXCLUDE_COMPOSER_FILES_KEY = 'exclude-composer-files';
193
    private const EXCLUDE_DEV_FILES_KEY = 'exclude-dev-files';
194
    private const FILES_KEY = 'files';
195
    private const FILES_BIN_KEY = 'files-bin';
196
    private const FINDER_KEY = 'finder';
197
    private const FINDER_BIN_KEY = 'finder-bin';
198
    private const GIT_KEY = 'git';
199
    private const GIT_COMMIT_KEY = 'git-commit';
200
    private const GIT_COMMIT_SHORT_KEY = 'git-commit-short';
201
    private const GIT_TAG_KEY = 'git-tag';
202
    private const GIT_VERSION_KEY = 'git-version';
203
    private const INTERCEPT_KEY = 'intercept';
204
    private const KEY_KEY = 'key';
205
    private const KEY_PASS_KEY = 'key-pass';
206
    private const MAIN_KEY = 'main';
207
    private const MAP_KEY = 'map';
208
    private const METADATA_KEY = 'metadata';
209
    private const OUTPUT_KEY = 'output';
210
    private const PHP_SCOPER_KEY = 'php-scoper';
211
    private const REPLACEMENT_SIGIL_KEY = 'replacement-sigil';
212
    private const REPLACEMENTS_KEY = 'replacements';
213
    private const SHEBANG_KEY = 'shebang';
214
    private const STUB_KEY = 'stub';
215
216
    private $file;
217
    private $fileMode;
218
    private $alias;
219
    private $basePath;
220
    private $composerJson;
221
    private $composerLock;
222
    private $files;
223
    private $binaryFiles;
224
    private $autodiscoveredFiles;
225
    private $dumpAutoload;
226
    private $excludeComposerFiles;
227
    private $excludeDevFiles;
228
    private $compactors;
229
    private $compressionAlgorithm;
230
    private $mainScriptPath;
231
    private $mainScriptContents;
232
    private $fileMapper;
233
    private $metadata;
234
    private $tmpOutputPath;
235
    private $outputPath;
236
    private $privateKeyPassphrase;
237
    private $privateKeyPath;
238
    private $promptForPrivateKey;
239
    private $processedReplacements;
240
    private $shebang;
241
    private $signingAlgorithm;
242
    private $stubBannerContents;
243
    private $stubBannerPath;
244
    private $stubPath;
245
    private $isInterceptFileFuncs;
246
    private $isStubGenerated;
247
    private $checkRequirements;
248
    private $warnings;
249
    private $recommendations;
250
251
    public static function create(?string $file, stdClass $raw): self
252
    {
253
        $logger = new ConfigurationLogger();
254
255
        $basePath = self::retrieveBasePath($file, $raw, $logger);
256
257
        $composerFiles = self::retrieveComposerFiles($basePath);
258
259
        /**
260
         * @var (string|null)[]
261
         * @var (string|null)[] $composerLock
262
         * @var (string|null)[] $installedJson
263
         */
264
        [$composerJson, $composerLock, $installedJson] = $composerFiles;
265
266
        $dumpAutoload = self::retrieveDumpAutoload($raw, null !== $composerJson[0], $logger);
267
268
        if ($dumpAutoload && null !== $installedJson[0] && null === $composerLock[0]) {
269
            $logger->addWarning(
270
                'A vendor/composer/installed.json file has been found but its related file composer.lock could not. '
271
                .'This is likely due to the file having been removed despite being necessary. This will not break the '
272
                .'build but the dump-autoload had to be disabled.'
273
            );
274
        }
275
276
        $excludeComposerFiles = self::retrieveExcludeComposerFiles($raw, $logger);
277
278
        $mainScriptPath = self::retrieveMainScriptPath($raw, $basePath, $composerFiles[0][1], $logger);
279
        $mainScriptContents = self::retrieveMainScriptContents($mainScriptPath);
280
281
        [$tmpOutputPath, $outputPath] = self::retrieveOutputPath($raw, $basePath, $mainScriptPath, $logger);
282
283
        $stubPath = self::retrieveStubPath($raw, $basePath, $logger);
284
        $isStubGenerated = self::retrieveIsStubGenerated($raw, $stubPath, $logger);
285
286
        $alias = self::retrieveAlias($raw, null !== $stubPath, $logger);
287
288
        $shebang = self::retrieveShebang($raw, $isStubGenerated, $logger);
289
290
        $stubBannerContents = self::retrieveStubBannerContents($raw, $isStubGenerated, $logger);
291
        $stubBannerPath = self::retrieveStubBannerPath($raw, $basePath, $isStubGenerated, $logger);
292
293
        if (null !== $stubBannerPath) {
294
            $stubBannerContents = file_contents($stubBannerPath);
295
        }
296
297
        $stubBannerContents = self::normalizeStubBannerContents($stubBannerContents);
298
299
        if (null !== $stubBannerPath && self::getDefaultBanner() === $stubBannerContents) {
300
            self::addRecommendationForDefaultValue($logger, self::BANNER_FILE_KEY);
301
        }
302
303
        $isInterceptsFileFunctions = self::retrieveInterceptsFileFunctions($raw, $isStubGenerated, $logger);
304
305
        $checkRequirements = self::retrieveCheckRequirements(
306
            $raw,
307
            null !== $composerJson[0],
308
            null !== $composerLock[0],
309
            false === $isStubGenerated && null === $stubPath,
310
            $logger
311
        );
312
313
        $excludeDevPackages = self::retrieveExcludeDevFiles($raw, $dumpAutoload, $logger);
314
315
        $devPackages = ComposerConfiguration::retrieveDevPackages(
316
            $basePath,
317
            $composerJson[1],
318
            $composerLock[1],
319
            $excludeDevPackages
320
        );
321
322
        /**
323
         * @var string[]
324
         * @var Closure  $blacklistFilter
325
         */
326
        [$excludedPaths, $blacklistFilter] = self::retrieveBlacklistFilter(
327
            $raw,
328
            $basePath,
329
            $logger,
330
            $tmpOutputPath,
331
            $outputPath,
332
            $mainScriptPath
333
        );
334
        // Excluded paths above is a bit misleading since including a file directly has precedence over the blacklist.
335
        // If you consider the following:
336
        //
337
        // {
338
        //   "files": ["file1"],
339
        //   "blacklist": ["file1"],
340
        // }
341
        //
342
        // In the end the file "file1" _will_ be included: blacklist are here to help out to exclude files for finders
343
        // and directories but the user should always have the possibility to force his way to include a file.
344
        //
345
        // The exception however, is for the following which is essential for the good functioning of Box
346
        $alwaysExcludedPaths = array_map(
347
            static function (string $excludedPath) use ($basePath): string {
348
                return self::normalizePath($excludedPath, $basePath);
349
            },
350
            array_filter([$tmpOutputPath, $outputPath, $mainScriptPath])
351
        );
352
353
        $autodiscoverFiles = self::autodiscoverFiles($file, $raw);
354
        $forceFilesAutodiscovery = self::retrieveForceFilesAutodiscovery($raw, $logger);
355
356
        $filesAggregate = self::collectFiles(
357
            $raw,
358
            $basePath,
359
            $mainScriptPath,
360
            $blacklistFilter,
361
            $excludedPaths,
362
            $alwaysExcludedPaths,
363
            $devPackages,
364
            $composerFiles,
365
            $autodiscoverFiles,
366
            $forceFilesAutodiscovery,
367
            $dumpAutoload,
368
            $logger
369
        );
370
        $binaryFilesAggregate = self::collectBinaryFiles(
371
            $raw,
372
            $basePath,
373
            $blacklistFilter,
374
            $excludedPaths,
375
            $alwaysExcludedPaths,
376
            $devPackages,
377
            $logger
378
        );
379
380
        $compactors = self::retrieveCompactors($raw, $basePath, $logger);
381
        $compressionAlgorithm = self::retrieveCompressionAlgorithm($raw, $logger);
382
383
        $fileMode = self::retrieveFileMode($raw, $logger);
384
385
        $map = self::retrieveMap($raw, $logger);
386
        $fileMapper = new MapFile($basePath, $map);
387
388
        $metadata = self::retrieveMetadata($raw, $logger);
389
390
        $signingAlgorithm = self::retrieveSigningAlgorithm($raw, $logger);
391
        $promptForPrivateKey = self::retrievePromptForPrivateKey($raw, $signingAlgorithm, $logger);
392
        $privateKeyPath = self::retrievePrivateKeyPath($raw, $basePath, $signingAlgorithm, $logger);
393
        $privateKeyPassphrase = self::retrievePrivateKeyPassphrase($raw, $signingAlgorithm, $logger);
394
395
        $replacements = self::retrieveReplacements($raw, $file, $logger);
396
397
        return new self(
398
            $file,
399
            $alias,
400
            $basePath,
401
            $composerJson,
402
            $composerLock,
403
            $filesAggregate,
404
            $binaryFilesAggregate,
405
            $autodiscoverFiles || $forceFilesAutodiscovery,
406
            $dumpAutoload,
407
            $excludeComposerFiles,
408
            $excludeDevPackages,
409
            $compactors,
410
            $compressionAlgorithm,
411
            $fileMode,
412
            $mainScriptPath,
413
            $mainScriptContents,
414
            $fileMapper,
415
            $metadata,
416
            $tmpOutputPath,
417
            $outputPath,
418
            $privateKeyPassphrase,
419
            $privateKeyPath,
420
            $promptForPrivateKey,
421
            $replacements,
422
            $shebang,
423
            $signingAlgorithm,
424
            $stubBannerContents,
425
            $stubBannerPath,
426
            $stubPath,
427
            $isInterceptsFileFunctions,
428
            $isStubGenerated,
429
            $checkRequirements,
430
            $logger->getWarnings(),
431
            $logger->getRecommendations()
432
        );
433
    }
434
435
    /**
436
     * @param string        $basePath             Utility to private the base path used and be able to retrieve a
437
     *                                            path relative to it (the base path)
438
     * @param array         $composerJson         The first element is the path to the `composer.json` file as a
439
     *                                            string and the second element its decoded contents as an
440
     *                                            associative array.
441
     * @param array         $composerLock         The first element is the path to the `composer.lock` file as a
442
     *                                            string and the second element its decoded contents as an
443
     *                                            associative array.
444
     * @param SplFileInfo[] $files                List of files
445
     * @param SplFileInfo[] $binaryFiles          List of binary files
446
     * @param bool          $dumpAutoload         Whether or not the Composer autoloader should be dumped
447
     * @param bool          $excludeComposerFiles Whether or not the Composer files composer.json, composer.lock and
448
     *                                            installed.json should be removed from the PHAR
449
     * @param Compactor[]   $compactors           List of file contents compactors
450
     * @param null|int      $compressionAlgorithm Compression algorithm constant value. See the \Phar class constants
451
     * @param null|int      $fileMode             File mode in octal form
452
     * @param string        $mainScriptPath       The main script file path
453
     * @param string        $mainScriptContents   The processed content of the main script file
454
     * @param MapFile       $fileMapper           Utility to map the files from outside and inside the PHAR
455
     * @param mixed         $metadata             The PHAR Metadata
456
     * @param bool          $promptForPrivateKey  If the user should be prompted for the private key passphrase
457
     * @param scalar[]      $replacements         The processed list of replacement placeholders and their values
458
     * @param null|string   $shebang              The shebang line
459
     * @param int           $signingAlgorithm     The PHAR siging algorithm. See \Phar constants
460
     * @param null|string   $stubBannerContents   The stub banner comment
461
     * @param null|string   $stubBannerPath       The path to the stub banner comment file
462
     * @param null|string   $stubPath             The PHAR stub file path
463
     * @param bool          $isInterceptFileFuncs Whether or not Phar::interceptFileFuncs() should be used
464
     * @param bool          $isStubGenerated      Whether or not if the PHAR stub should be generated
465
     * @param bool          $checkRequirements    Whether the PHAR will check the application requirements before
466
     *                                            running
467
     * @param string[]      $warnings
468
     * @param string[]      $recommendations
469
     */
470
    private function __construct(
471
        ?string $file,
472
        string $alias,
473
        string $basePath,
474
        array $composerJson,
475
        array $composerLock,
476
        array $files,
477
        array $binaryFiles,
478
        bool $autodiscoveredFiles,
479
        bool $dumpAutoload,
480
        bool $excludeComposerFiles,
481
        bool $excludeDevPackages,
482
        array $compactors,
483
        ?int $compressionAlgorithm,
484
        ?int $fileMode,
485
        ?string $mainScriptPath,
486
        ?string $mainScriptContents,
487
        MapFile $fileMapper,
488
        $metadata,
489
        string $tmpOutputPath,
490
        string $outputPath,
491
        ?string $privateKeyPassphrase,
492
        ?string $privateKeyPath,
493
        bool $promptForPrivateKey,
494
        array $replacements,
495
        ?string $shebang,
496
        int $signingAlgorithm,
497
        ?string $stubBannerContents,
498
        ?string $stubBannerPath,
499
        ?string $stubPath,
500
        bool $isInterceptFileFuncs,
501
        bool $isStubGenerated,
502
        bool $checkRequirements,
503
        array $warnings,
504
        array $recommendations
505
    ) {
506
        Assertion::nullOrInArray(
507
            $compressionAlgorithm,
508
            get_phar_compression_algorithms(),
509
            sprintf(
510
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
511
                implode('", "', array_keys(get_phar_compression_algorithms()))
512
            )
513
        );
514
515
        if (null === $mainScriptPath) {
516
            Assertion::null($mainScriptContents);
517
        } else {
518
            Assertion::notNull($mainScriptContents);
519
        }
520
521
        $this->file = $file;
522
        $this->alias = $alias;
523
        $this->basePath = $basePath;
524
        $this->composerJson = $composerJson;
525
        $this->composerLock = $composerLock;
526
        $this->files = $files;
527
        $this->binaryFiles = $binaryFiles;
528
        $this->autodiscoveredFiles = $autodiscoveredFiles;
529
        $this->dumpAutoload = $dumpAutoload;
530
        $this->excludeComposerFiles = $excludeComposerFiles;
531
        $this->excludeDevFiles = $excludeDevPackages;
532
        $this->compactors = $compactors;
533
        $this->compressionAlgorithm = $compressionAlgorithm;
534
        $this->fileMode = $fileMode;
535
        $this->mainScriptPath = $mainScriptPath;
536
        $this->mainScriptContents = $mainScriptContents;
537
        $this->fileMapper = $fileMapper;
538
        $this->metadata = $metadata;
539
        $this->tmpOutputPath = $tmpOutputPath;
540
        $this->outputPath = $outputPath;
541
        $this->privateKeyPassphrase = $privateKeyPassphrase;
542
        $this->privateKeyPath = $privateKeyPath;
543
        $this->promptForPrivateKey = $promptForPrivateKey;
544
        $this->processedReplacements = $replacements;
545
        $this->shebang = $shebang;
546
        $this->signingAlgorithm = $signingAlgorithm;
547
        $this->stubBannerContents = $stubBannerContents;
548
        $this->stubBannerPath = $stubBannerPath;
549
        $this->stubPath = $stubPath;
550
        $this->isInterceptFileFuncs = $isInterceptFileFuncs;
551
        $this->isStubGenerated = $isStubGenerated;
552
        $this->checkRequirements = $checkRequirements;
553
        $this->warnings = $warnings;
554
        $this->recommendations = $recommendations;
555
    }
556
557
    public function getConfigurationFile(): ?string
558
    {
559
        return $this->file;
560
    }
561
562
    public function getAlias(): string
563
    {
564
        return $this->alias;
565
    }
566
567
    public function getBasePath(): string
568
    {
569
        return $this->basePath;
570
    }
571
572
    public function getComposerJson(): ?string
573
    {
574
        return $this->composerJson[0];
575
    }
576
577
    public function getDecodedComposerJsonContents(): ?array
578
    {
579
        return $this->composerJson[1];
580
    }
581
582
    public function getComposerLock(): ?string
583
    {
584
        return $this->composerLock[0];
585
    }
586
587
    public function getDecodedComposerLockContents(): ?array
588
    {
589
        return $this->composerLock[1];
590
    }
591
592
    /**
593
     * @return SplFileInfo[]
594
     */
595
    public function getFiles(): array
596
    {
597
        return $this->files;
598
    }
599
600
    /**
601
     * @return SplFileInfo[]
602
     */
603
    public function getBinaryFiles(): array
604
    {
605
        return $this->binaryFiles;
606
    }
607
608
    public function hasAutodiscoveredFiles(): bool
609
    {
610
        return $this->autodiscoveredFiles;
611
    }
612
613
    public function dumpAutoload(): bool
614
    {
615
        return $this->dumpAutoload;
616
    }
617
618
    public function excludeComposerFiles(): bool
619
    {
620
        return $this->excludeComposerFiles;
621
    }
622
623
    public function excludeDevFiles(): bool
624
    {
625
        return $this->excludeDevFiles;
626
    }
627
628
    /**
629
     * @return Compactor[] the list of compactors
630
     */
631
    public function getCompactors(): array
632
    {
633
        return $this->compactors;
634
    }
635
636
    public function getCompressionAlgorithm(): ?int
637
    {
638
        return $this->compressionAlgorithm;
639
    }
640
641
    public function getFileMode(): ?int
642
    {
643
        return $this->fileMode;
644
    }
645
646
    public function hasMainScript(): bool
647
    {
648
        return null !== $this->mainScriptPath;
649
    }
650
651
    public function getMainScriptPath(): string
652
    {
653
        Assertion::notNull(
654
            $this->mainScriptPath,
655
            'Cannot retrieve the main script path: no main script configured.'
656
        );
657
658
        return $this->mainScriptPath;
659
    }
660
661
    public function getMainScriptContents(): string
662
    {
663
        Assertion::notNull(
664
            $this->mainScriptPath,
665
            'Cannot retrieve the main script contents: no main script configured.'
666
        );
667
668
        return $this->mainScriptContents;
669
    }
670
671
    public function checkRequirements(): bool
672
    {
673
        return $this->checkRequirements;
674
    }
675
676
    public function getTmpOutputPath(): string
677
    {
678
        return $this->tmpOutputPath;
679
    }
680
681
    public function getOutputPath(): string
682
    {
683
        return $this->outputPath;
684
    }
685
686
    public function getFileMapper(): MapFile
687
    {
688
        return $this->fileMapper;
689
    }
690
691
    /**
692
     * @return mixed
693
     */
694
    public function getMetadata()
695
    {
696
        return $this->metadata;
697
    }
698
699
    public function getPrivateKeyPassphrase(): ?string
700
    {
701
        return $this->privateKeyPassphrase;
702
    }
703
704
    public function getPrivateKeyPath(): ?string
705
    {
706
        return $this->privateKeyPath;
707
    }
708
709
    /**
710
     * @deprecated Use promptForPrivateKey() instead
711
     */
712
    public function isPrivateKeyPrompt(): bool
713
    {
714
        return $this->promptForPrivateKey;
715
    }
716
717
    public function promptForPrivateKey(): bool
718
    {
719
        return $this->promptForPrivateKey;
720
    }
721
722
    /**
723
     * @return scalar[]
724
     */
725
    public function getReplacements(): array
726
    {
727
        return $this->processedReplacements;
728
    }
729
730
    public function getShebang(): ?string
731
    {
732
        return $this->shebang;
733
    }
734
735
    public function getSigningAlgorithm(): int
736
    {
737
        return $this->signingAlgorithm;
738
    }
739
740
    public function getStubBannerContents(): ?string
741
    {
742
        return $this->stubBannerContents;
743
    }
744
745
    public function getStubBannerPath(): ?string
746
    {
747
        return $this->stubBannerPath;
748
    }
749
750
    public function getStubPath(): ?string
751
    {
752
        return $this->stubPath;
753
    }
754
755
    public function isInterceptFileFuncs(): bool
756
    {
757
        return $this->isInterceptFileFuncs;
758
    }
759
760
    public function isStubGenerated(): bool
761
    {
762
        return $this->isStubGenerated;
763
    }
764
765
    /**
766
     * @return string[]
767
     */
768
    public function getWarnings(): array
769
    {
770
        return $this->warnings;
771
    }
772
773
    /**
774
     * @return string[]
775
     */
776
    public function getRecommendations(): array
777
    {
778
        return $this->recommendations;
779
    }
780
781
    private static function retrieveAlias(stdClass $raw, bool $userStubUsed, ConfigurationLogger $logger): string
782
    {
783
        self::checkIfDefaultValue($logger, $raw, self::ALIAS_KEY);
784
785
        if (false === isset($raw->{self::ALIAS_KEY})) {
786
            return unique_id(self::DEFAULT_ALIAS_PREFIX).'.phar';
787
        }
788
789
        $alias = trim($raw->{self::ALIAS_KEY});
790
791
        Assertion::notEmpty($alias, 'A PHAR alias cannot be empty when provided.');
792
793
        if ($userStubUsed) {
794
            $logger->addWarning(
795
                sprintf(
796
                    'The "%s" setting has been set but is ignored since a custom stub path is used',
797
                    self::ALIAS_KEY
798
                )
799
            );
800
        }
801
802
        return $alias;
803
    }
804
805
    private static function retrieveBasePath(?string $file, stdClass $raw, ConfigurationLogger $logger): string
806
    {
807
        if (null === $file) {
808
            return getcwd();
809
        }
810
811
        if (false === isset($raw->{self::BASE_PATH_KEY})) {
812
            return realpath(dirname($file));
813
        }
814
815
        $basePath = trim($raw->{self::BASE_PATH_KEY});
816
817
        Assertion::directory(
818
            $basePath,
819
            'The base path "%s" is not a directory or does not exist.'
820
        );
821
822
        $basePath = realpath($basePath);
823
        $defaultPath = realpath(dirname($file));
824
825
        if ($basePath === $defaultPath) {
826
            self::addRecommendationForDefaultValue($logger, self::BASE_PATH_KEY);
827
        }
828
829
        return $basePath;
830
    }
831
832
    /**
833
     * Checks if files should be auto-discovered. It does NOT account for the force-autodiscovery setting.
834
     */
835
    private static function autodiscoverFiles(?string $file, stdClass $raw): bool
836
    {
837
        if (null === $file) {
838
            return true;
839
        }
840
841
        $associativeRaw = (array) $raw;
842
843
        return self::FILES_SETTINGS === array_diff(self::FILES_SETTINGS, array_keys($associativeRaw));
844
    }
845
846
    private static function retrieveForceFilesAutodiscovery(stdClass $raw, ConfigurationLogger $logger): bool
847
    {
848
        self::checkIfDefaultValue($logger, $raw, self::AUTO_DISCOVERY_KEY, false);
849
850
        return $raw->{self::AUTO_DISCOVERY_KEY} ?? false;
851
    }
852
853
    private static function retrieveBlacklistFilter(
854
        stdClass $raw,
855
        string $basePath,
856
        ConfigurationLogger $logger,
857
        ?string ...$excludedPaths
858
    ): array {
859
        $blacklist = array_flip(
860
            self::retrieveBlacklist($raw, $basePath, $logger, ...$excludedPaths)
861
        );
862
863
        $blacklistFilter = static function (SplFileInfo $file) use ($blacklist): ?bool {
864
            if ($file->isLink()) {
865
                return false;
866
            }
867
868
            if (false === $file->getRealPath()) {
869
                return false;
870
            }
871
872
            if (array_key_exists($file->getRealPath(), $blacklist)) {
873
                return false;
874
            }
875
876
            return null;
877
        };
878
879
        return [array_keys($blacklist), $blacklistFilter];
880
    }
881
882
    /**
883
     * @param null[]|string[] $excludedPaths
884
     *
885
     * @return string[]
886
     */
887
    private static function retrieveBlacklist(
888
        stdClass $raw,
889
        string $basePath,
890
        ConfigurationLogger $logger,
891
        ?string ...$excludedPaths
892
    ): array {
893
        self::checkIfDefaultValue($logger, $raw, self::BLACKLIST_KEY, []);
894
895
        $normalizedBlacklist = array_map(
896
            static function (string $excludedPath) use ($basePath): string {
897
                return self::normalizePath($excludedPath, $basePath);
898
            },
899
            array_filter($excludedPaths)
900
        );
901
902
        /** @var string[] $blacklist */
903
        $blacklist = $raw->{self::BLACKLIST_KEY} ?? [];
904
905
        foreach ($blacklist as $file) {
906
            $normalizedBlacklist[] = self::normalizePath($file, $basePath);
907
            $normalizedBlacklist[] = canonicalize(make_path_relative(trim($file), $basePath));
908
        }
909
910
        return array_unique($normalizedBlacklist);
911
    }
912
913
    /**
914
     * @param string[] $excludedPaths
915
     * @param string[] $alwaysExcludedPaths
916
     * @param string[] $devPackages
917
     *
918
     * @return SplFileInfo[]
919
     */
920
    private static function collectFiles(
921
        stdClass $raw,
922
        string $basePath,
923
        ?string $mainScriptPath,
924
        Closure $blacklistFilter,
925
        array $excludedPaths,
926
        array $alwaysExcludedPaths,
927
        array $devPackages,
928
        array $composerFiles,
929
        bool $autodiscoverFiles,
930
        bool $forceFilesAutodiscovery,
931
        bool $dumpAutoload,
932
        ConfigurationLogger $logger
933
    ): array {
934
        $files = [self::retrieveFiles($raw, self::FILES_KEY, $basePath, $composerFiles, $alwaysExcludedPaths, $logger)];
935
936
        if ($autodiscoverFiles || $forceFilesAutodiscovery) {
937
            [$filesToAppend, $directories] = self::retrieveAllDirectoriesToInclude(
938
                $basePath,
939
                $composerFiles[0][1],
940
                $devPackages,
941
                array_filter(
942
                    array_column($composerFiles, 0)
943
                ),
944
                $excludedPaths
945
            );
946
947
            $files[] = self::wrapInSplFileInfo($filesToAppend);
948
949
            $files[] = self::retrieveAllFiles(
950
                $basePath,
951
                $directories,
952
                $mainScriptPath,
953
                $blacklistFilter,
954
                $excludedPaths,
955
                $devPackages
956
            );
957
        }
958
959
        if (false === $autodiscoverFiles) {
960
            $files[] = self::retrieveDirectories(
961
                $raw,
962
                self::DIRECTORIES_KEY,
963
                $basePath,
964
                $blacklistFilter,
965
                $excludedPaths,
966
                $logger
967
            );
968
969
            $filesFromFinders = self::retrieveFilesFromFinders(
970
                $raw,
971
                self::FINDER_KEY,
972
                $basePath,
973
                $blacklistFilter,
974
                $devPackages,
975
                $logger
976
            );
977
978
            foreach ($filesFromFinders as $filesFromFinder) {
979
                // Avoid an array_merge here as it can be quite expansive at this stage depending of the number of files
980
                $files[] = $filesFromFinder;
981
            }
982
        }
983
984
        $aggregate = self::retrieveFilesAggregate(...$files);
985
986
        if (false === $dumpAutoload) {
987
            return $aggregate;
988
        }
989
990
        if (null === $composerFiles[1][0]) {
991
            return $aggregate;
992
        }
993
994
        foreach ($aggregate as $file) {
995
            if ($file->getRealPath() === $composerFiles[2][0]) {
996
                return $aggregate;
997
            }
998
        }
999
1000
        $logger->addWarning(
1001
            'A composer.lock file has been found but its related file vendor/composer/installed.json could not. This '
1002
            .'could be due to either dependencies incorrectly installed or an incorrect Box configuration which is not '
1003
            .'including the installed.json file. This will not break the build but will likely result in a broken '
1004
            .'Composer classmap.'
1005
        );
1006
1007
        return $aggregate;
1008
    }
1009
1010
    /**
1011
     * @param string[] $excludedPaths
1012
     * @param string[] $alwaysExcludedPaths
1013
     * @param string[] $devPackages
1014
     *
1015
     * @return SplFileInfo[]
1016
     */
1017
    private static function collectBinaryFiles(
1018
        stdClass $raw,
1019
        string $basePath,
1020
        Closure $blacklistFilter,
1021
        array $excludedPaths,
1022
        array $alwaysExcludedPaths,
1023
        array $devPackages,
1024
        ConfigurationLogger $logger
1025
    ): array {
1026
        $binaryFiles = self::retrieveFiles($raw, self::FILES_BIN_KEY, $basePath, [], $alwaysExcludedPaths, $logger);
1027
1028
        $binaryDirectories = self::retrieveDirectories(
1029
            $raw,
1030
            self::DIRECTORIES_BIN_KEY,
1031
            $basePath,
1032
            $blacklistFilter,
1033
            $excludedPaths,
1034
            $logger
1035
        );
1036
1037
        $binaryFilesFromFinders = self::retrieveFilesFromFinders(
1038
            $raw,
1039
            self::FINDER_BIN_KEY,
1040
            $basePath,
1041
            $blacklistFilter,
1042
            $devPackages,
1043
            $logger
1044
        );
1045
1046
        return self::retrieveFilesAggregate($binaryFiles, $binaryDirectories, ...$binaryFilesFromFinders);
1047
    }
1048
1049
    /**
1050
     * @param string[] $excludedFiles
1051
     *
1052
     * @return SplFileInfo[]
1053
     */
1054
    private static function retrieveFiles(
1055
        stdClass $raw,
1056
        string $key,
1057
        string $basePath,
1058
        array $composerFiles,
1059
        array $excludedFiles,
1060
        ConfigurationLogger $logger
1061
    ): array {
1062
        self::checkIfDefaultValue($logger, $raw, $key, []);
1063
1064
        $excludedFiles = array_flip($excludedFiles);
1065
        $files = [];
1066
1067
        if (isset($composerFiles[0][0])) {
1068
            $files[] = $composerFiles[0][0];
1069
        }
1070
1071
        if (isset($composerFiles[1][1])) {
1072
            $files[] = $composerFiles[1][0];
1073
        }
1074
1075
        if (false === isset($raw->{$key})) {
1076
            return self::wrapInSplFileInfo($files);
1077
        }
1078
1079
        if ([] === (array) $raw->{$key}) {
1080
            return self::wrapInSplFileInfo($files);
1081
        }
1082
1083
        $files = array_merge((array) $raw->{$key}, $files);
1084
1085
        Assertion::allString($files);
1086
1087
        $normalizePath = static function (string $file) use ($basePath, $key, $excludedFiles): ?SplFileInfo {
1088
            $file = self::normalizePath($file, $basePath);
1089
1090
            Assertion::false(
1091
                is_link($file),
1092
                sprintf(
1093
                    'Cannot add the link "%s": links are not supported.',
1094
                    $file
1095
                )
1096
            );
1097
1098
            Assertion::file(
1099
                $file,
1100
                sprintf(
1101
                    '"%s" must contain a list of existing files. Could not find "%%s".',
1102
                    $key
1103
                )
1104
            );
1105
1106
            return array_key_exists($file, $excludedFiles) ? null : new SplFileInfo($file);
1107
        };
1108
1109
        return array_filter(array_map($normalizePath, $files));
1110
    }
1111
1112
    /**
1113
     * @param string   $key           Config property name
1114
     * @param string[] $excludedPaths
1115
     *
1116
     * @return iterable|SplFileInfo[]
1117
     */
1118
    private static function retrieveDirectories(
1119
        stdClass $raw,
1120
        string $key,
1121
        string $basePath,
1122
        Closure $blacklistFilter,
1123
        array $excludedPaths,
1124
        ConfigurationLogger $logger
1125
    ): iterable {
1126
        $directories = self::retrieveDirectoryPaths($raw, $key, $basePath, $logger);
1127
1128
        if ([] !== $directories) {
1129
            $finder = Finder::create()
1130
                ->files()
1131
                ->filter($blacklistFilter)
1132
                ->ignoreVCS(true)
1133
                ->in($directories)
1134
            ;
1135
1136
            foreach ($excludedPaths as $excludedPath) {
1137
                $finder->notPath($excludedPath);
1138
            }
1139
1140
            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[]|iterable.
Loading history...
1141
        }
1142
1143
        return [];
1144
    }
1145
1146
    /**
1147
     * @param string[] $devPackages
1148
     *
1149
     * @return iterable[]|SplFileInfo[][]
1150
     */
1151
    private static function retrieveFilesFromFinders(
1152
        stdClass $raw,
1153
        string $key,
1154
        string $basePath,
1155
        Closure $blacklistFilter,
1156
        array $devPackages,
1157
        ConfigurationLogger $logger
1158
    ): array {
1159
        self::checkIfDefaultValue($logger, $raw, $key, []);
1160
1161
        if (false === isset($raw->{$key})) {
1162
            return [];
1163
        }
1164
1165
        $finder = $raw->{$key};
1166
1167
        return self::processFinders($finder, $basePath, $blacklistFilter, $devPackages);
1168
    }
1169
1170
    /**
1171
     * @param iterable[]|SplFileInfo[][] $fileIterators
1172
     *
1173
     * @return SplFileInfo[]
1174
     */
1175
    private static function retrieveFilesAggregate(iterable ...$fileIterators): array
1176
    {
1177
        $files = [];
1178
1179
        foreach ($fileIterators as $fileIterator) {
1180
            foreach ($fileIterator as $file) {
1181
                $files[(string) $file] = $file;
1182
            }
1183
        }
1184
1185
        return array_values($files);
1186
    }
1187
1188
    /**
1189
     * @param string[] $devPackages
1190
     *
1191
     * @return Finder[]|SplFileInfo[][]
1192
     */
1193
    private static function processFinders(
1194
        array $findersConfig,
1195
        string $basePath,
1196
        Closure $blacklistFilter,
1197
        array $devPackages
1198
    ): array {
1199
        $processFinderConfig = static function (stdClass $config) use ($basePath, $blacklistFilter, $devPackages) {
1200
            return self::processFinder($config, $basePath, $blacklistFilter, $devPackages);
1201
        };
1202
1203
        return array_map($processFinderConfig, $findersConfig);
1204
    }
1205
1206
    /**
1207
     * @param string[] $devPackages
1208
     *
1209
     * @return Finder|SplFileInfo[]
1210
     */
1211
    private static function processFinder(
1212
        stdClass $config,
1213
        string $basePath,
1214
        Closure $blacklistFilter,
1215
        array $devPackages
1216
    ): Finder {
1217
        $finder = Finder::create()
1218
            ->files()
1219
            ->filter($blacklistFilter)
1220
            ->filter(
1221
                static function (SplFileInfo $fileInfo) use ($devPackages): bool {
1222
                    foreach ($devPackages as $devPackage) {
1223
                        if ($devPackage === longest_common_base_path([$devPackage, $fileInfo->getRealPath()])) {
1224
                            // File belongs to the dev package
1225
                            return false;
1226
                        }
1227
                    }
1228
1229
                    return true;
1230
                }
1231
            )
1232
            ->ignoreVCS(true)
1233
        ;
1234
1235
        $normalizedConfig = (static function (array $config, Finder $finder): array {
1236
            $normalizedConfig = [];
1237
1238
            foreach ($config as $method => $arguments) {
1239
                $method = trim($method);
1240
                $arguments = (array) $arguments;
1241
1242
                Assertion::methodExists(
1243
                    $method,
1244
                    $finder,
1245
                    'The method "Finder::%s" does not exist.'
1246
                );
1247
1248
                $normalizedConfig[$method] = $arguments;
1249
            }
1250
1251
            krsort($normalizedConfig);
1252
1253
            return $normalizedConfig;
1254
        })((array) $config, $finder);
1255
1256
        $createNormalizedDirectories = static function (string $directory) use ($basePath): ?string {
1257
            $directory = self::normalizePath($directory, $basePath);
1258
1259
            Assertion::false(
1260
                is_link($directory),
1261
                sprintf(
1262
                    'Cannot append the link "%s" to the Finder: links are not supported.',
1263
                    $directory
1264
                )
1265
            );
1266
1267
            Assertion::directory($directory);
1268
1269
            return $directory;
1270
        };
1271
1272
        $normalizeFileOrDirectory = static function (?string &$fileOrDirectory) use ($basePath, $blacklistFilter): void {
1273
            $fileOrDirectory = self::normalizePath($fileOrDirectory, $basePath);
0 ignored issues
show
Bug introduced by
It seems like $fileOrDirectory can also be of type null; however, parameter $file of KevinGH\Box\Configuration::normalizePath() 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

1273
            $fileOrDirectory = self::normalizePath(/** @scrutinizer ignore-type */ $fileOrDirectory, $basePath);
Loading history...
1274
1275
            Assertion::false(
1276
                is_link($fileOrDirectory),
1277
                sprintf(
1278
                    'Cannot append the link "%s" to the Finder: links are not supported.',
1279
                    $fileOrDirectory
1280
                )
1281
            );
1282
1283
            Assertion::true(
1284
                file_exists($fileOrDirectory),
1285
                sprintf(
1286
                    'Path "%s" was expected to be a file or directory. It may be a symlink (which are unsupported).',
1287
                    $fileOrDirectory
1288
                )
1289
            );
1290
1291
            if (false === is_file($fileOrDirectory)) {
1292
                Assertion::directory($fileOrDirectory);
1293
            } else {
1294
                Assertion::file($fileOrDirectory);
1295
            }
1296
1297
            if (false === $blacklistFilter(new SplFileInfo($fileOrDirectory))) {
1298
                $fileOrDirectory = null;
1299
            }
1300
        };
1301
1302
        foreach ($normalizedConfig as $method => $arguments) {
1303
            if ('in' === $method) {
1304
                $normalizedConfig[$method] = $arguments = array_map($createNormalizedDirectories, $arguments);
1305
            }
1306
1307
            if ('exclude' === $method) {
1308
                $arguments = array_unique(array_map('trim', $arguments));
1309
            }
1310
1311
            if ('append' === $method) {
1312
                array_walk($arguments, $normalizeFileOrDirectory);
1313
1314
                $arguments = [array_filter($arguments)];
1315
            }
1316
1317
            foreach ($arguments as $argument) {
1318
                $finder->$method($argument);
1319
            }
1320
        }
1321
1322
        return $finder;
1323
    }
1324
1325
    /**
1326
     * @param string[] $devPackages
1327
     * @param string[] $filesToAppend
1328
     *
1329
     * @return string[][]
1330
     */
1331
    private static function retrieveAllDirectoriesToInclude(
1332
        string $basePath,
1333
        ?array $decodedJsonContents,
1334
        array $devPackages,
1335
        array $filesToAppend,
1336
        array $excludedPaths
1337
    ): array {
1338
        $toString = static function ($file): string {
1339
            // @param string|SplFileInfo $file
1340
            return (string) $file;
1341
        };
1342
1343
        if (null !== $decodedJsonContents && array_key_exists('vendor-dir', $decodedJsonContents)) {
1344
            $vendorDir = self::normalizePath($decodedJsonContents['vendor-dir'], $basePath);
1345
        } else {
1346
            $vendorDir = self::normalizePath('vendor', $basePath);
1347
        }
1348
1349
        if (file_exists($vendorDir)) {
1350
            // The installed.json file is necessary for dumping the autoload correctly. Note however that it will not exists if no
1351
            // dependencies are included in the `composer.json`
1352
            $installedJsonFiles = self::normalizePath($vendorDir.'/composer/installed.json', $basePath);
1353
1354
            if (file_exists($installedJsonFiles)) {
1355
                $filesToAppend[] = $installedJsonFiles;
1356
            }
1357
1358
            $vendorPackages = toArray(values(map(
1359
                $toString,
1360
                Finder::create()
1361
                    ->in($vendorDir)
1362
                    ->directories()
1363
                    ->depth(1)
1364
                    ->ignoreUnreadableDirs()
1365
                    ->filter(
1366
                        static function (SplFileInfo $fileInfo): ?bool {
1367
                            if ($fileInfo->isLink()) {
1368
                                return false;
1369
                            }
1370
1371
                            return null;
1372
                        }
1373
                    )
1374
            )));
1375
1376
            $vendorPackages = array_diff($vendorPackages, $devPackages);
1377
1378
            if (null === $decodedJsonContents || false === array_key_exists('autoload', $decodedJsonContents)) {
1379
                $files = toArray(values(map(
1380
                    $toString,
1381
                    Finder::create()
1382
                        ->in($basePath)
1383
                        ->files()
1384
                        ->depth(0)
1385
                )));
1386
1387
                $directories = toArray(values(map(
1388
                    $toString,
1389
                    Finder::create()
1390
                        ->in($basePath)
1391
                        ->notPath('vendor')
1392
                        ->directories()
1393
                        ->depth(0)
1394
                )));
1395
1396
                return [
1397
                    array_merge($files, $filesToAppend),
1398
                    array_merge($directories, $vendorPackages),
1399
                ];
1400
            }
1401
1402
            $paths = $vendorPackages;
1403
        } else {
1404
            $paths = [];
1405
        }
1406
1407
        $autoload = $decodedJsonContents['autoload'] ?? [];
1408
1409
        if (array_key_exists('psr-4', $autoload)) {
1410
            foreach ($autoload['psr-4'] as $path) {
1411
                /** @var string|string[] $path */
1412
                $composerPaths = (array) $path;
1413
1414
                foreach ($composerPaths as $composerPath) {
1415
                    $paths[] = '' !== trim($composerPath) ? $composerPath : $basePath;
1416
                }
1417
            }
1418
        }
1419
1420
        if (array_key_exists('psr-0', $autoload)) {
1421
            foreach ($autoload['psr-0'] as $path) {
1422
                /** @var string|string[] $path */
1423
                $composerPaths = (array) $path;
1424
1425
                foreach ($composerPaths as $composerPath) {
1426
                    $paths[] = '' !== trim($composerPath) ? $composerPath : $basePath;
1427
                }
1428
            }
1429
        }
1430
1431
        if (array_key_exists('classmap', $autoload)) {
1432
            foreach ($autoload['classmap'] as $path) {
1433
                // @var string $path
1434
                $paths[] = $path;
1435
            }
1436
        }
1437
1438
        $normalizePath = static function (string $path) use ($basePath): string {
1439
            return is_absolute_path($path)
1440
                ? canonicalize($path)
1441
                : self::normalizePath(trim($path, '/ '), $basePath)
1442
            ;
1443
        };
1444
1445
        if (array_key_exists('files', $autoload)) {
1446
            foreach ($autoload['files'] as $path) {
1447
                // @var string $path
1448
                $path = $normalizePath($path);
1449
1450
                Assertion::file($path);
1451
                Assertion::false(is_link($path), 'Cannot add the link "'.$path.'": links are not supported.');
1452
1453
                $filesToAppend[] = $path;
1454
            }
1455
        }
1456
1457
        $files = $filesToAppend;
1458
        $directories = [];
1459
1460
        foreach ($paths as $path) {
1461
            $path = $normalizePath($path);
1462
1463
            Assertion::true(file_exists($path), 'File or directory "'.$path.'" was expected to exist.');
1464
            Assertion::false(is_link($path), 'Cannot add the link "'.$path.'": links are not supported.');
1465
1466
            if (is_file($path)) {
1467
                $files[] = $path;
1468
            } else {
1469
                $directories[] = $path;
1470
            }
1471
        }
1472
1473
        [$files, $directories] = [
1474
            array_unique($files),
1475
            array_unique($directories),
1476
        ];
1477
1478
        return [
1479
            array_diff($files, $excludedPaths),
1480
            array_diff($directories, $excludedPaths),
1481
        ];
1482
    }
1483
1484
    /**
1485
     * @param string[] $files
1486
     * @param string[] $directories
1487
     * @param string[] $excludedPaths
1488
     * @param string[] $devPackages
1489
     *
1490
     * @return SplFileInfo[]
1491
     */
1492
    private static function retrieveAllFiles(
1493
        string $basePath,
1494
        array $directories,
1495
        ?string $mainScriptPath,
1496
        Closure $blacklistFilter,
1497
        array $excludedPaths,
1498
        array $devPackages
1499
    ): iterable {
1500
        if ([] === $directories) {
1501
            return [];
1502
        }
1503
1504
        $relativeDevPackages = array_map(
1505
            static function (string $packagePath) use ($basePath): string {
1506
                return make_path_relative($packagePath, $basePath);
1507
            },
1508
            $devPackages
1509
        );
1510
1511
        $finder = Finder::create()
1512
            ->files()
1513
            ->filter($blacklistFilter)
1514
            ->exclude($relativeDevPackages)
1515
            ->ignoreVCS(true)
1516
            ->ignoreDotFiles(true)
1517
            // Remove build files
1518
            ->notName('composer.json')
1519
            ->notName('composer.lock')
1520
            ->notName('Makefile')
1521
            ->notName('Vagrantfile')
1522
            ->notName('phpstan*.neon*')
1523
            ->notName('infection*.json*')
1524
            ->notName('humbug*.json*')
1525
            ->notName('easy-coding-standard.neon*')
1526
            ->notName('phpbench.json*')
1527
            ->notName('phpcs.xml*')
1528
            ->notName('psalm.xml*')
1529
            ->notName('scoper.inc*')
1530
            ->notName('box*.json*')
1531
            ->notName('phpdoc*.xml*')
1532
            ->notName('codecov.yml*')
1533
            ->notName('Dockerfile')
1534
            ->exclude('build')
1535
            ->exclude('dist')
1536
            ->exclude('example')
1537
            ->exclude('examples')
1538
            // Remove documentation
1539
            ->notName('*.md')
1540
            ->notName('*.rst')
1541
            ->notName('/^readme(\..*+)?$/i')
1542
            ->notName('/^upgrade(\..*+)?$/i')
1543
            ->notName('/^contributing(\..*+)?$/i')
1544
            ->notName('/^changelog(\..*+)?$/i')
1545
            ->notName('/^authors?(\..*+)?$/i')
1546
            ->notName('/^conduct(\..*+)?$/i')
1547
            ->notName('/^todo(\..*+)?$/i')
1548
            ->exclude('doc')
1549
            ->exclude('docs')
1550
            ->exclude('documentation')
1551
            // Remove backup files
1552
            ->notName('*~')
1553
            ->notName('*.back')
1554
            ->notName('*.swp')
1555
            // Remove tests
1556
            ->notName('*Test.php')
1557
            ->exclude('test')
1558
            ->exclude('Test')
1559
            ->exclude('tests')
1560
            ->exclude('Tests')
1561
            ->notName('/phpunit.*\.xml(.dist)?/')
1562
            ->notName('/behat.*\.yml(.dist)?/')
1563
            ->exclude('spec')
1564
            ->exclude('specs')
1565
            ->exclude('features')
1566
            // Remove CI config
1567
            ->exclude('travis')
1568
            ->notName('travis.yml')
1569
            ->notName('appveyor.yml')
1570
            ->notName('build.xml*')
1571
        ;
1572
1573
        if (null !== $mainScriptPath) {
1574
            $finder->notPath(make_path_relative($mainScriptPath, $basePath));
1575
        }
1576
1577
        $finder->in($directories);
1578
1579
        $excludedPaths = array_unique(
1580
            array_filter(
1581
                array_map(
1582
                    static function (string $path) use ($basePath): string {
1583
                        return make_path_relative($path, $basePath);
1584
                    },
1585
                    $excludedPaths
1586
                ),
1587
                static function (string $path): bool {
1588
                    return '..' !== substr($path, 0, 2);
1589
                }
1590
            )
1591
        );
1592
1593
        foreach ($excludedPaths as $excludedPath) {
1594
            $finder->notPath($excludedPath);
1595
        }
1596
1597
        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...
1598
    }
1599
1600
    /**
1601
     * @param string $key Config property name
1602
     *
1603
     * @return string[]
1604
     */
1605
    private static function retrieveDirectoryPaths(
1606
        stdClass $raw,
1607
        string $key,
1608
        string $basePath,
1609
        ConfigurationLogger $logger
1610
    ): array {
1611
        self::checkIfDefaultValue($logger, $raw, $key, []);
1612
1613
        if (false === isset($raw->{$key})) {
1614
            return [];
1615
        }
1616
1617
        $directories = $raw->{$key};
1618
1619
        $normalizeDirectory = static function (string $directory) use ($basePath, $key): string {
1620
            $directory = self::normalizePath($directory, $basePath);
1621
1622
            Assertion::false(
1623
                is_link($directory),
1624
                sprintf(
1625
                    'Cannot add the link "%s": links are not supported.',
1626
                    $directory
1627
                )
1628
            );
1629
1630
            Assertion::directory(
1631
                $directory,
1632
                sprintf(
1633
                    '"%s" must contain a list of existing directories. Could not find "%%s".',
1634
                    $key
1635
                )
1636
            );
1637
1638
            return $directory;
1639
        };
1640
1641
        return array_map($normalizeDirectory, $directories);
1642
    }
1643
1644
    private static function normalizePath(string $file, string $basePath): string
1645
    {
1646
        return make_path_absolute(trim($file), $basePath);
1647
    }
1648
1649
    /**
1650
     * @param string[] $files
1651
     *
1652
     * @return SplFileInfo[]
1653
     */
1654
    private static function wrapInSplFileInfo(array $files): array
1655
    {
1656
        return array_map(
1657
            static function (string $file): SplFileInfo {
1658
                return new SplFileInfo($file);
1659
            },
1660
            $files
1661
        );
1662
    }
1663
1664
    private static function retrieveDumpAutoload(stdClass $raw, bool $composerJson, ConfigurationLogger $logger): bool
1665
    {
1666
        self::checkIfDefaultValue($logger, $raw, self::DUMP_AUTOLOAD_KEY, true);
1667
1668
        if (false === property_exists($raw, self::DUMP_AUTOLOAD_KEY)) {
1669
            return $composerJson;
1670
        }
1671
1672
        $dumpAutoload = $raw->{self::DUMP_AUTOLOAD_KEY} ?? true;
1673
1674
        if (false === $composerJson && $dumpAutoload) {
1675
            // TODO: use sprintf there; check other occurrences as well
1676
            $logger->addWarning(
1677
                'The "dump-autoload" setting has been set but has been ignored because the composer.json file necessary'
1678
                .' for it could not be found'
1679
            );
1680
1681
            return false;
1682
        }
1683
1684
        return $composerJson && false !== $dumpAutoload;
1685
    }
1686
1687
    private static function retrieveExcludeDevFiles(stdClass $raw, bool $dumpAutoload, ConfigurationLogger $logger): bool
1688
    {
1689
        self::checkIfDefaultValue($logger, $raw, self::EXCLUDE_DEV_FILES_KEY, $dumpAutoload);
1690
1691
        if (false === property_exists($raw, self::EXCLUDE_DEV_FILES_KEY)) {
1692
            return $dumpAutoload;
1693
        }
1694
1695
        $excludeDevFiles = $raw->{self::EXCLUDE_DEV_FILES_KEY} ?? $dumpAutoload;
1696
1697
        if (true === $excludeDevFiles && false === $dumpAutoload) {
1698
            $logger->addWarning(sprintf(
1699
                'The "%s" setting has been set but has been ignored because the Composer autoloader is not dumped',
1700
                self::EXCLUDE_DEV_FILES_KEY
1701
            ));
1702
1703
            return false;
1704
        }
1705
1706
        return $excludeDevFiles;
1707
    }
1708
1709
    private static function retrieveExcludeComposerFiles(stdClass $raw, ConfigurationLogger $logger): bool
1710
    {
1711
        self::checkIfDefaultValue($logger, $raw, self::EXCLUDE_COMPOSER_FILES_KEY, true);
1712
1713
        return $raw->{self::EXCLUDE_COMPOSER_FILES_KEY} ?? true;
1714
    }
1715
1716
    /**
1717
     * @return Compactor[]
1718
     */
1719
    private static function retrieveCompactors(stdClass $raw, string $basePath, ConfigurationLogger $logger): array
1720
    {
1721
        self::checkIfDefaultValue($logger, $raw, self::COMPACTORS_KEY, []);
1722
1723
        $compactorClasses = array_unique((array) ($raw->{self::COMPACTORS_KEY} ?? []));
1724
1725
        $ignoredAnnotations = self::retrievePhpCompactorIgnoredAnnotations($raw, $compactorClasses, $logger);
1726
1727
        if (false === isset($raw->{self::COMPACTORS_KEY})) {
1728
            return [];
1729
        }
1730
1731
        $compactors = array_map(
1732
            static function (string $class) use ($raw, $basePath, $logger, $ignoredAnnotations): Compactor {
1733
                Assertion::classExists($class, 'The compactor class "%s" does not exist.');
1734
                Assertion::implementsInterface($class, Compactor::class, 'The class "%s" is not a compactor class.');
1735
1736
                if (LegacyPhp::class === $class) {
1737
                    $logger->addRecommendation(
1738
                        sprintf(
1739
                            'The compactor "%s" has been deprecated, use "%s" instead.',
1740
                            LegacyPhp::class,
1741
                            PhpCompactor::class
1742
                        )
1743
                    );
1744
                }
1745
1746
                if (LegacyJson::class === $class) {
1747
                    $logger->addRecommendation(
1748
                        sprintf(
1749
                            'The compactor "%s" has been deprecated, use "%s" instead.',
1750
                            LegacyJson::class,
1751
                            JsonCompactor::class
1752
                        )
1753
                    );
1754
                }
1755
1756
                if (PhpCompactor::class === $class || LegacyPhp::class === $class) {
1757
                    return self::createPhpCompactor($ignoredAnnotations);
1758
                }
1759
1760
                if (PhpScoperCompactor::class === $class) {
1761
                    return self::createPhpScoperCompactor($raw, $basePath, $logger);
1762
                }
1763
1764
                return new $class();
1765
            },
1766
            $compactorClasses
1767
        );
1768
1769
        $scoperCompactor = false;
1770
1771
        foreach ($compactors as $compactor) {
1772
            if ($compactor instanceof PhpScoperCompactor) {
1773
                $scoperCompactor = true;
1774
            }
1775
1776
            if ($compactor instanceof PhpCompactor) {
1777
                if (true === $scoperCompactor) {
1778
                    $logger->addRecommendation(
1779
                        sprintf(
1780
                            'The PHP compactor has been registered after the PhpScoper compactor. It is '
1781
                            .'recommended to register the PHP compactor before for a clearer code and faster processing.'
1782
                        )
1783
                    );
1784
                }
1785
1786
                break;
1787
            }
1788
        }
1789
1790
        return $compactors;
1791
    }
1792
1793
    private static function retrieveCompressionAlgorithm(stdClass $raw, ConfigurationLogger $logger): ?int
1794
    {
1795
        self::checkIfDefaultValue($logger, $raw, self::COMPRESSION_KEY, 'NONE');
1796
1797
        if (false === isset($raw->{self::COMPRESSION_KEY})) {
1798
            return null;
1799
        }
1800
1801
        $knownAlgorithmNames = array_keys(get_phar_compression_algorithms());
1802
1803
        Assertion::inArray(
1804
            $raw->{self::COMPRESSION_KEY},
1805
            $knownAlgorithmNames,
1806
            sprintf(
1807
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
1808
                implode('", "', $knownAlgorithmNames)
1809
            )
1810
        );
1811
1812
        $value = get_phar_compression_algorithms()[$raw->{self::COMPRESSION_KEY}];
1813
1814
        // Phar::NONE is not valid for compressFiles()
1815
        if (Phar::NONE === $value) {
1816
            return null;
1817
        }
1818
1819
        return $value;
1820
    }
1821
1822
    private static function retrieveFileMode(stdClass $raw, ConfigurationLogger $logger): ?int
1823
    {
1824
        if (property_exists($raw, self::CHMOD_KEY) && null === $raw->{self::CHMOD_KEY}) {
1825
            self::addRecommendationForDefaultValue($logger, self::CHMOD_KEY);
1826
        }
1827
1828
        $defaultChmod = intval(0755, 8);
1829
1830
        if (isset($raw->{self::CHMOD_KEY})) {
1831
            $chmod = intval($raw->{self::CHMOD_KEY}, 8);
1832
1833
            if ($defaultChmod === $chmod) {
1834
                self::addRecommendationForDefaultValue($logger, self::CHMOD_KEY);
1835
            }
1836
1837
            return $chmod;
1838
        }
1839
1840
        return $defaultChmod;
1841
    }
1842
1843
    private static function retrieveMainScriptPath(
1844
        stdClass $raw,
1845
        string $basePath,
1846
        ?array $decodedJsonContents,
1847
        ConfigurationLogger $logger
1848
    ): ?string {
1849
        $firstBin = false;
1850
1851
        if (null !== $decodedJsonContents && array_key_exists('bin', $decodedJsonContents)) {
1852
            /** @var false|string $firstBin */
1853
            $firstBin = current((array) $decodedJsonContents['bin']);
1854
1855
            if (false !== $firstBin) {
1856
                $firstBin = self::normalizePath($firstBin, $basePath);
1857
            }
1858
        }
1859
1860
        if (isset($raw->{self::MAIN_KEY})) {
1861
            $main = $raw->{self::MAIN_KEY};
1862
1863
            if (is_string($main)) {
1864
                $main = self::normalizePath($main, $basePath);
1865
1866
                if ($main === $firstBin) {
1867
                    $logger->addRecommendation('The "main" setting can be omitted since is set to its default value');
1868
                }
1869
            }
1870
        } else {
1871
            $main = false !== $firstBin ? $firstBin : self::normalizePath(self::DEFAULT_MAIN_SCRIPT, $basePath);
1872
        }
1873
1874
        if (is_bool($main)) {
1875
            Assertion::false(
1876
                $main,
1877
                'Cannot "enable" a main script: either disable it with `false` or give the main script file path.'
1878
            );
1879
1880
            return null;
1881
        }
1882
1883
        Assertion::file($main);
1884
1885
        return $main;
1886
    }
1887
1888
    private static function retrieveMainScriptContents(?string $mainScriptPath): ?string
1889
    {
1890
        if (null === $mainScriptPath) {
1891
            return null;
1892
        }
1893
1894
        $contents = file_contents($mainScriptPath);
1895
1896
        // Remove the shebang line: the shebang line in a PHAR should be located in the stub file which is the real
1897
        // PHAR entry point file.
1898
        // If one needs the shebang, then the main file should act as the stub and be registered as such and in which
1899
        // case the main script can be ignored or disabled.
1900
        return preg_replace('/^#!.*\s*/', '', $contents);
1901
    }
1902
1903
    /**
1904
     * @return string|null[][]
1905
     */
1906
    private static function retrieveComposerFiles(string $basePath): array
1907
    {
1908
        $retrieveFileAndContents = static function (string $file): array {
1909
            $json = new Json();
1910
1911
            if (false === file_exists($file) || false === is_file($file) || false === is_readable($file)) {
1912
                return [null, null];
1913
            }
1914
1915
            try {
1916
                $contents = $json->decodeFile($file, true);
1917
            } catch (ParsingException $exception) {
1918
                throw new InvalidArgumentException(
1919
                    sprintf(
1920
                        'Expected the file "%s" to be a valid composer.json file but an error has been found: %s',
1921
                        $file,
1922
                        $exception->getMessage()
1923
                    ),
1924
                    0,
1925
                    $exception
1926
                );
1927
            }
1928
1929
            return [$file, $contents];
1930
        };
1931
1932
        return [
1933
            $retrieveFileAndContents(canonicalize($basePath.'/composer.json')),
1934
            $retrieveFileAndContents(canonicalize($basePath.'/composer.lock')),
1935
            $retrieveFileAndContents(canonicalize($basePath.'/vendor/composer/installed.json')),
1936
        ];
1937
    }
1938
1939
    /**
1940
     * @return string[][]
1941
     */
1942
    private static function retrieveMap(stdClass $raw, ConfigurationLogger $logger): array
1943
    {
1944
        self::checkIfDefaultValue($logger, $raw, self::MAP_KEY, []);
1945
1946
        if (false === isset($raw->{self::MAP_KEY})) {
1947
            return [];
1948
        }
1949
1950
        $map = [];
1951
        $rawMap = (array) $raw->{self::MAP_KEY};
1952
1953
        foreach ($rawMap as $item) {
1954
            $processed = [];
1955
1956
            foreach ($item as $match => $replace) {
1957
                $processed[canonicalize(trim($match))] = canonicalize(trim($replace));
1958
            }
1959
1960
            if (isset($processed['_empty_'])) {
1961
                $processed[''] = $processed['_empty_'];
1962
1963
                unset($processed['_empty_']);
1964
            }
1965
1966
            $map[] = $processed;
1967
        }
1968
1969
        return $map;
1970
    }
1971
1972
    /**
1973
     * @return mixed
1974
     */
1975
    private static function retrieveMetadata(stdClass $raw, ConfigurationLogger $logger)
1976
    {
1977
        self::checkIfDefaultValue($logger, $raw, self::METADATA_KEY);
1978
1979
        if (false === isset($raw->{self::METADATA_KEY})) {
1980
            return null;
1981
        }
1982
1983
        $metadata = $raw->{self::METADATA_KEY};
1984
1985
        return is_object($metadata) ? (array) $metadata : $metadata;
1986
    }
1987
1988
    /**
1989
     * @return string[] The first element is the temporary output path and the second the final one
1990
     */
1991
    private static function retrieveOutputPath(
1992
        stdClass $raw,
1993
        string $basePath,
1994
        ?string $mainScriptPath,
1995
        ConfigurationLogger $logger
1996
    ): array {
1997
        $defaultPath = null;
1998
1999
        if (null !== $mainScriptPath
2000
            && 1 === preg_match('/^(?<main>.*?)(?:\.[\p{L}\d]+)?$/u', $mainScriptPath, $matches)
2001
        ) {
2002
            $defaultPath = $matches['main'].'.phar';
2003
        }
2004
2005
        if (isset($raw->{self::OUTPUT_KEY})) {
2006
            $path = self::normalizePath($raw->{self::OUTPUT_KEY}, $basePath);
2007
2008
            if ($path === $defaultPath) {
2009
                self::addRecommendationForDefaultValue($logger, self::OUTPUT_KEY);
2010
            }
2011
        } elseif (null !== $defaultPath) {
2012
            $path = $defaultPath;
2013
        } else {
2014
            // Last resort, should not happen
2015
            $path = self::normalizePath(self::DEFAULT_OUTPUT_FALLBACK, $basePath);
2016
        }
2017
2018
        $tmp = $real = $path;
2019
2020
        if ('.phar' !== substr($real, -5)) {
2021
            $tmp .= '.phar';
2022
        }
2023
2024
        return [$tmp, $real];
2025
    }
2026
2027
    private static function retrievePrivateKeyPath(
2028
        stdClass $raw,
2029
        string $basePath,
2030
        int $signingAlgorithm,
2031
        ConfigurationLogger $logger
2032
    ): ?string {
2033
        if (property_exists($raw, self::KEY_KEY) && Phar::OPENSSL !== $signingAlgorithm) {
2034
            if (null === $raw->{self::KEY_KEY}) {
2035
                $logger->addRecommendation(
2036
                    'The setting "key" has been set but is unnecessary since the signing algorithm is not "OPENSSL".'
2037
                );
2038
            } else {
2039
                $logger->addWarning(
2040
                    'The setting "key" has been set but is ignored since the signing algorithm is not "OPENSSL".'
2041
                );
2042
            }
2043
2044
            return null;
2045
        }
2046
2047
        if (!isset($raw->{self::KEY_KEY})) {
2048
            Assertion::true(
2049
                Phar::OPENSSL !== $signingAlgorithm,
2050
                'Expected to have a private key for OpenSSL signing but none have been provided.'
2051
            );
2052
2053
            return null;
2054
        }
2055
2056
        $path = self::normalizePath($raw->{self::KEY_KEY}, $basePath);
2057
2058
        Assertion::file($path);
2059
2060
        return $path;
2061
    }
2062
2063
    private static function retrievePrivateKeyPassphrase(
2064
        stdClass $raw,
2065
        int $algorithm,
2066
        ConfigurationLogger $logger
2067
    ): ?string {
2068
        self::checkIfDefaultValue($logger, $raw, self::KEY_PASS_KEY);
2069
2070
        if (false === property_exists($raw, self::KEY_PASS_KEY)) {
2071
            return null;
2072
        }
2073
2074
        /** @var null|false|string $keyPass */
2075
        $keyPass = $raw->{self::KEY_PASS_KEY};
2076
2077
        if (Phar::OPENSSL !== $algorithm) {
2078
            if (false === $keyPass || null === $keyPass) {
2079
                $logger->addRecommendation(
2080
                    sprintf(
2081
                        'The setting "%s" has been set but is unnecessary since the signing algorithm is '
2082
                        .'not "OPENSSL".',
2083
                        self::KEY_PASS_KEY
2084
                    )
2085
                );
2086
            } else {
2087
                $logger->addWarning(
2088
                    sprintf(
2089
                        'The setting "%s" has been set but ignored the signing algorithm is not "OPENSSL".',
2090
                        self::KEY_PASS_KEY
2091
                    )
2092
                );
2093
            }
2094
2095
            return null;
2096
        }
2097
2098
        return is_string($keyPass) ? $keyPass : null;
2099
    }
2100
2101
    /**
2102
     * @return scalar[]
2103
     */
2104
    private static function retrieveReplacements(stdClass $raw, ?string $file, ConfigurationLogger $logger): array
2105
    {
2106
        self::checkIfDefaultValue($logger, $raw, self::REPLACEMENTS_KEY, new stdClass());
2107
2108
        if (null === $file) {
2109
            return [];
2110
        }
2111
2112
        $replacements = isset($raw->{self::REPLACEMENTS_KEY}) ? (array) $raw->{self::REPLACEMENTS_KEY} : [];
2113
2114
        if (null !== ($git = self::retrievePrettyGitPlaceholder($raw, $logger))) {
2115
            $replacements[$git] = self::retrievePrettyGitTag($file);
2116
        }
2117
2118
        if (null !== ($git = self::retrieveGitHashPlaceholder($raw, $logger))) {
2119
            $replacements[$git] = self::retrieveGitHash($file);
2120
        }
2121
2122
        if (null !== ($git = self::retrieveGitShortHashPlaceholder($raw, $logger))) {
2123
            $replacements[$git] = self::retrieveGitHash($file, true);
2124
        }
2125
2126
        if (null !== ($git = self::retrieveGitTagPlaceholder($raw, $logger))) {
2127
            $replacements[$git] = self::retrieveGitTag($file);
2128
        }
2129
2130
        if (null !== ($git = self::retrieveGitVersionPlaceholder($raw, $logger))) {
2131
            $replacements[$git] = self::retrieveGitVersion($file);
2132
        }
2133
2134
        /**
2135
         * @var string
2136
         * @var bool   $valueSetByUser
2137
         */
2138
        [$datetimeFormat, $valueSetByUser] = self::retrieveDatetimeFormat($raw, $logger);
2139
2140
        if (null !== ($date = self::retrieveDatetimeNowPlaceHolder($raw, $logger))) {
2141
            $replacements[$date] = self::retrieveDatetimeNow($datetimeFormat);
2142
        } elseif ($valueSetByUser) {
2143
            $logger->addRecommendation(
2144
                sprintf(
2145
                    'The setting "%s" has been set but is unnecessary because the setting "%s" is not set.',
2146
                    self::DATETIME_FORMAT_KEY,
2147
                    self::DATETIME_KEY
2148
                )
2149
            );
2150
        }
2151
2152
        $sigil = self::retrieveReplacementSigil($raw, $logger);
2153
2154
        foreach ($replacements as $key => $value) {
2155
            unset($replacements[$key]);
2156
            $replacements[$sigil.$key.$sigil] = $value;
2157
        }
2158
2159
        return $replacements;
2160
    }
2161
2162
    private static function retrievePrettyGitPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
2163
    {
2164
        return self::retrievePlaceholder($raw, $logger, self::GIT_KEY);
2165
    }
2166
2167
    private static function retrieveGitHashPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
2168
    {
2169
        return self::retrievePlaceholder($raw, $logger, self::GIT_COMMIT_KEY);
2170
    }
2171
2172
    /**
2173
     * @param bool $short Use the short version
2174
     *
2175
     * @return string the commit hash
2176
     */
2177
    private static function retrieveGitHash(string $file, bool $short = false): string
2178
    {
2179
        return self::runGitCommand(
2180
            sprintf(
2181
                'git log --pretty="%s" -n1 HEAD',
2182
                $short ? '%h' : '%H'
2183
            ),
2184
            $file
2185
        );
2186
    }
2187
2188
    private static function retrieveGitShortHashPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
2189
    {
2190
        return self::retrievePlaceholder($raw, $logger, self::GIT_COMMIT_SHORT_KEY);
2191
    }
2192
2193
    private static function retrieveGitTagPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
2194
    {
2195
        return self::retrievePlaceholder($raw, $logger, self::GIT_TAG_KEY);
2196
    }
2197
2198
    private static function retrievePlaceholder(stdClass $raw, ConfigurationLogger $logger, string $key): ?string
2199
    {
2200
        self::checkIfDefaultValue($logger, $raw, $key);
2201
2202
        return $raw->{$key} ?? null;
2203
    }
2204
2205
    private static function retrieveGitTag(string $file): string
2206
    {
2207
        return self::runGitCommand('git describe --tags HEAD', $file);
2208
    }
2209
2210
    private static function retrievePrettyGitTag(string $file): string
2211
    {
2212
        $version = self::retrieveGitTag($file);
2213
2214
        if (preg_match('/^(?<tag>.+)-\d+-g(?<hash>[a-f0-9]{7})$/', $version, $matches)) {
2215
            return sprintf('%s@%s', $matches['tag'], $matches['hash']);
2216
        }
2217
2218
        return $version;
2219
    }
2220
2221
    private static function retrieveGitVersionPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
2222
    {
2223
        return self::retrievePlaceholder($raw, $logger, self::GIT_VERSION_KEY);
2224
    }
2225
2226
    private static function retrieveGitVersion(string $file): ?string
2227
    {
2228
        try {
2229
            return self::retrieveGitTag($file);
2230
        } catch (RuntimeException $exception) {
2231
            try {
2232
                return self::retrieveGitHash($file, true);
2233
            } catch (RuntimeException $exception) {
2234
                throw new RuntimeException(
2235
                    sprintf(
2236
                        'The tag or commit hash could not be retrieved from "%s": %s',
2237
                        dirname($file),
2238
                        $exception->getMessage()
2239
                    ),
2240
                    0,
2241
                    $exception
2242
                );
2243
            }
2244
        }
2245
    }
2246
2247
    private static function retrieveDatetimeNowPlaceHolder(stdClass $raw, ConfigurationLogger $logger): ?string
2248
    {
2249
        return self::retrievePlaceholder($raw, $logger, self::DATETIME_KEY);
2250
    }
2251
2252
    private static function retrieveDatetimeNow(string $format): string
2253
    {
2254
        $now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
2255
2256
        return $now->format($format);
2257
    }
2258
2259
    private static function retrieveDatetimeFormat(stdClass $raw, ConfigurationLogger $logger): array
2260
    {
2261
        self::checkIfDefaultValue($logger, $raw, self::DATETIME_FORMAT_KEY, self::DEFAULT_DATETIME_FORMAT);
2262
        self::checkIfDefaultValue($logger, $raw, self::DATETIME_FORMAT_KEY, self::DATETIME_FORMAT_DEPRECATED_KEY);
2263
2264
        if (isset($raw->{self::DATETIME_FORMAT_KEY})) {
2265
            $format = $raw->{self::DATETIME_FORMAT_KEY};
2266
        } elseif (isset($raw->{self::DATETIME_FORMAT_DEPRECATED_KEY})) {
2267
            @trigger_error(
2268
                'The "datetime_format" is deprecated, use "datetime-format" setting instead.',
2269
                E_USER_DEPRECATED
2270
            );
2271
            $logger->addWarning('The "datetime_format" is deprecated, use "datetime-format" setting instead.');
2272
2273
            $format = $raw->{self::DATETIME_FORMAT_DEPRECATED_KEY};
2274
        } else {
2275
            $format = null;
2276
        }
2277
2278
        if (null !== $format) {
2279
            $formattedDate = (new DateTimeImmutable())->format($format);
2280
2281
            Assertion::false(
2282
                false === $formattedDate || $formattedDate === $format,
2283
                sprintf(
2284
                    'Expected the datetime format to be a valid format: "%s" is not',
2285
                    $format
2286
                )
2287
            );
2288
2289
            return [$format, true];
2290
        }
2291
2292
        return [self::DEFAULT_DATETIME_FORMAT, false];
2293
    }
2294
2295
    private static function retrieveReplacementSigil(stdClass $raw, ConfigurationLogger $logger): string
2296
    {
2297
        return self::retrievePlaceholder($raw, $logger, self::REPLACEMENT_SIGIL_KEY) ?? self::DEFAULT_REPLACEMENT_SIGIL;
2298
    }
2299
2300
    private static function retrieveShebang(stdClass $raw, bool $stubIsGenerated, ConfigurationLogger $logger): ?string
2301
    {
2302
        self::checkIfDefaultValue($logger, $raw, self::SHEBANG_KEY, self::DEFAULT_SHEBANG);
2303
2304
        if (false === isset($raw->{self::SHEBANG_KEY})) {
2305
            return self::DEFAULT_SHEBANG;
2306
        }
2307
2308
        $shebang = $raw->{self::SHEBANG_KEY};
2309
2310
        if (false === $shebang) {
2311
            if (false === $stubIsGenerated) {
2312
                $logger->addRecommendation(
2313
                    sprintf(
2314
                        'The "%s" has been set to `false` but is unnecessary since the Box built-in stub is not'
2315
                        .' being used',
2316
                        self::SHEBANG_KEY
2317
                    )
2318
                );
2319
            }
2320
2321
            return null;
2322
        }
2323
2324
        Assertion::string($shebang, 'Expected shebang to be either a string, false or null, found true');
2325
2326
        $shebang = trim($shebang);
2327
2328
        Assertion::notEmpty($shebang, 'The shebang should not be empty.');
2329
        Assertion::true(
2330
            '#!' === substr($shebang, 0, 2),
2331
            sprintf(
2332
                'The shebang line must start with "#!". Got "%s" instead',
2333
                $shebang
2334
            )
2335
        );
2336
2337
        if (false === $stubIsGenerated) {
2338
            $logger->addWarning(
2339
                sprintf(
2340
                    'The "%s" has been set but ignored since it is used only with the Box built-in stub which is not'
2341
                    .' used',
2342
                    self::SHEBANG_KEY
2343
                )
2344
            );
2345
        }
2346
2347
        return $shebang;
2348
    }
2349
2350
    private static function retrieveSigningAlgorithm(stdClass $raw, ConfigurationLogger $logger): int
2351
    {
2352
        if (property_exists($raw, self::ALGORITHM_KEY) && null === $raw->{self::ALGORITHM_KEY}) {
2353
            self::addRecommendationForDefaultValue($logger, self::ALGORITHM_KEY);
2354
        }
2355
2356
        if (false === isset($raw->{self::ALGORITHM_KEY})) {
2357
            return self::DEFAULT_SIGNING_ALGORITHM;
2358
        }
2359
2360
        $algorithm = strtoupper($raw->{self::ALGORITHM_KEY});
2361
2362
        Assertion::inArray($algorithm, array_keys(get_phar_signing_algorithms()));
2363
2364
        Assertion::true(
2365
            defined('Phar::'.$algorithm),
2366
            sprintf(
2367
                'The signing algorithm "%s" is not supported by your current PHAR version.',
2368
                $algorithm
2369
            )
2370
        );
2371
2372
        $algorithm = constant('Phar::'.$algorithm);
2373
2374
        if (self::DEFAULT_SIGNING_ALGORITHM === $algorithm) {
2375
            self::addRecommendationForDefaultValue($logger, self::ALGORITHM_KEY);
2376
        }
2377
2378
        return $algorithm;
2379
    }
2380
2381
    private static function retrieveStubBannerContents(stdClass $raw, bool $stubIsGenerated, ConfigurationLogger $logger): ?string
2382
    {
2383
        self::checkIfDefaultValue($logger, $raw, self::BANNER_KEY, self::getDefaultBanner());
2384
2385
        if (false === isset($raw->{self::BANNER_KEY})) {
2386
            return self::getDefaultBanner();
2387
        }
2388
2389
        $banner = $raw->{self::BANNER_KEY};
2390
2391
        if (false === $banner) {
2392
            if (false === $stubIsGenerated) {
2393
                $logger->addRecommendation(
2394
                    sprintf(
2395
                        'The "%s" setting has been set but is unnecessary since the Box built-in stub is not '
2396
                        .'being used',
2397
                        self::BANNER_KEY
2398
                    )
2399
                );
2400
            }
2401
2402
            return null;
2403
        }
2404
2405
        Assertion::true(is_string($banner) || is_array($banner), 'The banner cannot accept true as a value');
2406
2407
        if (is_array($banner)) {
2408
            $banner = implode("\n", $banner);
2409
        }
2410
2411
        if (false === $stubIsGenerated) {
2412
            $logger->addWarning(
2413
                sprintf(
2414
                    'The "%s" setting has been set but is ignored since the Box built-in stub is not being used',
2415
                    self::BANNER_KEY
2416
                )
2417
            );
2418
        }
2419
2420
        return $banner;
2421
    }
2422
2423
    private static function getDefaultBanner(): string
2424
    {
2425
        return sprintf(self::DEFAULT_BANNER, get_box_version());
2426
    }
2427
2428
    private static function retrieveStubBannerPath(
2429
        stdClass $raw,
2430
        string $basePath,
2431
        bool $stubIsGenerated,
2432
        ConfigurationLogger $logger
2433
    ): ?string {
2434
        self::checkIfDefaultValue($logger, $raw, self::BANNER_FILE_KEY);
2435
2436
        if (false === isset($raw->{self::BANNER_FILE_KEY})) {
2437
            return null;
2438
        }
2439
2440
        $bannerFile = make_path_absolute($raw->{self::BANNER_FILE_KEY}, $basePath);
2441
2442
        Assertion::file($bannerFile);
2443
2444
        if (false === $stubIsGenerated) {
2445
            $logger->addWarning(
2446
                sprintf(
2447
                    'The "%s" setting has been set but is ignored since the Box built-in stub is not being used',
2448
                    self::BANNER_FILE_KEY
2449
                )
2450
            );
2451
        }
2452
2453
        return $bannerFile;
2454
    }
2455
2456
    private static function normalizeStubBannerContents(?string $contents): ?string
2457
    {
2458
        if (null === $contents) {
2459
            return null;
2460
        }
2461
2462
        $banner = explode("\n", $contents);
2463
        $banner = array_map('trim', $banner);
2464
2465
        return implode("\n", $banner);
2466
    }
2467
2468
    private static function retrieveStubPath(stdClass $raw, string $basePath, ConfigurationLogger $logger): ?string
2469
    {
2470
        self::checkIfDefaultValue($logger, $raw, self::STUB_KEY);
2471
2472
        if (isset($raw->{self::STUB_KEY}) && is_string($raw->{self::STUB_KEY})) {
2473
            $stubPath = make_path_absolute($raw->{self::STUB_KEY}, $basePath);
2474
2475
            Assertion::file($stubPath);
2476
2477
            return $stubPath;
2478
        }
2479
2480
        return null;
2481
    }
2482
2483
    private static function retrieveInterceptsFileFunctions(
2484
        stdClass $raw,
2485
        bool $stubIsGenerated,
2486
        ConfigurationLogger $logger
2487
    ): bool {
2488
        self::checkIfDefaultValue($logger, $raw, self::INTERCEPT_KEY, false);
2489
2490
        if (false === isset($raw->{self::INTERCEPT_KEY})) {
2491
            return false;
2492
        }
2493
2494
        $intercept = $raw->{self::INTERCEPT_KEY};
2495
2496
        if ($intercept && false === $stubIsGenerated) {
2497
            $logger->addWarning(
2498
                sprintf(
2499
                    'The "%s" setting has been set but is ignored since the Box built-in stub is not being used',
2500
                    self::INTERCEPT_KEY
2501
                )
2502
            );
2503
        }
2504
2505
        return $intercept;
2506
    }
2507
2508
    private static function retrievePromptForPrivateKey(
2509
        stdClass $raw,
2510
        int $signingAlgorithm,
2511
        ConfigurationLogger $logger
2512
    ): bool {
2513
        if (isset($raw->{self::KEY_PASS_KEY}) && true === $raw->{self::KEY_PASS_KEY}) {
2514
            if (Phar::OPENSSL !== $signingAlgorithm) {
2515
                $logger->addWarning(
2516
                    'A prompt for password for the private key has been requested but ignored since the signing '
2517
                    .'algorithm used is not "OPENSSL.'
2518
                );
2519
2520
                return false;
2521
            }
2522
2523
            return true;
2524
        }
2525
2526
        return false;
2527
    }
2528
2529
    private static function retrieveIsStubGenerated(stdClass $raw, ?string $stubPath, ConfigurationLogger $logger): bool
2530
    {
2531
        self::checkIfDefaultValue($logger, $raw, self::STUB_KEY, true);
2532
2533
        return null === $stubPath && (false === isset($raw->{self::STUB_KEY}) || false !== $raw->{self::STUB_KEY});
2534
    }
2535
2536
    private static function retrieveCheckRequirements(
2537
        stdClass $raw,
2538
        bool $hasComposerJson,
2539
        bool $hasComposerLock,
2540
        bool $pharStubUsed,
2541
        ConfigurationLogger $logger
2542
    ): bool {
2543
        self::checkIfDefaultValue($logger, $raw, self::CHECK_REQUIREMENTS_KEY, true);
2544
2545
        if (false === property_exists($raw, self::CHECK_REQUIREMENTS_KEY)) {
2546
            return $hasComposerJson || $hasComposerLock;
2547
        }
2548
2549
        /** @var bool $checkRequirements */
2550
        $checkRequirements = $raw->{self::CHECK_REQUIREMENTS_KEY} ?? true;
2551
2552
        if ($checkRequirements && false === $hasComposerJson && false === $hasComposerLock) {
2553
            $logger->addWarning(
2554
                'The requirement checker could not be used because the composer.json and composer.lock file could not '
2555
                .'be found.'
2556
            );
2557
2558
            return false;
2559
        }
2560
2561
        if ($checkRequirements && $pharStubUsed) {
2562
            $logger->addWarning(
2563
                sprintf(
2564
                    'The "%s" setting has been set but has been ignored since the PHAR built-in stub is being '
2565
                    .'used.',
2566
                    self::CHECK_REQUIREMENTS_KEY
2567
                )
2568
            );
2569
        }
2570
2571
        return $checkRequirements;
2572
    }
2573
2574
    private static function retrievePhpScoperConfig(stdClass $raw, string $basePath, ConfigurationLogger $logger): PhpScoperConfiguration
2575
    {
2576
        self::checkIfDefaultValue($logger, $raw, self::PHP_SCOPER_KEY, self::PHP_SCOPER_CONFIG);
2577
2578
        if (!isset($raw->{self::PHP_SCOPER_KEY})) {
2579
            $configFilePath = make_path_absolute(self::PHP_SCOPER_CONFIG, $basePath);
2580
2581
            return file_exists($configFilePath)
2582
                ? PhpScoperConfiguration::load($configFilePath)
2583
                : PhpScoperConfiguration::load()
2584
             ;
2585
        }
2586
2587
        $configFile = $raw->{self::PHP_SCOPER_KEY};
2588
2589
        Assertion::string($configFile);
2590
2591
        $configFilePath = make_path_absolute($configFile, $basePath);
2592
2593
        Assertion::file($configFilePath);
2594
        Assertion::readable($configFilePath);
2595
2596
        return PhpScoperConfiguration::load($configFilePath);
2597
    }
2598
2599
    /**
2600
     * Runs a Git command on the repository.
2601
     *
2602
     * @return string The trimmed output from the command
2603
     */
2604
    private static function runGitCommand(string $command, string $file): string
2605
    {
2606
        $path = dirname($file);
2607
2608
        $process = Process::fromShellCommandline($command, $path);
2609
2610
        if (0 === $process->run()) {
2611
            return trim($process->getOutput());
2612
        }
2613
2614
        throw new RuntimeException(
2615
            sprintf(
2616
                'The tag or commit hash could not be retrieved from "%s": %s',
2617
                $path,
2618
                $process->getErrorOutput()
2619
            )
2620
        );
2621
    }
2622
2623
    /**
2624
     * @param string[] $compactorClasses
2625
     *
2626
     * @return string[]
2627
     */
2628
    private static function retrievePhpCompactorIgnoredAnnotations(
2629
        stdClass $raw,
2630
        array $compactorClasses,
2631
        ConfigurationLogger $logger
2632
    ): array {
2633
        $hasPhpCompactor = in_array(PhpCompactor::class, $compactorClasses, true) || in_array(LegacyPhp::class, $compactorClasses, true);
2634
2635
        self::checkIfDefaultValue($logger, $raw, self::ANNOTATIONS_KEY, true);
2636
        self::checkIfDefaultValue($logger, $raw, self::ANNOTATIONS_KEY, null);
2637
2638
        if (false === property_exists($raw, self::ANNOTATIONS_KEY)) {
2639
            return self::DEFAULT_IGNORED_ANNOTATIONS;
2640
        }
2641
2642
        if (false === $hasPhpCompactor) {
2643
            $logger->addWarning(
2644
                sprintf(
2645
                    'The "%s" setting has been set but is ignored since no PHP compactor has been configured',
2646
                    self::ANNOTATIONS_KEY
2647
                )
2648
            );
2649
        }
2650
2651
        /** @var null|bool|stdClass $annotations */
2652
        $annotations = $raw->{self::ANNOTATIONS_KEY};
2653
2654
        if (true === $annotations || null === $annotations) {
2655
            return self::DEFAULT_IGNORED_ANNOTATIONS;
2656
        }
2657
2658
        if (false === $annotations) {
0 ignored issues
show
introduced by
The condition false === $annotations is always true.
Loading history...
2659
            return [];
2660
        }
2661
2662
        if (false === property_exists($annotations, self::IGNORED_ANNOTATIONS_KEY)) {
2663
            $logger->addWarning(
2664
                sprintf(
2665
                    'The "%s" setting has been set but no "%s" setting has been found, hence "%s" is treated as'
2666
                    .' if it is set to `false`',
2667
                    self::ANNOTATIONS_KEY,
2668
                    self::IGNORED_ANNOTATIONS_KEY,
2669
                    self::ANNOTATIONS_KEY
2670
                )
2671
            );
2672
2673
            return [];
2674
        }
2675
2676
        $ignored = [];
2677
2678
        if (property_exists($annotations, self::IGNORED_ANNOTATIONS_KEY)
2679
            && in_array($ignored = $annotations->{self::IGNORED_ANNOTATIONS_KEY}, [null, []], true)
2680
        ) {
2681
            self::addRecommendationForDefaultValue($logger, self::ANNOTATIONS_KEY.'#'.self::IGNORED_ANNOTATIONS_KEY);
2682
2683
            return (array) $ignored;
2684
        }
2685
2686
        return $ignored;
2687
    }
2688
2689
    private static function createPhpCompactor(array $ignoredAnnotations): Compactor
2690
    {
2691
        $ignoredAnnotations = array_values(
2692
            array_filter(
2693
                array_map(
2694
                    static function (string $annotation): ?string {
2695
                        return strtolower(trim($annotation));
2696
                    },
2697
                    $ignoredAnnotations
2698
                )
2699
            )
2700
        );
2701
2702
        return new PhpCompactor(
2703
            new DocblockAnnotationParser(
2704
                new DocblockParser(),
2705
                new AnnotationDumper(),
2706
                $ignoredAnnotations
2707
            )
2708
        );
2709
    }
2710
2711
    private static function createPhpScoperCompactor(stdClass $raw, string $basePath, ConfigurationLogger $logger): Compactor
2712
    {
2713
        $phpScoperConfig = self::retrievePhpScoperConfig($raw, $basePath, $logger);
2714
2715
        $phpScoper = (new class() extends ApplicationFactory {
2716
            public static function createScoper(): Scoper
2717
            {
2718
                return parent::createScoper();
2719
            }
2720
        })::createScoper();
2721
2722
        if ([] !== $phpScoperConfig->getWhitelistedFiles()) {
2723
            $whitelistedFiles = array_values(
2724
                array_unique(
2725
                    array_map(
2726
                        static function (string $path) use ($basePath): string {
2727
                            return make_path_relative($path, $basePath);
2728
                        },
2729
                        $phpScoperConfig->getWhitelistedFiles()
2730
                    )
2731
                )
2732
            );
2733
2734
            $phpScoper = new FileWhitelistScoper($phpScoper, ...$whitelistedFiles);
2735
        }
2736
2737
        $prefix = $phpScoperConfig->getPrefix() ?? unique_id('_HumbugBox');
2738
2739
        return new PhpScoperCompactor(
2740
            new SimpleScoper(
2741
                $phpScoper,
2742
                $prefix,
2743
                $phpScoperConfig->getWhitelist(),
2744
                $phpScoperConfig->getPatchers()
2745
            )
2746
        );
2747
    }
2748
2749
    private static function checkIfDefaultValue(
2750
        ConfigurationLogger $logger,
2751
        stdClass $raw,
2752
        string $key,
2753
        $defaultValue = null
2754
    ): void {
2755
        if (false === property_exists($raw, $key)) {
2756
            return;
2757
        }
2758
2759
        $value = $raw->{$key};
2760
2761
        if (null === $value
2762
            || (false === is_object($defaultValue) && $defaultValue === $value)
2763
            || (is_object($defaultValue) && $defaultValue == $value)
2764
        ) {
2765
            $logger->addRecommendation(
2766
                sprintf(
2767
                    'The "%s" setting can be omitted since is set to its default value',
2768
                    $key
2769
                )
2770
            );
2771
        }
2772
    }
2773
2774
    private static function addRecommendationForDefaultValue(ConfigurationLogger $logger, string $key): void
2775
    {
2776
        $logger->addRecommendation(
2777
            sprintf(
2778
                'The "%s" setting can be omitted since is set to its default value',
2779
                $key
2780
            )
2781
        );
2782
    }
2783
}
2784