Passed
Pull Request — master (#55)
by Théo
02:09
created

Configuration::retrieveCompactors()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 23
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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

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