Passed
Pull Request — master (#31)
by Théo
02:18
created

Configuration::getStubBannerContents()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
eloc 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 Herrera\Annotations\Tokenizer;
21
use Herrera\Box\Compactor\Php as LegacyPhp;
22
use Humbug\PhpScoper\Configuration as PhpScoperConfiguration;
23
use InvalidArgumentException;
24
use KevinGH\Box\Compactor\Php;
25
use KevinGH\Box\Compactor\PhpScoper;
26
use KevinGH\Box\Composer\ComposerConfiguration;
27
use Phar;
28
use RuntimeException;
29
use SplFileInfo;
30
use stdClass;
31
use Symfony\Component\Finder\Finder;
32
use Symfony\Component\Process\Process;
33
use function Humbug\PhpScoper\create_scoper;
34
use function iter\chain;
35
use function KevinGH\Box\FileSystem\canonicalize;
36
use function KevinGH\Box\FileSystem\file_contents;
37
use function KevinGH\Box\FileSystem\longest_common_base_path;
38
use function KevinGH\Box\FileSystem\make_path_absolute;
39
use function KevinGH\Box\FileSystem\make_path_relative;
40
41
final class Configuration
42
{
43
    private const DEFAULT_ALIAS = 'default.phar';
44
    private const DEFAULT_MAIN_SCRIPT = 'index.php';
45
    private const DEFAULT_DATETIME_FORMAT = 'Y-m-d H:i:s';
46
    private const DEFAULT_REPLACEMENT_SIGIL = '@';
47
    private const DEFAULT_SHEBANG = '#!/usr/bin/env php';
48
    private const DEFAULT_BANNER = <<<'BANNER'
49
Generated by Humbug Box.
50
51
@link https://github.com/humbug/box
52
BANNER;
53
    private const FILES_SETTINGS = [
54
        'files',
55
        'files-bin',
56
        'directories',
57
        'directories-bin',
58
        'finder',
59
        'finder-bin',
60
    ];
61
    private const PHP_SCOPER_CONFIG = 'scoper.inc.php';
62
63
    private $fileMode;
64
    private $alias;
65
    private $basePath;
66
    private $files;
67
    private $binaryFiles;
68
    private $bootstrapFile;
69
    private $compactors;
70
    private $compressionAlgorithm;
71
    private $mainScriptPath;
72
    private $mainScriptContents;
73
    private $map;
74
    private $fileMapper;
75
    private $metadata;
76
    private $outputPath;
77
    private $privateKeyPassphrase;
78
    private $privateKeyPath;
79
    private $isPrivateKeyPrompt;
80
    private $processedReplacements;
81
    private $shebang;
82
    private $signingAlgorithm;
83
    private $stubBannerContents;
84
    private $stubBannerPath;
85
    private $stubPath;
86
    private $isInterceptFileFuncs;
87
    private $isStubGenerated;
88
89
    /**
90
     * @param null|string   $file
91
     * @param null|string   $alias
92
     * @param string        $basePath              Utility to private the base path used and be able to retrieve a path relative to it (the base path)
93
     * @param SplFileInfo[] $files                 List of files
94
     * @param SplFileInfo[] $binaryFiles           List of binary files
95
     * @param null|string   $bootstrapFile         The bootstrap file path
96
     * @param Compactor[]   $compactors            List of file contents compactors
97
     * @param null|int      $compressionAlgorithm  Compression algorithm constant value. See the \Phar class constants
98
     * @param null|int      $fileMode              File mode in octal form
99
     * @param string        $mainScriptPath        The main script file path
100
     * @param string        $mainScriptContents    The processed content of the main script file
101
     * @param MapFile       $fileMapper            Utility to map the files from outside and inside the PHAR
102
     * @param mixed         $metadata              The PHAR Metadata
103
     * @param string        $outputPath
104
     * @param null|string   $privateKeyPassphrase
105
     * @param null|string   $privateKeyPath
106
     * @param bool          $isPrivateKeyPrompt    If the user should be prompted for the private key passphrase
107
     * @param array         $processedReplacements The processed list of replacement placeholders and their values
108
     * @param null|string   $shebang               The shebang line
109
     * @param int           $signingAlgorithm      The PHAR siging algorithm. See \Phar constants
110
     * @param null|string   $stubBannerContents    The stub banner comment
111
     * @param null|string   $stubBannerPath        The path to the stub banner comment file
112
     * @param null|string   $stubPath              The PHAR stub file path
113
     * @param bool          $isInterceptFileFuncs  wether or not Phar::interceptFileFuncs() should be used
114
     * @param bool          $isStubGenerated       Wether or not if the PHAR stub should be generated
115
     */
116
    private function __construct(
117
        ?string $file,
118
        ?string $alias,
119
        string $basePath,
120
        array $files,
121
        array $binaryFiles,
122
        ?string $bootstrapFile,
123
        array $compactors,
124
        ?int $compressionAlgorithm,
125
        ?int $fileMode,
126
        string $mainScriptPath,
127
        string $mainScriptContents,
128
        MapFile $fileMapper,
129
        $metadata,
130
        string $outputPath,
131
        ?string $privateKeyPassphrase,
132
        ?string $privateKeyPath,
133
        bool $isPrivateKeyPrompt,
134
        array $processedReplacements,
135
        ?string $shebang,
136
        int $signingAlgorithm,
137
        ?string $stubBannerContents,
138
        ?string $stubBannerPath,
139
        ?string $stubPath,
140
        bool $isInterceptFileFuncs,
141
        bool $isStubGenerated
142
    ) {
143
        Assertion::nullOrInArray(
144
            $compressionAlgorithm,
145
            get_phar_compression_algorithms(),
146
            sprintf(
147
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
148
                implode('", "', array_keys(get_phar_compression_algorithms()))
149
            )
150
        );
151
152
        $this->alias = $alias;
153
        $this->basePath = $basePath;
154
        $this->files = $files;
155
        $this->binaryFiles = $binaryFiles;
156
        $this->bootstrapFile = $bootstrapFile;
157
        $this->compactors = $compactors;
158
        $this->compressionAlgorithm = $compressionAlgorithm;
159
        $this->fileMode = $fileMode;
160
        $this->mainScriptPath = $mainScriptPath;
161
        $this->mainScriptContents = $mainScriptContents;
162
        $this->fileMapper = $fileMapper;
163
        $this->metadata = $metadata;
164
        $this->outputPath = $outputPath;
165
        $this->privateKeyPassphrase = $privateKeyPassphrase;
166
        $this->privateKeyPath = $privateKeyPath;
167
        $this->isPrivateKeyPrompt = $isPrivateKeyPrompt;
168
        $this->processedReplacements = $processedReplacements;
169
        $this->shebang = $shebang;
170
        $this->signingAlgorithm = $signingAlgorithm;
171
        $this->stubBannerContents = $stubBannerContents;
172
        $this->stubBannerPath = $stubBannerPath;
173
        $this->stubPath = $stubPath;
174
        $this->isInterceptFileFuncs = $isInterceptFileFuncs;
175
        $this->isStubGenerated = $isStubGenerated;
176
    }
177
178
    public static function create(?string $file, stdClass $raw): self
179
    {
180
        $alias = self::retrieveAlias($raw);
181
182
        $basePath = self::retrieveBasePath($file, $raw);
183
184
        $mainScriptPath = self::retrieveMainScriptPath($raw, $basePath);
185
        $mainScriptContents = self::retrieveMainScriptContents($mainScriptPath);
186
187
        $devPackages = ComposerConfiguration::retrieveDevPackages($basePath);
188
189
        $blacklistFilter = self::retrieveBlacklistFilter($raw, $basePath);
190
191
        if (self::shouldRetrieveAllFiles($file, $raw)) {
192
            $filesAggregate = self::retrieveAllFiles($basePath, $mainScriptPath, $blacklistFilter, $devPackages);
193
            $binaryFilesAggregate = [];
194
        } else {
195
            $files = self::retrieveFiles($raw, 'files', $basePath);
196
            $directories = self::retrieveDirectories($raw, 'directories', $basePath, $blacklistFilter);
197
            $filesFromFinders = self::retrieveFilesFromFinders($raw, 'finder', $basePath, $blacklistFilter, $devPackages);
198
199
            $filesAggregate = array_unique(iterator_to_array(chain($files, $directories, ...$filesFromFinders)));
200
201
            $binaryFiles = self::retrieveFiles($raw, 'files-bin', $basePath);
202
            $binaryDirectories = self::retrieveDirectories($raw, 'directories-bin', $basePath, $blacklistFilter);
203
            $binaryFilesFromFinders = self::retrieveFilesFromFinders($raw, 'finder-bin', $basePath, $blacklistFilter, $devPackages);
204
205
            $binaryFilesAggregate = array_unique(iterator_to_array(chain($binaryFiles, $binaryDirectories, ...$binaryFilesFromFinders)));
206
        }
207
208
        $bootstrapFile = self::retrieveBootstrapFile($raw, $basePath);
209
210
        $compactors = self::retrieveCompactors($raw, $basePath);
211
        $compressionAlgorithm = self::retrieveCompressionAlgorithm($raw);
212
213
        $fileMode = self::retrieveFileMode($raw);
214
215
        $map = self::retrieveMap($raw);
216
        $fileMapper = new MapFile($map);
217
218
        $metadata = self::retrieveMetadata($raw);
219
220
        $outputPath = self::retrieveOutputPath($raw, $basePath);
221
222
        $privateKeyPassphrase = self::retrievePrivateKeyPassphrase($raw);
223
        $privateKeyPath = self::retrievePrivateKeyPath($raw);
224
        $isPrivateKeyPrompt = self::retrieveIsPrivateKeyPrompt($raw);
225
226
        $replacements = self::retrieveReplacements($raw);
227
        $processedReplacements = self::retrieveProcessedReplacements($replacements, $raw, $file);
228
229
        $shebang = self::retrieveShebang($raw);
230
231
        $signingAlgorithm = self::retrieveSigningAlgorithm($raw);
232
233
        $stubBannerContents = self::retrieveStubBannerContents($raw);
234
        $stubBannerPath = self::retrieveStubBannerPath($raw, $basePath);
235
236
        if (null !== $stubBannerPath) {
237
            $stubBannerContents = file_contents($stubBannerPath);
238
        }
239
240
        $stubBannerContents = self::normalizeStubBannerContents($stubBannerContents);
241
242
        $stubPath = self::retrieveStubPath($raw, $basePath);
243
244
        $isInterceptFileFuncs = self::retrieveIsInterceptFileFuncs($raw);
245
        $isStubGenerated = self::retrieveIsStubGenerated($raw);
246
247
        return new self(
248
            $file,
249
            $alias,
250
            $basePath,
251
            $filesAggregate,
252
            $binaryFilesAggregate,
253
            $bootstrapFile,
254
            $compactors,
255
            $compressionAlgorithm,
256
            $fileMode,
257
            $mainScriptPath,
258
            $mainScriptContents,
259
            $fileMapper,
260
            $metadata,
261
            $outputPath,
262
            $privateKeyPassphrase,
263
            $privateKeyPath,
264
            $isPrivateKeyPrompt,
265
            $processedReplacements,
266
            $shebang,
267
            $signingAlgorithm,
268
            $stubBannerContents,
269
            $stubBannerPath,
270
            $stubPath,
271
            $isInterceptFileFuncs,
272
            $isStubGenerated
273
        );
274
    }
275
276
    public function getAlias(): ?string
277
    {
278
        return $this->alias;
279
    }
280
281
    public function getBasePath(): string
282
    {
283
        return $this->basePath;
284
    }
285
286
    /**
287
     * @return string[]
288
     */
289
    public function getFiles(): array
290
    {
291
        return $this->files;
292
    }
293
294
    /**
295
     * @return string[]
296
     */
297
    public function getBinaryFiles(): array
298
    {
299
        return $this->binaryFiles;
300
    }
301
302
    public function getBootstrapFile(): ?string
303
    {
304
        return $this->bootstrapFile;
305
    }
306
307
    public function loadBootstrap(): void
308
    {
309
        $file = $this->bootstrapFile;
310
311
        if (null !== $file) {
312
            include $file;
313
        }
314
    }
315
316
    /**
317
     * @return Compactor[] the list of compactors
318
     */
319
    public function getCompactors(): array
320
    {
321
        return $this->compactors;
322
    }
323
324
    public function getCompressionAlgorithm(): ?int
325
    {
326
        return $this->compressionAlgorithm;
327
    }
328
329
    public function getFileMode(): ?int
330
    {
331
        return $this->fileMode;
332
    }
333
334
    public function getMainScriptPath(): string
335
    {
336
        return $this->mainScriptPath;
337
    }
338
339
    public function getMainScriptContents(): string
340
    {
341
        return $this->mainScriptContents;
342
    }
343
344
    public function getOutputPath(): string
345
    {
346
        return $this->outputPath;
347
    }
348
349
    /**
350
     * @return string[]
351
     */
352
    public function getMap(): array
353
    {
354
        return $this->fileMapper->getMap();
355
    }
356
357
    public function getFileMapper(): MapFile
358
    {
359
        return $this->fileMapper;
360
    }
361
362
    /**
363
     * @return mixed
364
     */
365
    public function getMetadata()
366
    {
367
        return $this->metadata;
368
    }
369
370
    public function getPrivateKeyPassphrase(): ?string
371
    {
372
        return $this->privateKeyPassphrase;
373
    }
374
375
    public function getPrivateKeyPath(): ?string
376
    {
377
        return $this->privateKeyPath;
378
    }
379
380
    public function isPrivateKeyPrompt(): bool
381
    {
382
        return $this->isPrivateKeyPrompt;
383
    }
384
385
    public function getProcessedReplacements(): array
386
    {
387
        return $this->processedReplacements;
388
    }
389
390
    public function getShebang(): ?string
391
    {
392
        return $this->shebang;
393
    }
394
395
    public function getSigningAlgorithm(): int
396
    {
397
        return $this->signingAlgorithm;
398
    }
399
400
    public function getStubBannerContents(): ?string
401
    {
402
        return $this->stubBannerContents;
403
    }
404
405
    public function getStubBannerPath(): ?string
406
    {
407
        return $this->stubBannerPath;
408
    }
409
410
    public function getStubPath(): ?string
411
    {
412
        return $this->stubPath;
413
    }
414
415
    public function isInterceptFileFuncs(): bool
416
    {
417
        return $this->isInterceptFileFuncs;
418
    }
419
420
    public function isStubGenerated(): bool
421
    {
422
        return $this->isStubGenerated;
423
    }
424
425
    private static function retrieveAlias(stdClass $raw): ?string
426
    {
427
        if (false === isset($raw->alias)) {
428
            return null;
429
        }
430
431
        $alias = trim($raw->alias);
432
433
        Assertion::notEmpty($alias, 'A PHAR alias cannot be empty when provided.');
434
435
        return $alias;
436
    }
437
438
    private static function retrieveBasePath(?string $file, stdClass $raw): string
439
    {
440
        if (null === $file) {
441
            return getcwd();
442
        }
443
444
        if (false === isset($raw->{'base-path'})) {
445
            return realpath(dirname($file));
446
        }
447
448
        $basePath = trim($raw->{'base-path'});
449
450
        Assertion::directory(
451
            $basePath,
452
            'The base path "%s" is not a directory or does not exist.'
453
        );
454
455
        return realpath($basePath);
456
    }
457
458
    private static function shouldRetrieveAllFiles(?string $file, stdClass $raw): bool
459
    {
460
        if (null === $file) {
461
            return true;
462
        }
463
464
        // TODO: config should be casted into an array: it is easier to do and we need an array in several places now
465
        $rawConfig = (array) $raw;
466
467
        foreach (self::FILES_SETTINGS as $key) {
468
            if (array_key_exists($key, $rawConfig)) {
469
                return false;
470
            }
471
        }
472
473
        return true;
474
    }
475
476
    /**
477
     * @param stdClass $raw
478
     * @param string   $basePath
479
     *
480
     * @return Closure
481
     */
482
    private static function retrieveBlacklistFilter(stdClass $raw, string $basePath): Closure
483
    {
484
        $blacklist = self::retrieveBlacklist($raw, $basePath);
485
486
        return function (SplFileInfo $file) use ($blacklist): ?bool {
487
            if (in_array($file->getRealPath(), $blacklist, true)) {
488
                return false;
489
            }
490
491
            return null;
492
        };
493
    }
494
495
    /**
496
     * @param stdClass $raw
497
     * @param string   $basePath
498
     *
499
     * @return string[]
500
     */
501
    private static function retrieveBlacklist(stdClass $raw, string $basePath): array
502
    {
503
        if (false === isset($raw->blacklist)) {
504
            return [];
505
        }
506
507
        $blacklist = $raw->blacklist;
508
509
        $normalizePath = function ($file) use ($basePath): string {
510
            return self::normalizeFilePath($file, $basePath);
511
        };
512
513
        return array_unique(array_map($normalizePath, $blacklist));
514
    }
515
516
    /**
517
     * @param stdClass $raw
518
     * @param string   $key      Config property name
519
     * @param string   $basePath
520
     *
521
     * @return SplFileInfo[]
522
     */
523
    private static function retrieveFiles(stdClass $raw, string $key, string $basePath): array
524
    {
525
        if (false === isset($raw->{$key})) {
526
            return [];
527
        }
528
529
        $files = (array) $raw->{$key};
530
531
        Assertion::allString($files);
532
533
        $normalizePath = function (string $file) use ($basePath, $key): SplFileInfo {
534
            $file = self::normalizeFilePath($file, $basePath);
535
536
            if (is_link($file)) {
537
                // TODO: add this to baberlei/assert
538
                throw new InvalidArgumentException(
539
                    sprintf(
540
                        'Cannot add the link "%s": links are not supported.',
541
                        $file
542
                    )
543
                );
544
            }
545
546
            Assertion::file(
547
                $file,
548
                sprintf(
549
                    '"%s" must contain a list of existing files. Could not find "%%s".',
550
                    $key
551
                )
552
            );
553
554
            return new SplFileInfo($file);
555
        };
556
557
        return array_map($normalizePath, $files);
558
    }
559
560
    /**
561
     * @param stdClass $raw
562
     * @param string   $key             Config property name
563
     * @param string   $basePath
564
     * @param Closure  $blacklistFilter
565
     *
566
     * @return iterable|SplFileInfo[]
567
     */
568
    private static function retrieveDirectories(
569
        stdClass $raw,
570
        string $key,
571
        string $basePath,
572
        Closure $blacklistFilter
573
    ): iterable {
574
        $directories = self::retrieveDirectoryPaths($raw, $key, $basePath);
575
576
        if ([] !== $directories) {
577
            return Finder::create()
1 ignored issue
show
Bug Best Practice introduced by
The expression return Symfony\Component...true)->in($directories) returns the type Symfony\Component\Finder\Finder which is incompatible with the documented return type iterable|SplFileInfo[].
Loading history...
578
                ->files()
579
                ->filter($blacklistFilter)
580
                ->ignoreVCS(true)
581
                ->in($directories)
582
            ;
583
        }
584
585
        return [];
586
    }
587
588
    /**
589
     * @param stdClass $raw
590
     * @param string   $basePath
591
     * @param Closure  $blacklistFilter
592
     * @param string[] $devPackages
593
     *
594
     * @return iterable[]|SplFileInfo[][]
595
     */
596
    private static function retrieveFilesFromFinders(
597
        stdClass $raw,
598
        string $key,
599
        string $basePath,
600
        Closure $blacklistFilter,
601
        array $devPackages
602
    ): array {
603
        if (isset($raw->{$key})) {
604
            return self::processFinders($raw->{$key}, $basePath, $blacklistFilter, $devPackages);
605
        }
606
607
        return [];
608
    }
609
610
    /**
611
     * @param array    $findersConfig
612
     * @param string   $basePath
613
     * @param Closure  $blacklistFilter
614
     * @param string[] $devPackages
615
     *
616
     * @return Finder[]|SplFileInfo[][]
617
     */
618
    private static function processFinders(
619
        array $findersConfig,
620
        string $basePath,
621
        Closure $blacklistFilter,
622
        array $devPackages
623
    ): array {
624
        $processFinderConfig = function (stdClass $config) use ($basePath, $blacklistFilter, $devPackages) {
625
            return self::processFinder($config, $basePath, $blacklistFilter, $devPackages);
626
        };
627
628
        return array_map($processFinderConfig, $findersConfig);
629
    }
630
631
    /**
632
     * @param stdClass $config
633
     * @param string   $basePath
634
     * @param Closure  $blacklistFilter
635
     * @param string[] $devPackages
636
     *
637
     * @return Finder|SplFileInfo[]
638
     */
639
    private static function processFinder(stdClass $config, string $basePath, Closure $blacklistFilter, array $devPackages): Finder
640
    {
641
        $finder = Finder::create()
642
            ->files()
643
            ->filter($blacklistFilter)
644
            ->filter(
645
                function (SplFileInfo $fileInfo) use ($devPackages): bool {
646
                    foreach ($devPackages as $devPackage) {
647
                        if ($devPackage === longest_common_base_path([$devPackage, $fileInfo->getRealPath()])) {
648
                            // File belongs to the dev package
649
                            return false;
650
                        }
651
                    }
652
653
                    return true;
654
                }
655
            )
656
            ->ignoreVCS(true)
657
        ;
658
659
        $normalizedConfig = (function (array $config, Finder $finder): array {
660
            $normalizedConfig = [];
661
662
            foreach ($config as $method => $arguments) {
663
                $method = trim($method);
664
                $arguments = (array) $arguments;
665
666
                Assertion::methodExists(
667
                    $method,
668
                    $finder,
669
                    'The method "Finder::%s" does not exist.'
670
                );
671
672
                $normalizedConfig[$method] = $arguments;
673
            }
674
675
            krsort($normalizedConfig);
676
677
            return $normalizedConfig;
678
        })((array) $config, $finder);
679
680
        $createNormalizedDirectories = function (string $directory) use ($basePath): ?string {
681
            $directory = self::normalizeDirectoryPath($directory, $basePath);
682
683
            if (is_link($directory)) {
684
                // TODO: add this to baberlei/assert
685
                throw new InvalidArgumentException(
686
                    sprintf(
687
                        'Cannot append the link "%s" to the Finder: links are not supported.',
688
                        $directory
689
                    )
690
                );
691
            }
692
693
            Assertion::directory($directory);
694
695
            return $directory;
696
        };
697
698
        $normalizeFileOrDirectory = function (string &$fileOrDirectory) use ($basePath): void {
699
            $fileOrDirectory = self::normalizeDirectoryPath($fileOrDirectory, $basePath);
700
701
            if (is_link($fileOrDirectory)) {
702
                // TODO: add this to baberlei/assert
703
                throw new InvalidArgumentException(
704
                    sprintf(
705
                        'Cannot append the link "%s" to the Finder: links are not supported.',
706
                        $fileOrDirectory
707
                    )
708
                );
709
            }
710
711
            // TODO: add this to baberlei/assert
712
            if (false === file_exists($fileOrDirectory)) {
713
                throw new InvalidArgumentException(
714
                    sprintf(
715
                        'Path "%s" was expected to be a file or directory. It may be a symlink (which are unsupported).',
716
                        $fileOrDirectory
717
                    )
718
                );
719
            }
720
721
            //TODO: add fileExists (as file or directory) to Assert
722
            if (false === is_file($fileOrDirectory)) {
723
                Assertion::directory($fileOrDirectory);
724
            } else {
725
                Assertion::file($fileOrDirectory);
726
            }
727
        };
728
729
        foreach ($normalizedConfig as $method => $arguments) {
730
            if ('in' === $method) {
731
                $normalizedConfig[$method] = $arguments = array_map($createNormalizedDirectories, $arguments);
732
            }
733
734
            if ('exclude' === $method) {
735
                $arguments = array_unique(array_map('trim', $arguments));
736
            }
737
738
            if ('append' === $method) {
739
                array_walk($arguments, $normalizeFileOrDirectory);
740
741
                $arguments = [$arguments];
742
            }
743
744
            foreach ($arguments as $argument) {
745
                $finder->$method($argument);
746
            }
747
        }
748
749
        return $finder;
750
    }
751
752
    /**
753
     * @param string   $basePath
754
     * @param string   $mainScriptPath
755
     * @param Closure  $blacklistFilter
756
     * @param string[] $devPackages
757
     *
758
     * @return SplFileInfo[]
759
     */
760
    private static function retrieveAllFiles(
761
        string $basePath,
762
        string $mainScriptPath,
763
        Closure $blacklistFilter,
764
        array $devPackages
765
    ): array {
766
        $relativeDevPackages = array_map(
767
            function (string $packagePath) use ($basePath): string {
768
                return make_path_relative($packagePath, $basePath);
769
            },
770
            $devPackages
771
        );
772
773
        $finder = Finder::create()
774
            ->files()
775
            ->in($basePath)
776
            ->notPath(make_path_relative($mainScriptPath, $basePath))
777
            ->filter($blacklistFilter)
778
            ->exclude($relativeDevPackages)
779
            ->ignoreVCS(true)
780
        ;
781
782
        return array_filter(
783
            array_unique(
784
                array_map(
785
                    function (SplFileInfo $fileInfo): ?string {
786
                        if (is_link((string) $fileInfo)) {
787
                            return null;
788
                        }
789
790
                        return false !== $fileInfo->getRealPath() ? $fileInfo->getRealPath() : null;
791
                    },
792
                    iterator_to_array(
793
                        $finder
794
                    )
795
                )
796
            )
797
        );
798
    }
799
800
    /**
801
     * @param stdClass $raw
802
     * @param string   $key      Config property name
803
     * @param string   $basePath
804
     *
805
     * @return string[]
806
     */
807
    private static function retrieveDirectoryPaths(stdClass $raw, string $key, string $basePath): array
808
    {
809
        if (false === isset($raw->{$key})) {
810
            return [];
811
        }
812
813
        $directories = $raw->{$key};
814
815
        $normalizeDirectory = function (string $directory) use ($basePath, $key): string {
816
            $directory = self::normalizeDirectoryPath($directory, $basePath);
817
818
            if (is_link($directory)) {
819
                // TODO: add this to baberlei/assert
820
                throw new InvalidArgumentException(
821
                    sprintf(
822
                        'Cannot add the link "%s": links are not supported.',
823
                        $directory
824
                    )
825
                );
826
            }
827
828
            Assertion::directory(
829
                $directory,
830
                sprintf(
831
                    '"%s" must contain a list of existing directories. Could not find "%%s".',
832
                    $key
833
                )
834
            );
835
836
            return $directory;
837
        };
838
839
        return array_map($normalizeDirectory, $directories);
840
    }
841
842
    private static function normalizeFilePath(string $file, string $basePath): string
843
    {
844
        return make_path_absolute(trim($file), $basePath);
845
    }
846
847
    private static function normalizeDirectoryPath(string $directory, string $basePath): string
848
    {
849
        return make_path_absolute(trim($directory), $basePath);
850
    }
851
852
    private static function retrieveBootstrapFile(stdClass $raw, string $basePath): ?string
853
    {
854
        // TODO: deprecate its usage & document this BC break. Compactors will not be configurable
855
        // through that extension point so this is pretty much useless unless proven otherwise.
856
        if (false === isset($raw->bootstrap)) {
857
            return null;
858
        }
859
860
        $file = self::normalizeFilePath($raw->bootstrap, $basePath);
861
862
        Assertion::file($file, 'The bootstrap path "%s" is not a file or does not exist.');
863
864
        return $file;
865
    }
866
867
    /**
868
     * @return Compactor[]
869
     */
870
    private static function retrieveCompactors(stdClass $raw, string $basePath): array
871
    {
872
        // TODO: only accept arrays when set unlike the doc says (it allows a string).
873
        if (false === isset($raw->compactors)) {
874
            return [];
875
        }
876
877
        $compactorClasses = array_unique((array) $raw->compactors);
878
879
        return array_map(
880
            function (string $class) use ($raw, $basePath): Compactor {
881
                Assertion::classExists($class, 'The compactor class "%s" does not exist.');
882
                Assertion::implementsInterface($class, Compactor::class, 'The class "%s" is not a compactor class.');
883
884
                if (Php::class === $class || LegacyPhp::class === $class) {
885
                    return self::createPhpCompactor($raw);
886
                }
887
888
                if (PhpScoper::class === $class) {
889
                    $phpScoperConfig = self::retrievePhpScoperConfig($raw, $basePath);
890
891
                    return new PhpScoper(create_scoper(), $phpScoperConfig);
892
                }
893
894
                return new $class();
895
            },
896
            $compactorClasses
897
        );
898
    }
899
900
    private static function retrieveCompressionAlgorithm(stdClass $raw): ?int
901
    {
902
        // TODO: if in dev mode (when added), do not comment about the compression.
903
        // If not, add a warning to notify the user if no compression algorithm is used
904
        // provided the PHAR is not configured for web purposes.
905
        // If configured for the web, add a warning when a compression algorithm is used
906
        // as this can result in an overhead. Add a doc link explaining this.
907
        //
908
        // Unlike the doc: do not accept integers and document this BC break.
909
        if (false === isset($raw->compression)) {
910
            return null;
911
        }
912
913
        if (false === is_string($raw->compression)) {
914
            Assertion::integer(
915
                $raw->compression,
916
                'Expected compression to be an algorithm name, found %s instead.'
917
            );
918
919
            return $raw->compression;
1 ignored issue
show
Bug Best Practice introduced by
The expression return $raw->compression returns the type string which is incompatible with the type-hinted return null|integer.
Loading history...
920
        }
921
922
        $knownAlgorithmNames = array_keys(get_phar_compression_algorithms());
923
924
        Assertion::inArray(
925
            $raw->compression,
926
            $knownAlgorithmNames,
927
            sprintf(
928
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
929
                implode('", "', $knownAlgorithmNames)
930
            )
931
        );
932
933
        $value = get_phar_compression_algorithms()[$raw->compression];
934
935
        // Phar::NONE is not valid for compressFiles()
936
        if (Phar::NONE === $value) {
937
            return null;
938
        }
939
940
        return $value;
941
    }
942
943
    private static function retrieveFileMode(stdClass $raw): ?int
944
    {
945
        if (isset($raw->chmod)) {
946
            return intval($raw->chmod, 8);
947
        }
948
949
        return null;
950
    }
951
952
    private static function retrieveMainScriptPath(stdClass $raw, string $basePath): string
953
    {
954
        $main = isset($raw->main) ? $raw->main : self::DEFAULT_MAIN_SCRIPT;
955
956
        return self::normalizeFilePath($main, $basePath);
957
    }
958
959
    private static function retrieveMainScriptContents(string $mainScriptPath): string
960
    {
961
        $contents = file_contents($mainScriptPath);
962
963
        // Remove the shebang line: the shebang line in a PHAR should be located in the stub file which is the real
964
        // PHAR entry point file.
965
        return preg_replace('/^#!.*\s*/', '', $contents);
966
    }
967
968
    /**
969
     * @return string[][]
970
     */
971
    private static function retrieveMap(stdClass $raw): array
972
    {
973
        if (false === isset($raw->map)) {
974
            return [];
975
        }
976
977
        $map = [];
978
979
        foreach ((array) $raw->map as $item) {
980
            $processed = [];
981
982
            foreach ($item as $match => $replace) {
983
                $processed[canonicalize(trim($match))] = canonicalize(trim($replace));
984
            }
985
986
            if (isset($processed['_empty_'])) {
987
                $processed[''] = $processed['_empty_'];
988
989
                unset($processed['_empty_']);
990
            }
991
992
            $map[] = $processed;
993
        }
994
995
        return $map;
996
    }
997
998
    /**
999
     * @return mixed
1000
     */
1001
    private static function retrieveMetadata(stdClass $raw)
1002
    {
1003
        // TODO: the doc currently say this can be any value; check if true
1004
        // and if not add checks accordingly
1005
        //
1006
        // Also review the doc as I don't find it very helpful...
1007
        if (isset($raw->metadata)) {
1008
            if (is_object($raw->metadata)) {
1009
                return (array) $raw->metadata;
1010
            }
1011
1012
            return $raw->metadata;
1013
        }
1014
1015
        return null;
1016
    }
1017
1018
    private static function retrieveOutputPath(stdClass $raw, string $basePath): string
1019
    {
1020
        if (isset($raw->output)) {
1021
            $path = $raw->output;
1022
        } else {
1023
            $path = self::DEFAULT_ALIAS;
1024
        }
1025
1026
        return make_path_absolute($path, $basePath);
1027
    }
1028
1029
    private static function retrievePrivateKeyPassphrase(stdClass $raw): ?string
1030
    {
1031
        // TODO: add check to not allow this setting without the private key path
1032
        if (isset($raw->{'key-pass'})
1033
            && is_string($raw->{'key-pass'})
1034
        ) {
1035
            return $raw->{'key-pass'};
1036
        }
1037
1038
        return null;
1039
    }
1040
1041
    private static function retrievePrivateKeyPath(stdClass $raw): ?string
1042
    {
1043
        // TODO: If passed need to check its existence
1044
        // Also need
1045
1046
        if (isset($raw->key)) {
1047
            return $raw->key;
1048
        }
1049
1050
        return null;
1051
    }
1052
1053
    private static function retrieveReplacements(stdClass $raw): array
1054
    {
1055
        // TODO: add exmample in the doc
1056
        // Add checks against the values
1057
        if (isset($raw->replacements)) {
1058
            return (array) $raw->replacements;
1059
        }
1060
1061
        return [];
1062
    }
1063
1064
    private static function retrieveProcessedReplacements(
1065
        array $replacements,
1066
        stdClass $raw,
1067
        ?string $file
1068
    ): array {
1069
        if (null === $file) {
1070
            return [];
1071
        }
1072
1073
        if (null !== ($git = self::retrieveGitHashPlaceholder($raw))) {
1074
            $replacements[$git] = self::retrieveGitHash($file);
1075
        }
1076
1077
        if (null !== ($git = self::retrieveGitShortHashPlaceholder($raw))) {
1078
            $replacements[$git] = self::retrieveGitHash($file, true);
1079
        }
1080
1081
        if (null !== ($git = self::retrieveGitTagPlaceholder($raw))) {
1082
            $replacements[$git] = self::retrieveGitTag($file);
1083
        }
1084
1085
        if (null !== ($git = self::retrieveGitVersionPlaceholder($raw))) {
1086
            $replacements[$git] = self::retrieveGitVersion($file);
1087
        }
1088
1089
        if (null !== ($date = self::retrieveDatetimeNowPlaceHolder($raw))) {
1090
            $replacements[$date] = self::retrieveDatetimeNow(
1091
                self::retrieveDatetimeFormat($raw)
1092
            );
1093
        }
1094
1095
        $sigil = self::retrieveReplacementSigil($raw);
1096
1097
        foreach ($replacements as $key => $value) {
1098
            unset($replacements[$key]);
1099
            $replacements["$sigil$key$sigil"] = $value;
1100
        }
1101
1102
        return $replacements;
1103
    }
1104
1105
    private static function retrieveGitHashPlaceholder(stdClass $raw): ?string
1106
    {
1107
        if (isset($raw->{'git-commit'})) {
1108
            return $raw->{'git-commit'};
1109
        }
1110
1111
        return null;
1112
    }
1113
1114
    /**
1115
     * @param string $file
1116
     * @param bool   $short Use the short version
1117
     *
1118
     * @return string the commit hash
1119
     */
1120
    private static function retrieveGitHash(string $file, bool $short = false): string
1121
    {
1122
        return self::runGitCommand(
1123
            sprintf(
1124
                'git log --pretty="%s" -n1 HEAD',
1125
                $short ? '%h' : '%H'
1126
            ),
1127
            $file
1128
        );
1129
    }
1130
1131
    private static function retrieveGitShortHashPlaceholder(stdClass $raw): ?string
1132
    {
1133
        if (isset($raw->{'git-commit-short'})) {
1134
            return $raw->{'git-commit-short'};
1135
        }
1136
1137
        return null;
1138
    }
1139
1140
    private static function retrieveGitTagPlaceholder(stdClass $raw): ?string
1141
    {
1142
        if (isset($raw->{'git-tag'})) {
1143
            return $raw->{'git-tag'};
1144
        }
1145
1146
        return null;
1147
    }
1148
1149
    private static function retrieveGitTag(string $file): ?string
1150
    {
1151
        return self::runGitCommand('git describe --tags HEAD', $file);
1152
    }
1153
1154
    private static function retrieveGitVersionPlaceholder(stdClass $raw): ?string
1155
    {
1156
        if (isset($raw->{'git-version'})) {
1157
            return $raw->{'git-version'};
1158
        }
1159
1160
        return null;
1161
    }
1162
1163
    private static function retrieveGitVersion(string $file): ?string
1164
    {
1165
        // TODO: check if is still relevant as IMO we are better off using OcramiusVersionPackage
1166
        // to avoid messing around with that
1167
1168
        try {
1169
            return self::retrieveGitTag($file);
1170
        } catch (RuntimeException $exception) {
1171
            try {
1172
                return self::retrieveGitHash($file, true);
1173
            } catch (RuntimeException $exception) {
1174
                throw new RuntimeException(
1175
                    sprintf(
1176
                        'The tag or commit hash could not be retrieved from "%s": %s',
1177
                        dirname($file),
1178
                        $exception->getMessage()
1179
                    ),
1180
                    0,
1181
                    $exception
1182
                );
1183
            }
1184
        }
1185
    }
1186
1187
    private static function retrieveDatetimeNowPlaceHolder(stdClass $raw): ?string
1188
    {
1189
        // TODO: double check why this is done and how it is used it's not completely clear to me.
1190
        // Also make sure the documentation is up to date after.
1191
        // Instead of having two sistinct doc entries for `datetime` and `datetime-format`, it would
1192
        // be better to have only one element IMO like:
1193
        //
1194
        // "datetime": {
1195
        //   "value": "val",
1196
        //   "format": "Y-m-d"
1197
        // }
1198
        //
1199
        // Also add a check that one cannot be provided without the other. Or maybe it should? I guess
1200
        // if the datetime format is the default one it's ok; but in any case the format should not
1201
        // be added without the datetime value...
1202
1203
        if (isset($raw->{'datetime'})) {
1204
            return $raw->{'datetime'};
1205
        }
1206
1207
        return null;
1208
    }
1209
1210
    private static function retrieveDatetimeNow(string $format)
1211
    {
1212
        $now = new DateTimeImmutable('now');
1213
1214
        $datetime = $now->format($format);
1215
1216
        if (!$datetime) {
1217
            throw new InvalidArgumentException(
1218
                sprintf(
1219
                    '""%s" is not a valid PHP date format',
1220
                    $format
1221
                )
1222
            );
1223
        }
1224
1225
        return $datetime;
1226
    }
1227
1228
    private static function retrieveDatetimeFormat(stdClass $raw): string
1229
    {
1230
        if (isset($raw->{'datetime_format'})) {
1231
            return $raw->{'datetime_format'};
1232
        }
1233
1234
        return self::DEFAULT_DATETIME_FORMAT;
1235
    }
1236
1237
    private static function retrieveReplacementSigil(stdClass $raw)
1238
    {
1239
        if (isset($raw->{'replacement-sigil'})) {
1240
            return $raw->{'replacement-sigil'};
1241
        }
1242
1243
        return self::DEFAULT_REPLACEMENT_SIGIL;
1244
    }
1245
1246
    private static function retrieveShebang(stdClass $raw): ?string
1247
    {
1248
        if (false === array_key_exists('shebang', (array) $raw)) {
1249
            return self::DEFAULT_SHEBANG;
1250
        }
1251
1252
        if (null === $raw->shebang) {
1253
            return null;
1254
        }
1255
1256
        $shebang = trim($raw->shebang);
1257
1258
        Assertion::notEmpty($shebang, 'The shebang should not be empty.');
1259
        Assertion::true(
1260
            '#!' === substr($shebang, 0, 2),
1261
            sprintf(
1262
                'The shebang line must start with "#!". Got "%s" instead',
1263
                $shebang
1264
            )
1265
        );
1266
1267
        return $shebang;
1268
    }
1269
1270
    private static function retrieveSigningAlgorithm(stdClass $raw): int
1271
    {
1272
        // TODO: trigger warning: if no signing algorithm is given provided we are not in dev mode
1273
        // TODO: trigger a warning if the signing algorithm used is weak
1274
        // TODO: no longer accept strings & document BC break
1275
        if (false === isset($raw->algorithm)) {
1276
            return Phar::SHA1;
1277
        }
1278
1279
        if (is_string($raw->algorithm)) {
1280
            if (false === defined('Phar::'.$raw->algorithm)) {
1281
                throw new InvalidArgumentException(
1282
                    sprintf(
1283
                        'The signing algorithm "%s" is not supported.',
1284
                        $raw->algorithm
1285
                    )
1286
                );
1287
            }
1288
1289
            return constant('Phar::'.$raw->algorithm);
1290
        }
1291
1292
        return $raw->algorithm;
1293
    }
1294
1295
    private static function retrieveStubBannerContents(stdClass $raw): ?string
1296
    {
1297
        if (false === array_key_exists('banner', (array) $raw)) {
1298
            return self::DEFAULT_BANNER;
1299
        }
1300
1301
        if (null === $raw->banner) {
1302
            return null;
1303
        }
1304
1305
        $banner = $raw->banner;
1306
1307
        if (is_array($banner)) {
1308
            $banner = implode("\n", $banner);
1309
        }
1310
1311
        return $banner;
1312
    }
1313
1314
    private static function retrieveStubBannerPath(stdClass $raw, string $basePath): ?string
1315
    {
1316
        if (false === isset($raw->{'banner-file'})) {
1317
            return null;
1318
        }
1319
1320
        $bannerFile = make_path_absolute($raw->{'banner-file'}, $basePath);
1321
1322
        Assertion::file($bannerFile);
1323
1324
        return $bannerFile;
1325
    }
1326
1327
    private static function normalizeStubBannerContents(?string $contents): ?string
1328
    {
1329
        if (null === $contents) {
1330
            return null;
1331
        }
1332
1333
        $banner = explode("\n", $contents);
1334
        $banner = array_map('trim', $banner);
1335
1336
        return implode("\n", $banner);
1337
    }
1338
1339
    private static function retrieveStubPath(stdClass $raw, string $basePath): ?string
1340
    {
1341
        if (isset($raw->stub) && is_string($raw->stub)) {
1342
            $stubPath = make_path_absolute($raw->stub, $basePath);
1343
1344
            Assertion::file($stubPath);
1345
1346
            return $stubPath;
1347
        }
1348
1349
        return null;
1350
    }
1351
1352
    private static function retrieveIsInterceptFileFuncs(stdClass $raw): bool
1353
    {
1354
        if (isset($raw->intercept)) {
1355
            return $raw->intercept;
1356
        }
1357
1358
        return false;
1359
    }
1360
1361
    private static function retrieveIsPrivateKeyPrompt(stdClass $raw): bool
1362
    {
1363
        return isset($raw->{'key-pass'}) && (true === $raw->{'key-pass'});
1364
    }
1365
1366
    private static function retrieveIsStubGenerated(stdClass $raw): bool
1367
    {
1368
        return isset($raw->stub) && (true === $raw->stub);
1369
    }
1370
1371
    private static function retrievePhpScoperConfig(stdClass $raw, string $basePath): PhpScoperConfiguration
1372
    {
1373
        if (!isset($raw->{'php-scoper'})) {
1374
            $configFilePath = make_path_absolute(self::PHP_SCOPER_CONFIG, $basePath);
1375
1376
            return file_exists($configFilePath)
1377
                ? PhpScoperConfiguration::load($configFilePath)
1378
                : PhpScoperConfiguration::load()
1379
             ;
1380
        }
1381
1382
        $configFile = $raw->phpScoper;
1383
1384
        Assertion::string($configFile);
1385
1386
        $configFilePath = make_path_absolute($configFile, $basePath);
1387
1388
        Assertion::file($configFilePath);
1389
        Assertion::readable($configFilePath);
1390
1391
        return PhpScoperConfiguration::load($configFilePath);
1392
    }
1393
1394
    /**
1395
     * Runs a Git command on the repository.
1396
     *
1397
     * @param string $command the command
1398
     *
1399
     * @return string the trimmed output from the command
1400
     */
1401
    private static function runGitCommand(string $command, string $file): string
1402
    {
1403
        $path = dirname($file);
1404
1405
        $process = new Process($command, $path);
1406
1407
        if (0 === $process->run()) {
1408
            return trim($process->getOutput());
1409
        }
1410
1411
        throw new RuntimeException(
1412
            sprintf(
1413
                'The tag or commit hash could not be retrieved from "%s": %s',
1414
                $path,
1415
                $process->getErrorOutput()
1416
            )
1417
        );
1418
    }
1419
1420
    private static function createPhpCompactor(stdClass $raw): Compactor
1421
    {
1422
        // TODO: false === not set; check & add test/doc
1423
        $tokenizer = new Tokenizer();
1424
1425
        if (false === empty($raw->annotations) && isset($raw->annotations->ignore)) {
1426
            $tokenizer->ignore(
1427
                (array) $raw->annotations->ignore
1428
            );
1429
        }
1430
1431
        return new Php($tokenizer);
1432
    }
1433
}
1434