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

Configuration::retrieveFilesFromFinders()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 3
nc 2
nop 4
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 function array_map;
18
use function array_unique;
19
use Assert\Assertion;
20
use Closure;
21
use DateTimeImmutable;
22
use const DIRECTORY_SEPARATOR;
23
use Herrera\Annotations\Tokenizer;
24
use Herrera\Box\Compactor\Php as LegacyPhp;
25
use Humbug\PhpScoper\Console\Configuration as PhpScoperConfiguration;
0 ignored issues
show
Bug introduced by
The type Humbug\PhpScoper\Console\Configuration was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

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

749
        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...
750
            $file = $basePath.DIRECTORY_SEPARATOR.canonicalize($file);
751
        }
752
753
        return $file;
754
    }
755
756
    private static function normalizeDirectoryPath(string $directory, string $basePath): string
757
    {
758
        $directory = trim($directory);
759
760
        if (false === self::$fileSystem->isAbsolutePath($directory)) {
761
            $directory = sprintf(
762
                '%s%s',
763
                $basePath.DIRECTORY_SEPARATOR,
764
                rtrim(
765
                    canonicalize($directory),
766
                    DIRECTORY_SEPARATOR
767
                )
768
            );
769
        }
770
771
        return $directory;
772
    }
773
774
    private static function retrieveBootstrapFile(stdClass $raw, string $basePath): ?string
775
    {
776
        // TODO: deprecate its usage & document this BC break. Compactors will not be configurable
777
        // through that extension point so this is pretty much useless unless proven otherwise.
778
        if (false === isset($raw->bootstrap)) {
779
            return null;
780
        }
781
782
        $file = $raw->bootstrap;
783
784
        if (false === is_absolute($file)) {
785
            $file = canonicalize(
786
                $basePath.DIRECTORY_SEPARATOR.$file
787
            );
788
        }
789
790
        if (false === file_exists($file)) {
791
            throw new InvalidArgumentException(
792
                sprintf(
793
                    'The bootstrap path "%s" is not a file or does not exist.',
794
                    $file
795
                )
796
            );
797
        }
798
799
        return $file;
800
    }
801
802
    /**
803
     * @return Compactor[]
804
     */
805
    private static function retrieveCompactors(stdClass $raw, string $basePath): array
806
    {
807
        // TODO: only accept arrays when set unlike the doc says (it allows a string).
808
        if (false === isset($raw->compactors)) {
809
            return [];
810
        }
811
812
        $compactorClasses = array_unique((array) $raw->compactors);
813
814
        return array_map(
815
            function (string $class) use ($raw, $basePath): Compactor {
816
                Assertion::classExists($class, 'The compactor class "%s" does not exist.');
817
                Assertion::implementsInterface($class, Compactor::class, 'The class "%s" is not a compactor class.');
818
819
                if (Php::class === $class || LegacyPhp::class === $class) {
820
                    return self::createPhpCompactor($raw);
821
                }
822
823
                if (PhpScoper::class === $class) {
824
                    $phpScoperConfig = self::retrievePhpScoperConfig($raw, $basePath);
825
826
                    return new PhpScoper(create_scoper(), $phpScoperConfig);
0 ignored issues
show
Bug introduced by
The function create_scoper was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

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