Passed
Pull Request — master (#282)
by Théo
05:10 queued 01:50
created

Configuration::getRecommendations()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

}

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

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

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

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