Completed
Pull Request — master (#31)
by Théo
02:51
created

Configuration::getBasePathRetriever()   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\Console\Configuration as PhpScoperConfiguration;
23
use InvalidArgumentException;
24
use KevinGH\Box\Compactor\Php;
25
use KevinGH\Box\Compactor\PhpScoper;
26
use Phar;
27
use RuntimeException;
28
use SplFileInfo;
29
use stdClass;
30
use Symfony\Component\Filesystem\Filesystem;
31
use Symfony\Component\Finder\Finder;
32
use Symfony\Component\Process\Process;
33
use function Humbug\PhpScoper\create_scoper;
34
35
final class Configuration
36
{
37
    private const DEFAULT_ALIAS = 'default.phar';
38
    private const DEFAULT_DATETIME_FORMAT = 'Y-m-d H:i:s';
39
    private const DEFAULT_REPLACEMENT_SIGIL = '@';
40
    private const PHP_SCOPER_CONFIG = 'scoper.inc.php';
41
42
    /**
43
     * @var null|Filesystem Available only when the Configuration is being instantiated
44
     */
45
    private static $fileSystem;
46
47
    private $fileMode;
48
    private $alias;
49
    private $basePathRetriever;
50
    private $files;
51
    private $binaryFiles;
52
    private $bootstrapFile;
53
    private $compactors;
54
    private $compressionAlgorithm;
55
    private $mainScriptPath;
56
    private $mainScriptContent;
57
    private $map;
58
    private $fileMapper;
59
    private $metadata;
60
    private $mimetypeMapping;
61
    private $mungVariables;
62
    private $notFoundScriptPath;
63
    private $outputPath;
64
    private $privateKeyPassphrase;
65
    private $privateKeyPath;
66
    private $isPrivateKeyPrompt;
67
    private $processedReplacements;
68
    private $shebang;
69
    private $signingAlgorithm;
70
    private $stubBanner;
71
    private $stubBannerPath;
72
    private $stubBannerFromFile;
73
    private $stubPath;
74
    private $isExtractable;
75
    private $isInterceptFileFuncs;
76
    private $isStubGenerated;
77
    private $isWebPhar;
78
79
    /**
80
     * @param string                   $alias
81
     * @param RetrieveRelativeBasePath $basePathRetriever     Utility to private the base path used and be able to retrieve a path relative to it (the base path)
82
     * @param SplFileInfo[]            $files                 List of files
83
     * @param SplFileInfo[]            $binaryFiles           List of binary files
84
     * @param null|string              $bootstrapFile         The bootstrap file path
85
     * @param Compactor[]              $compactors            List of file contents compactors
86
     * @param null|int                 $compressionAlgorithm  Compression algorithm constant value. See the \Phar class constants
87
     * @param null|int                 $fileMode              File mode in octal form
88
     * @param null|string              $mainScriptPath        The main script file path
89
     * @param null|string              $mainScriptContent     The processed content of the main script file
90
     * @param MapFile                  $fileMapper            Utility to map the files from outside and inside the PHAR
91
     * @param mixed                    $metadata              The PHAR Metadata
92
     * @param array                    $mimetypeMapping       The file extension MIME type mapping
93
     * @param array                    $mungVariables         The list of server variables to modify for execution
94
     * @param null|string              $notFoundScriptPath    The file path to the script to execute when a file is not found
95
     * @param string                   $outputPath
96
     * @param null|string              $privateKeyPassphrase
97
     * @param null|string              $privateKeyPath
98
     * @param bool                     $isPrivateKeyPrompt    If the user should be prompted for the private key passphrase
99
     * @param array                    $processedReplacements The processed list of replacement placeholders and their values
100
     * @param null|string              $shebang               The shebang line
101
     * @param int                      $signingAlgorithm      The PHAR siging algorithm. See \Phar constants
102
     * @param null|string              $stubBanner            The stub banner comment
103
     * @param null|string              $stubBannerPath        The path to the stub banner comment file
104
     * @param null|string              $stubBannerFromFile    The stub banner comment from the fine
105
     * @param null|string              $stubPath              The PHAR stub file path
106
     * @param bool                     $isExtractable         Wether or not StubGenerator::extract() should be used
107
     * @param bool                     $isInterceptFileFuncs  wether or not Phar::interceptFileFuncs() should be used
108
     * @param bool                     $isStubGenerated       Wether or not if the PHAR stub should be generated
109
     * @param bool                     $isWebPhar             Wether or not the PHAR is going to be used for the web
110
     */
111
    private function __construct(
112
        string $alias,
113
        RetrieveRelativeBasePath $basePathRetriever,
114
        array $files,
115
        array $binaryFiles,
116
        ?string $bootstrapFile,
117
        array $compactors,
118
        ?int $compressionAlgorithm,
119
        ?int $fileMode,
120
        ?string $mainScriptPath,
121
        ?string $mainScriptContent,
122
        MapFile $fileMapper,
123
        $metadata,
124
        array $mimetypeMapping,
125
        array $mungVariables,
126
        ?string $notFoundScriptPath,
127
        string $outputPath,
128
        ?string $privateKeyPassphrase,
129
        ?string $privateKeyPath,
130
        bool $isPrivateKeyPrompt,
131
        array $processedReplacements,
132
        ?string $shebang,
133
        int $signingAlgorithm,
134
        ?string $stubBanner,
135
        ?string $stubBannerPath,
136
        ?string $stubBannerFromFile,
137
        ?string $stubPath,
138
        bool $isExtractable,
139
        bool $isInterceptFileFuncs,
140
        bool $isStubGenerated,
141
        bool $isWebPhar
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->basePathRetriever = $basePathRetriever;
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->mainScriptContent = $mainScriptContent;
162
        $this->fileMapper = $fileMapper;
163
        $this->metadata = $metadata;
164
        $this->mimetypeMapping = $mimetypeMapping;
165
        $this->mungVariables = $mungVariables;
166
        $this->notFoundScriptPath = $notFoundScriptPath;
167
        $this->outputPath = $outputPath;
168
        $this->privateKeyPassphrase = $privateKeyPassphrase;
169
        $this->privateKeyPath = $privateKeyPath;
170
        $this->isPrivateKeyPrompt = $isPrivateKeyPrompt;
171
        $this->processedReplacements = $processedReplacements;
172
        $this->shebang = $shebang;
173
        $this->signingAlgorithm = $signingAlgorithm;
174
        $this->stubBanner = $stubBanner;
175
        $this->stubBannerPath = $stubBannerPath;
176
        $this->stubBannerFromFile = $stubBannerFromFile;
177
        $this->stubPath = $stubPath;
178
        $this->isExtractable = $isExtractable;
179
        $this->isInterceptFileFuncs = $isInterceptFileFuncs;
180
        $this->isStubGenerated = $isStubGenerated;
181
        $this->isWebPhar = $isWebPhar;
182
    }
183
184
    public static function create(string $file, stdClass $raw): self
185
    {
186
        self::$fileSystem = new Filesystem();
187
188
        $alias = self::retrieveAlias($raw);
189
190
        $basePath = self::retrieveBasePath($file, $raw);
191
        $basePathRetriever = new RetrieveRelativeBasePath($basePath);
192
193
        $blacklistFilter = self::retrieveBlacklistFilter($raw, $basePath);
194
195
        $files = self::retrieveFiles($raw, 'files', $basePath);
196
        $directories = self::retrieveDirectories($raw, 'directories', $basePath, $blacklistFilter);
197
        $filesFromFinders = self::retrieveFilesFromFinders($raw, 'finder', $basePath, $blacklistFilter);
198
199
        $filesAggregate = array_unique(iterator_to_array(iterables_to_iterator($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);
204
205
        $binaryFilesAggregate = array_unique(iterator_to_array(iterables_to_iterator($binaryFiles, $binaryDirectories, ...$binaryFilesFromFinders)));
206
207
        $bootstrapFile = self::retrieveBootstrapFile($raw, $basePath);
208
209
        $compactors = self::retrieveCompactors($raw, $basePath);
210
        $compressionAlgorithm = self::retrieveCompressionAlgorithm($raw);
211
212
        $fileMode = self::retrieveFileMode($raw);
213
214
        $mainScriptPath = self::retrieveMainScriptPath($raw);
215
        $mainScriptContent = self::retrieveMainScriptContents($mainScriptPath, $basePath);
216
217
        $map = self::retrieveMap($raw);
218
        $fileMapper = new MapFile($map);
219
220
        $metadata = self::retrieveMetadata($raw);
221
222
        $mimeTypeMapping = self::retrieveMimetypeMapping($raw);
223
        $mungVariables = self::retrieveMungVariables($raw);
224
        $notFoundScriptPath = self::retrieveNotFoundScriptPath($raw);
225
        $outputPath = self::retrieveOutputPath($raw, $file);
226
227
        $privateKeyPassphrase = self::retrievePrivateKeyPassphrase($raw);
228
        $privateKeyPath = self::retrievePrivateKeyPath($raw);
229
        $isPrivateKeyPrompt = self::retrieveIsPrivateKeyPrompt($raw);
230
231
        $replacements = self::retrieveReplacements($raw);
232
        $processedReplacements = self::retrieveProcessedReplacements($replacements, $raw, $file);
233
234
        $shebang = self::retrieveShebang($raw);
235
236
        $signingAlgorithm = self::retrieveSigningAlgorithm($raw);
237
238
        $stubBanner = self::retrieveStubBanner($raw);
239
        $stubBannerPath = self::retrieveStubBannerPath($raw);
240
        $stubBannerFromFile = self::retrieveStubBannerFromFile($basePath, $stubBannerPath);
241
242
        $stubPath = self::retrieveStubPath($raw);
243
244
        $isExtractable = self::retrieveIsExtractable($raw);
245
        $isInterceptFileFuncs = self::retrieveIsInterceptFileFuncs($raw);
246
        $isStubGenerated = self::retrieveIsStubGenerated($raw);
247
        $isWebPhar = self::retrieveIsWebPhar($raw);
248
249
        self::$fileSystem = null;
250
251
        return new self(
252
            $alias,
253
            $basePathRetriever,
254
            $filesAggregate,
255
            $binaryFilesAggregate,
256
            $bootstrapFile,
257
            $compactors,
258
            $compressionAlgorithm,
259
            $fileMode,
260
            $mainScriptPath,
261
            $mainScriptContent,
262
            $fileMapper,
263
            $metadata,
264
            $mimeTypeMapping,
265
            $mungVariables,
266
            $notFoundScriptPath,
267
            $outputPath,
268
            $privateKeyPassphrase,
269
            $privateKeyPath,
270
            $isPrivateKeyPrompt,
271
            $processedReplacements,
272
            $shebang,
273
            $signingAlgorithm,
274
            $stubBanner,
275
            $stubBannerPath,
276
            $stubBannerFromFile,
277
            $stubPath,
278
            $isExtractable,
279
            $isInterceptFileFuncs,
280
            $isStubGenerated,
281
            $isWebPhar
282
        );
283
    }
284
285
    public function getBasePathRetriever(): RetrieveRelativeBasePath
286
    {
287
        return $this->basePathRetriever;
288
    }
289
290
    public function getAlias(): string
291
    {
292
        return $this->alias;
293
    }
294
295
    public function getBasePath(): string
296
    {
297
        return $this->basePathRetriever->getBasePath();
298
    }
299
300
    /**
301
     * @return SplFileInfo[]
302
     */
303
    public function getFiles(): array
304
    {
305
        return $this->files;
306
    }
307
308
    /**
309
     * @return SplFileInfo[]
310
     */
311
    public function getBinaryFiles(): array
312
    {
313
        return $this->binaryFiles;
314
    }
315
316
    public function getBootstrapFile(): ?string
317
    {
318
        return $this->bootstrapFile;
319
    }
320
321
    public function loadBootstrap(): void
322
    {
323
        $file = $this->bootstrapFile;
324
325
        if (null !== $file) {
326
            include $file;
327
        }
328
    }
329
330
    /**
331
     * @return Compactor[] the list of compactors
332
     */
333
    public function getCompactors(): array
334
    {
335
        return $this->compactors;
336
    }
337
338
    public function getCompressionAlgorithm(): ?int
339
    {
340
        return $this->compressionAlgorithm;
341
    }
342
343
    public function getFileMode(): ?int
344
    {
345
        return $this->fileMode;
346
    }
347
348
    public function getMainScriptPath(): ?string
349
    {
350
        return $this->mainScriptPath;
351
    }
352
353
    public function getMainScriptContent(): ?string
354
    {
355
        return $this->mainScriptContent;
356
    }
357
358
    public function getMimetypeMapping(): array
359
    {
360
        return $this->mimetypeMapping;
361
    }
362
363
    public function getMungVariables(): array
364
    {
365
        return $this->mungVariables;
366
    }
367
368
    public function getNotFoundScriptPath(): ?string
369
    {
370
        return $this->notFoundScriptPath;
371
    }
372
373
    public function getOutputPath(): string
374
    {
375
        return $this->outputPath;
376
    }
377
378
    /**
379
     * @return string[]
380
     */
381
    public function getMap(): array
382
    {
383
        return $this->fileMapper->getMap();
384
    }
385
386
    public function getFileMapper(): MapFile
387
    {
388
        return $this->fileMapper;
389
    }
390
391
    /**
392
     * @return mixed
393
     */
394
    public function getMetadata()
395
    {
396
        return $this->metadata;
397
    }
398
399
    public function getPrivateKeyPassphrase(): ?string
400
    {
401
        return $this->privateKeyPassphrase;
402
    }
403
404
    public function getPrivateKeyPath(): ?string
405
    {
406
        return $this->privateKeyPath;
407
    }
408
409
    public function isPrivateKeyPrompt(): bool
410
    {
411
        return $this->isPrivateKeyPrompt;
412
    }
413
414
    public function getProcessedReplacements(): array
415
    {
416
        return $this->processedReplacements;
417
    }
418
419
    public function getShebang(): ?string
420
    {
421
        return $this->shebang;
422
    }
423
424
    public function getSigningAlgorithm(): int
425
    {
426
        return $this->signingAlgorithm;
427
    }
428
429
    public function getStubBanner(): ?string
430
    {
431
        return $this->stubBanner;
432
    }
433
434
    public function getStubBannerPath(): ?string
435
    {
436
        return $this->stubBannerPath;
437
    }
438
439
    public function getStubBannerFromFile()
440
    {
441
        return $this->stubBannerFromFile;
442
    }
443
444
    public function getStubPath(): ?string
445
    {
446
        return $this->stubPath;
447
    }
448
449
    public function isExtractable(): bool
450
    {
451
        return $this->isExtractable;
452
    }
453
454
    public function isInterceptFileFuncs(): bool
455
    {
456
        return $this->isInterceptFileFuncs;
457
    }
458
459
    public function isStubGenerated(): bool
460
    {
461
        return $this->isStubGenerated;
462
    }
463
464
    public function isWebPhar(): bool
465
    {
466
        return $this->isWebPhar;
467
    }
468
469
    private static function retrieveAlias(stdClass $raw): string
470
    {
471
        $alias = $raw->alias ?? self::DEFAULT_ALIAS;
472
473
        $alias = trim($alias);
474
475
        Assertion::notEmpty($alias, 'A PHAR alias cannot be empty.');
476
477
        return $alias;
478
    }
479
480
    private static function retrieveBasePath(string $file, stdClass $raw): string
481
    {
482
        if (false === isset($raw->{'base-path'})) {
483
            return realpath(dirname($file));
484
        }
485
486
        $basePath = trim($raw->{'base-path'});
487
488
        Assertion::directory(
489
            $basePath,
490
            'The base path "%s" is not a directory or does not exist.'
491
        );
492
493
        return realpath($basePath);
494
    }
495
496
    /**
497
     * @param stdClass $raw
498
     * @param string   $basePath
499
     *
500
     * @return Closure
501
     */
502
    private static function retrieveBlacklistFilter(stdClass $raw, string $basePath): Closure
503
    {
504
        $blacklist = self::retrieveBlacklist($raw, $basePath);
505
506
        return function (SplFileInfo $file) use ($blacklist): ?bool {
507
            if (in_array($file->getRealPath(), $blacklist, true)) {
508
                return false;
509
            }
510
511
            return null;
512
        };
513
    }
514
515
    /**
516
     * @return string[]
517
     */
518
    private static function retrieveBlacklist(stdClass $raw, string $basePath): array
519
    {
520
        if (false === isset($raw->blacklist)) {
521
            return [];
522
        }
523
524
        $blacklist = $raw->blacklist;
525
526
        $normalizePath = function ($file) use ($basePath): string {
527
            return self::normalizeFilePath($file, $basePath);
528
        };
529
530
        return array_map($normalizePath, $blacklist);
531
    }
532
533
    /**
534
     * @param stdClass $raw
535
     * @param string   $key      Config property name
536
     * @param string   $basePath
537
     *
538
     * @return SplFileInfo[]
539
     */
540
    private static function retrieveFiles(stdClass $raw, string $key, string $basePath): array
541
    {
542
        if (false === isset($raw->{$key})) {
543
            return [];
544
        }
545
546
        $files = (array) $raw->{$key};
547
548
        Assertion::allString($files);
549
550
        $normalizePath = function (string $file) use ($basePath, $key): SplFileInfo {
551
            $file = self::normalizeFilePath($file, $basePath);
552
553
            Assertion::file(
554
                $file,
555
                sprintf(
556
                    '"%s" must contain a list of existing files. Could not find "%%s".',
557
                    $key
558
                )
559
            );
560
561
            return new SplFileInfo($file);
562
        };
563
564
        return array_map($normalizePath, $files);
565
    }
566
567
    /**
568
     * @param stdClass $raw
569
     * @param string   $key             Config property name
570
     * @param string   $basePath
571
     * @param Closure  $blacklistFilter
572
     *
573
     * @return iterable|SplFileInfo[]
574
     */
575
    private static function retrieveDirectories(stdClass $raw, string $key, string $basePath, Closure $blacklistFilter): iterable
576
    {
577
        $directories = self::retrieveDirectoryPaths($raw, $key, $basePath);
578
579
        if ([] !== $directories) {
580
            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...
581
                ->files()
582
                ->filter($blacklistFilter)
583
                ->ignoreVCS(true)
584
                ->in($directories)
585
            ;
586
        }
587
588
        return [];
589
    }
590
591
    /**
592
     * @param stdClass $raw
593
     * @param string   $basePath
594
     * @param Closure  $blacklistFilter
595
     *
596
     * @return iterable[]|SplFileInfo[][]
597
     */
598
    private static function retrieveFilesFromFinders(stdClass $raw, string $key, string $basePath, Closure $blacklistFilter): array
599
    {
600
        if (isset($raw->{$key})) {
601
            return self::processFinders($raw->{$key}, $basePath, $blacklistFilter);
602
        }
603
604
        return [];
605
    }
606
607
    /**
608
     * @param array   $findersConfig   the configuration
609
     * @param string  $basePath
610
     * @param Closure $blacklistFilter
611
     *
612
     * @return Finder[]|SplFileInfo[][]
613
     */
614
    private static function processFinders(array $findersConfig, string $basePath, Closure $blacklistFilter): array
615
    {
616
        $processFinderConfig = function (stdClass $config) use ($basePath, $blacklistFilter) {
617
            return self::processFinder($config, $basePath, $blacklistFilter);
618
        };
619
620
        return array_map($processFinderConfig, $findersConfig);
621
    }
622
623
    /**
624
     * @param array   $findersConfig   the configuration
625
     * @param string  $basePath
626
     * @param Closure $blacklistFilter
627
     *
628
     * @return Finder
629
     */
630
    private static function processFinder(stdClass $config, string $basePath, Closure $blacklistFilter): Finder
631
    {
632
        $finder = Finder::create()
633
            ->files()
634
            ->filter($blacklistFilter)
635
            ->ignoreVCS(true)
636
        ;
637
638
        $normalizedConfig = (function (array $config, Finder $finder): array {
639
            $normalizedConfig = [];
640
641
            foreach ($config as $method => $arguments) {
642
                $method = trim($method);
643
                $arguments = (array) $arguments;
644
645
                Assertion::methodExists(
646
                    $method,
647
                    $finder,
648
                    'The method "Finder::%s" does not exist.'
649
                );
650
651
                $normalizedConfig[$method] = $arguments;
652
            }
653
654
            krsort($normalizedConfig);
655
656
            return $normalizedConfig;
657
        })((array) $config, $finder);
658
659
        $createNormalizedDirectories = function (string $directory) use ($basePath): string {
660
            $directory = self::normalizeDirectoryPath($directory, $basePath);
661
662
            Assertion::directory($directory);
663
664
            return $directory;
665
        };
666
667
        $normalizeFileOrDirectory = function (string &$fileOrDirectory) use ($basePath): void {
668
            $fileOrDirectory = self::normalizeDirectoryPath($fileOrDirectory, $basePath);
669
670
            if (false === file_exists($fileOrDirectory)) {
671
                throw new InvalidArgumentException(
672
                    sprintf(
673
                        'Path "%s" was expected to be a file or directory.',
674
                        $fileOrDirectory
675
                    )
676
                );
677
            }
678
679
            if (false === is_file($fileOrDirectory)) {
680
                Assertion::directory($fileOrDirectory);
681
            } else {
682
                Assertion::file($fileOrDirectory);
683
            }
684
        };
685
686
        foreach ($normalizedConfig as $method => $arguments) {
687
            if ('in' === $method) {
688
                $normalizedConfig[$method] = $arguments = array_map($createNormalizedDirectories, $arguments);
689
            }
690
691
            if ('exclude' === $method) {
692
                $arguments = array_unique(array_map('trim', $arguments));
693
            }
694
695
            if ('append' === $method) {
696
                array_walk($arguments, $normalizeFileOrDirectory);
697
698
                $arguments = [$arguments];
699
            }
700
701
            foreach ($arguments as $argument) {
702
                $finder->$method($argument);
703
            }
704
        }
705
706
        return $finder;
707
    }
708
709
    /**
710
     * @param stdClass $raw
711
     * @param string   $key      Config property name
712
     * @param string   $basePath
713
     *
714
     * @return string[]
715
     */
716
    private static function retrieveDirectoryPaths(stdClass $raw, string $key, string $basePath): array
717
    {
718
        if (false === isset($raw->{$key})) {
719
            return [];
720
        }
721
722
        $directories = $raw->{$key};
723
724
        $normalizeDirectory = function (string $directory) use ($basePath, $key): string {
725
            $directory = self::normalizeDirectoryPath($directory, $basePath);
726
727
            Assertion::directory(
728
                $directory,
729
                sprintf(
730
                    '"%s" must contain a list of existing directories. Could not find "%%s".',
731
                    $key
732
                )
733
            );
734
735
            return $directory;
736
        };
737
738
        return array_map($normalizeDirectory, $directories);
739
    }
740
741
    private static function normalizeFilePath(string $file, string $basePath): string
742
    {
743
        $file = trim($file);
744
745
        if (false === self::$fileSystem->isAbsolutePath($file)) {
0 ignored issues
show
Bug introduced by
The method isAbsolutePath() does not exist on null. ( Ignorable by Annotation )

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

745
        if (false === self::$fileSystem->/** @scrutinizer ignore-call */ isAbsolutePath($file)) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
746
            $file = $basePath.DIRECTORY_SEPARATOR.canonicalize($file);
747
        }
748
749
        return $file;
750
    }
751
752
    private static function normalizeDirectoryPath(string $directory, string $basePath): string
753
    {
754
        $directory = trim($directory);
755
756
        if (false === self::$fileSystem->isAbsolutePath($directory)) {
757
            $directory = sprintf(
758
                '%s%s',
759
                $basePath.DIRECTORY_SEPARATOR,
760
                rtrim(
761
                    canonicalize($directory),
762
                    DIRECTORY_SEPARATOR
763
                )
764
            );
765
        }
766
767
        return $directory;
768
    }
769
770
    private static function retrieveBootstrapFile(stdClass $raw, string $basePath): ?string
771
    {
772
        // TODO: deprecate its usage & document this BC break. Compactors will not be configurable
773
        // through that extension point so this is pretty much useless unless proven otherwise.
774
        if (false === isset($raw->bootstrap)) {
775
            return null;
776
        }
777
778
        $file = $raw->bootstrap;
779
780
        if (false === is_absolute($file)) {
781
            $file = canonicalize(
782
                $basePath.DIRECTORY_SEPARATOR.$file
783
            );
784
        }
785
786
        if (false === file_exists($file)) {
787
            throw new InvalidArgumentException(
788
                sprintf(
789
                    'The bootstrap path "%s" is not a file or does not exist.',
790
                    $file
791
                )
792
            );
793
        }
794
795
        return $file;
796
    }
797
798
    /**
799
     * @return Compactor[]
800
     */
801
    private static function retrieveCompactors(stdClass $raw, string $basePath): array
802
    {
803
        // TODO: only accept arrays when set unlike the doc says (it allows a string).
804
        if (false === isset($raw->compactors)) {
805
            return [];
806
        }
807
808
        $compactorClasses = array_unique((array) $raw->compactors);
809
810
        return array_map(
811
            function (string $class) use ($raw, $basePath): Compactor {
812
                Assertion::classExists($class, 'The compactor class "%s" does not exist.');
813
                Assertion::implementsInterface($class, Compactor::class, 'The class "%s" is not a compactor class.');
814
815
                if (Php::class === $class || LegacyPhp::class === $class) {
816
                    return self::createPhpCompactor($raw);
817
                }
818
819
                if (PhpScoper::class === $class) {
820
                    $phpScoperConfig = self::retrievePhpScoperConfig($raw, $basePath);
821
822
                    return new PhpScoper(create_scoper(), $phpScoperConfig);
823
                }
824
825
                return new $class();
826
            },
827
            $compactorClasses
828
        );
829
    }
830
831
    private static function retrieveCompressionAlgorithm(stdClass $raw): ?int
832
    {
833
        // TODO: if in dev mode (when added), do not comment about the compression.
834
        // If not, add a warning to notify the user if no compression algorithm is used
835
        // provided the PHAR is not configured for web purposes.
836
        // If configured for the web, add a warning when a compression algorithm is used
837
        // as this can result in an overhead. Add a doc link explaining this.
838
        //
839
        // Unlike the doc: do not accept integers and document this BC break.
840
        if (false === isset($raw->compression)) {
841
            return null;
842
        }
843
844
        if (false === is_string($raw->compression)) {
845
            Assertion::integer(
846
                $raw->compression,
847
                'Expected compression to be an algorithm name, found %s instead.'
848
            );
849
850
            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...
851
        }
852
853
        $knownAlgorithmNames = array_keys(get_phar_compression_algorithms());
854
855
        Assertion::inArray(
856
            $raw->compression,
857
            $knownAlgorithmNames,
858
            sprintf(
859
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
860
                implode('", "', $knownAlgorithmNames)
861
            )
862
        );
863
864
        $value = get_phar_compression_algorithms()[$raw->compression];
865
866
        // Phar::NONE is not valid for compressFiles()
867
        if (Phar::NONE === $value) {
868
            return null;
869
        }
870
871
        return $value;
872
    }
873
874
    private static function retrieveFileMode(stdClass $raw): ?int
875
    {
876
        if (isset($raw->chmod)) {
877
            return intval($raw->chmod, 8);
878
        }
879
880
        return null;
881
    }
882
883
    private static function retrieveMainScriptPath(stdClass $raw): ?string
884
    {
885
        // TODO: check if is used for the web as well when web is set to true
886
        // If that the case make this field mandatory otherwise adjust the check
887
        // rules accordinly to ensure we do not have an empty PHAR
888
        if (isset($raw->main)) {
889
            return canonicalize($raw->main);
890
        }
891
892
        return null;
893
    }
894
895
    private static function retrieveMainScriptContents(?string $mainScriptPath, string $basePath): ?string
896
    {
897
        if (null === $mainScriptPath) {
898
            return null;
899
        }
900
901
        $mainScriptPath = $basePath.DIRECTORY_SEPARATOR.$mainScriptPath;
902
903
        Assertion::readable($mainScriptPath);
904
905
        $contents = file_get_contents($mainScriptPath);
906
907
        // Remove the shebang line
908
        return preg_replace('/^#!.*\s*/', '', $contents);
909
    }
910
911
    /**
912
     * @return string[][]
913
     */
914
    private static function retrieveMap(stdClass $raw): array
915
    {
916
        if (false === isset($raw->map)) {
917
            return [];
918
        }
919
920
        $map = [];
921
922
        foreach ((array) $raw->map as $item) {
923
            $processed = [];
924
925
            foreach ($item as $match => $replace) {
926
                $processed[canonicalize(trim($match))] = canonicalize(trim($replace));
927
            }
928
929
            if (isset($processed['_empty_'])) {
930
                $processed[''] = $processed['_empty_'];
931
932
                unset($processed['_empty_']);
933
            }
934
935
            $map[] = $processed;
936
        }
937
938
        return $map;
939
    }
940
941
    /**
942
     * @return mixed
943
     */
944
    private static function retrieveMetadata(stdClass $raw)
945
    {
946
        // TODO: the doc currently say this can be any value; check if true
947
        // and if not add checks accordingly
948
        //
949
        // Also review the doc as I don't find it very helpful...
950
        if (isset($raw->metadata)) {
951
            if (is_object($raw->metadata)) {
952
                return (array) $raw->metadata;
953
            }
954
955
            return $raw->metadata;
956
        }
957
958
        return null;
959
    }
960
961
    private static function retrieveMimetypeMapping(stdClass $raw): array
962
    {
963
        // TODO: this parameter is not clear to me: review usage, doc & checks
964
        if (isset($raw->mimetypes)) {
965
            return (array) $raw->mimetypes;
966
        }
967
968
        return [];
969
    }
970
971
    private static function retrieveMungVariables(stdClass $raw): array
972
    {
973
        // TODO: this parameter is not clear to me: review usage, doc & checks
974
        // TODO: add error/warning if used when web is not enabled
975
        if (isset($raw->mung)) {
976
            return (array) $raw->mung;
977
        }
978
979
        return [];
980
    }
981
982
    private static function retrieveNotFoundScriptPath(stdClass $raw): ?string
983
    {
984
        // TODO: this parameter is not clear to me: review usage, doc & checks
985
        // TODO: add error/warning if used when web is not enabled
986
        if (isset($raw->{'not-found'})) {
987
            return $raw->{'not-found'};
988
        }
989
990
        return null;
991
    }
992
993
    private static function retrieveOutputPath(stdClass $raw, string $file): string
994
    {
995
        // TODO: make this path relative to the base path like everything else
996
        // otherwise this is really confusing. This is a BC break that needs to be
997
        // documented though (and update the doc accordingly as well)
998
        $base = getcwd().DIRECTORY_SEPARATOR;
999
1000
        if (isset($raw->output)) {
1001
            $path = $raw->output;
1002
1003
            if (false === is_absolute($path)) {
1004
                $path = canonicalize($base.$path);
1005
            }
1006
        } else {
1007
            $path = $base.self::DEFAULT_ALIAS;
1008
        }
1009
1010
        if (false !== strpos($path, '@'.'git-version@')) {
1011
            $gitVersion = self::retrieveGitVersion($file);
1012
1013
            $path = str_replace('@'.'git-version@', $gitVersion, $path);
1014
        }
1015
1016
        return $path;
1017
    }
1018
1019
    private static function retrievePrivateKeyPassphrase(stdClass $raw): ?string
1020
    {
1021
        // TODO: add check to not allow this setting without the private key path
1022
        if (isset($raw->{'key-pass'})
1023
            && is_string($raw->{'key-pass'})
1024
        ) {
1025
            return $raw->{'key-pass'};
1026
        }
1027
1028
        return null;
1029
    }
1030
1031
    private static function retrievePrivateKeyPath(stdClass $raw): ?string
1032
    {
1033
        // TODO: If passed need to check its existence
1034
        // Also need
1035
1036
        if (isset($raw->key)) {
1037
            return $raw->key;
1038
        }
1039
1040
        return null;
1041
    }
1042
1043
    private static function retrieveReplacements(stdClass $raw): array
1044
    {
1045
        // TODO: add exmample in the doc
1046
        // Add checks against the values
1047
        if (isset($raw->replacements)) {
1048
            return (array) $raw->replacements;
1049
        }
1050
1051
        return [];
1052
    }
1053
1054
    private static function retrieveProcessedReplacements(
1055
        array $replacements,
1056
        stdClass $raw,
1057
        string $file
1058
    ): array {
1059
        if (null !== ($git = self::retrieveGitHashPlaceholder($raw))) {
1060
            $replacements[$git] = self::retrieveGitHash($file);
1061
        }
1062
1063
        if (null !== ($git = self::retrieveGitShortHashPlaceholder($raw))) {
1064
            $replacements[$git] = self::retrieveGitHash($file, true);
1065
        }
1066
1067
        if (null !== ($git = self::retrieveGitTagPlaceholder($raw))) {
1068
            $replacements[$git] = self::retrieveGitTag($file);
1069
        }
1070
1071
        if (null !== ($git = self::retrieveGitVersionPlaceholder($raw))) {
1072
            $replacements[$git] = self::retrieveGitVersion($file);
1073
        }
1074
1075
        if (null !== ($date = self::retrieveDatetimeNowPlaceHolder($raw))) {
1076
            $replacements[$date] = self::retrieveDatetimeNow(
1077
                self::retrieveDatetimeFormat($raw)
1078
            );
1079
        }
1080
1081
        $sigil = self::retrieveReplacementSigil($raw);
1082
1083
        foreach ($replacements as $key => $value) {
1084
            unset($replacements[$key]);
1085
            $replacements["$sigil$key$sigil"] = $value;
1086
        }
1087
1088
        return $replacements;
1089
    }
1090
1091
    private static function retrieveGitHashPlaceholder(stdClass $raw): ?string
1092
    {
1093
        if (isset($raw->{'git-commit'})) {
1094
            return $raw->{'git-commit'};
1095
        }
1096
1097
        return null;
1098
    }
1099
1100
    /**
1101
     * @param string $file
1102
     * @param bool   $short Use the short version
1103
     *
1104
     * @return string the commit hash
1105
     */
1106
    private static function retrieveGitHash(string $file, bool $short = false): string
1107
    {
1108
        return self::runGitCommand(
1109
            sprintf(
1110
                'git log --pretty="%s" -n1 HEAD',
1111
                $short ? '%h' : '%H'
1112
            ),
1113
            $file
1114
        );
1115
    }
1116
1117
    private static function retrieveGitShortHashPlaceholder(stdClass $raw): ?string
1118
    {
1119
        if (isset($raw->{'git-commit-short'})) {
1120
            return $raw->{'git-commit-short'};
1121
        }
1122
1123
        return null;
1124
    }
1125
1126
    private static function retrieveGitTagPlaceholder(stdClass $raw): ?string
1127
    {
1128
        if (isset($raw->{'git-tag'})) {
1129
            return $raw->{'git-tag'};
1130
        }
1131
1132
        return null;
1133
    }
1134
1135
    private static function retrieveGitTag(string $file): ?string
1136
    {
1137
        return self::runGitCommand('git describe --tags HEAD', $file);
1138
    }
1139
1140
    private static function retrieveGitVersionPlaceholder(stdClass $raw): ?string
1141
    {
1142
        if (isset($raw->{'git-version'})) {
1143
            return $raw->{'git-version'};
1144
        }
1145
1146
        return null;
1147
    }
1148
1149
    private static function retrieveGitVersion(string $file): ?string
1150
    {
1151
        // TODO: check if is still relevant as IMO we are better off using OcramiusVersionPackage
1152
        // to avoid messing around with that
1153
1154
        try {
1155
            return self::retrieveGitTag($file);
1156
        } catch (RuntimeException $exception) {
1157
            try {
1158
                return self::retrieveGitHash($file, true);
1159
            } catch (RuntimeException $exception) {
1160
                throw new RuntimeException(
1161
                    sprintf(
1162
                        'The tag or commit hash could not be retrieved from "%s": %s',
1163
                        dirname($file),
1164
                        $exception->getMessage()
1165
                    ),
1166
                    0,
1167
                    $exception
1168
                );
1169
            }
1170
        }
1171
    }
1172
1173
    private static function retrieveDatetimeNowPlaceHolder(stdClass $raw): ?string
1174
    {
1175
        // TODO: double check why this is done and how it is used it's not completely clear to me.
1176
        // Also make sure the documentation is up to date after.
1177
        // Instead of having two sistinct doc entries for `datetime` and `datetime-format`, it would
1178
        // be better to have only one element IMO like:
1179
        //
1180
        // "datetime": {
1181
        //   "value": "val",
1182
        //   "format": "Y-m-d"
1183
        // }
1184
        //
1185
        // Also add a check that one cannot be provided without the other. Or maybe it should? I guess
1186
        // if the datetime format is the default one it's ok; but in any case the format should not
1187
        // be added without the datetime value...
1188
1189
        if (isset($raw->{'datetime'})) {
1190
            return $raw->{'datetime'};
1191
        }
1192
1193
        return null;
1194
    }
1195
1196
    private static function retrieveDatetimeNow(string $format)
1197
    {
1198
        $now = new DateTimeImmutable('now');
1199
1200
        $datetime = $now->format($format);
1201
1202
        if (!$datetime) {
1203
            throw new InvalidArgumentException(
1204
                sprintf(
1205
                    '""%s" is not a valid PHP date format',
1206
                    $format
1207
                )
1208
            );
1209
        }
1210
1211
        return $datetime;
1212
    }
1213
1214
    private static function retrieveDatetimeFormat(stdClass $raw): string
1215
    {
1216
        if (isset($raw->{'datetime_format'})) {
1217
            return $raw->{'datetime_format'};
1218
        }
1219
1220
        return self::DEFAULT_DATETIME_FORMAT;
1221
    }
1222
1223
    private static function retrieveReplacementSigil(stdClass $raw)
1224
    {
1225
        if (isset($raw->{'replacement-sigil'})) {
1226
            return $raw->{'replacement-sigil'};
1227
        }
1228
1229
        return self::DEFAULT_REPLACEMENT_SIGIL;
1230
    }
1231
1232
    private static function retrieveShebang(stdClass $raw): ?string
1233
    {
1234
        // TODO: unlike the doc says do not allow empty strings.
1235
        // Leverage `Assertion` here?
1236
        if (false === isset($raw->shebang)) {
1237
            return null;
1238
        }
1239
1240
        if (('' === $raw->shebang) || (false === $raw->shebang)) {
1241
            return '';
1242
        }
1243
1244
        $shebang = trim($raw->shebang);
1245
1246
        if ('#!' !== substr($shebang, 0, 2)) {
1247
            throw new InvalidArgumentException(
1248
                sprintf(
1249
                    'The shebang line must start with "#!": %s',
1250
                    $shebang
1251
                )
1252
            );
1253
        }
1254
1255
        return $shebang;
1256
    }
1257
1258
    private static function retrieveSigningAlgorithm(stdClass $raw): int
1259
    {
1260
        // TODO: trigger warning: if no signing algorithm is given provided we are not in dev mode
1261
        // TODO: trigger a warning if the signing algorithm used is weak
1262
        // TODO: no longer accept strings & document BC break
1263
        if (false === isset($raw->algorithm)) {
1264
            return Phar::SHA1;
1265
        }
1266
1267
        if (is_string($raw->algorithm)) {
1268
            if (false === defined('Phar::'.$raw->algorithm)) {
1269
                throw new InvalidArgumentException(
1270
                    sprintf(
1271
                        'The signing algorithm "%s" is not supported.',
1272
                        $raw->algorithm
1273
                    )
1274
                );
1275
            }
1276
1277
            return constant('Phar::'.$raw->algorithm);
1278
        }
1279
1280
        return $raw->algorithm;
1281
    }
1282
1283
    private static function retrieveStubBanner(stdClass $raw): ?string
1284
    {
1285
        if (isset($raw->{'banner'})) {
1286
            return $raw->{'banner'};
1287
        }
1288
1289
        return null;
1290
    }
1291
1292
    private static function retrieveStubBannerPath(stdClass $raw): ?string
1293
    {
1294
        // TODO: if provided check its existence here or should it be defered to later?
1295
        // Works case this check can be duplicated...
1296
        //
1297
        // Check if is relative to base path: if not make it so (may be a BC break to document).
1298
        // Once checked, a mention in the doc that this path is relative to base-path (unless
1299
        // absolute).
1300
        // Check that the path is not provided if a banner is already provided.
1301
        if (isset($raw->{'banner-file'})) {
1302
            return canonicalize($raw->{'banner-file'});
1303
        }
1304
1305
        return null;
1306
    }
1307
1308
    private static function retrieveStubBannerFromFile(string $basePath, ?string $stubBannerPath): ?string
1309
    {
1310
        // TODO: Add checks
1311
        // TODO: The documentation is not clear enough IMO
1312
        if (null == $stubBannerPath) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $stubBannerPath of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
1313
            return null;
1314
        }
1315
1316
        $stubBannerPath = $basePath.DIRECTORY_SEPARATOR.$stubBannerPath;
1317
1318
        if (false === ($contents = @file_get_contents($stubBannerPath))) {
0 ignored issues
show
introduced by
The condition false === $contents = @f...ntents($stubBannerPath) can never be true.
Loading history...
1319
            $errors = error_get_last();
1320
1321
            if (null === $errors) {
1322
                $errors = ['message' => 'failed to get contents of "'.$stubBannerPath.'""'];
1323
            }
1324
1325
            throw new InvalidArgumentException($errors['message']);
1326
        }
1327
1328
        return $contents;
1329
    }
1330
1331
    private static function retrieveStubPath(stdClass $raw): ?string
1332
    {
1333
        if (isset($raw->stub) && is_string($raw->stub)) {
1334
            return $raw->stub;
1335
        }
1336
1337
        return null;
1338
    }
1339
1340
    private static function retrieveIsExtractable(stdClass $raw): bool
1341
    {
1342
        // TODO: look it up, really not clear to me neither is the doc
1343
        if (isset($raw->extract)) {
1344
            return $raw->extract;
1345
        }
1346
1347
        return false;
1348
    }
1349
1350
    private static function retrieveIsInterceptFileFuncs(stdClass $raw): bool
1351
    {
1352
        if (isset($raw->intercept)) {
1353
            return $raw->intercept;
1354
        }
1355
1356
        return false;
1357
    }
1358
1359
    private static function retrieveIsPrivateKeyPrompt(stdClass $raw): bool
1360
    {
1361
        if (isset($raw->{'key-pass'})
1362
            && (true === $raw->{'key-pass'})) {
1363
            return true;
1364
        }
1365
1366
        return false;
1367
    }
1368
1369
    private static function retrieveIsStubGenerated(stdClass $raw): bool
1370
    {
1371
        if (isset($raw->stub) && (true === $raw->stub)) {
1372
            return true;
1373
        }
1374
1375
        return false;
1376
    }
1377
1378
    private static function retrieveIsWebPhar(stdClass $raw): bool
1379
    {
1380
        // TODO: doc is not clear enough
1381
        // Also check if is compatible web + CLI
1382
        if (isset($raw->web)) {
1383
            return $raw->web;
1384
        }
1385
1386
        return false;
1387
    }
1388
1389
    private static function retrievePhpScoperConfig(stdClass $raw, string $basePath): PhpScoperConfiguration
1390
    {
1391
        if (!isset($raw->{'php-scoper'})) {
1392
            $configFilePath = $basePath.DIRECTORY_SEPARATOR.self::PHP_SCOPER_CONFIG;
1393
1394
             return file_exists($configFilePath)
1395
                ? PhpScoperConfiguration::load($configFilePath)
1396
                : PhpScoperConfiguration::load()
1397
             ;
1398
        }
1399
1400
        $configFile = $raw->phpScoper;
1401
1402
        Assertion::string($configFile);
1403
1404
        if (false === self::$fileSystem->isAbsolutePath($configFile)) {
1405
            $configFilePath = $basePath.DIRECTORY_SEPARATOR.$configFile;
1406
        } else {
1407
            $configFilePath = $configFile;
1408
        }
1409
1410
        Assertion::file($configFilePath);
1411
        Assertion::readable($configFilePath);
1412
1413
        return PhpScoperConfiguration::load($configFilePath);
1414
    }
1415
1416
    /**
1417
     * Runs a Git command on the repository.
1418
     *
1419
     * @param string $command the command
1420
     *
1421
     * @return string the trimmed output from the command
1422
     */
1423
    private static function runGitCommand(string $command, string $file): string
1424
    {
1425
        $path = dirname($file);
1426
1427
        $process = new Process($command, $path);
1428
1429
        if (0 === $process->run()) {
1430
            return trim($process->getOutput());
1431
        }
1432
1433
        throw new RuntimeException(
1434
            sprintf(
1435
                'The tag or commit hash could not be retrieved from "%s": %s',
1436
                $path,
1437
                $process->getErrorOutput()
1438
            )
1439
        );
1440
    }
1441
1442
    private static function createPhpCompactor(stdClass $raw): Compactor
1443
    {
1444
        // TODO: false === not set; check & add test/doc
1445
        $tokenizer = new Tokenizer();
1446
1447
        if (false === empty($raw->annotations) && isset($raw->annotations->ignore)) {
1448
            $tokenizer->ignore(
1449
                (array) $raw->annotations->ignore
1450
            );
1451
        }
1452
1453
        return new Php($tokenizer);
1454
    }
1455
}
1456