Passed
Pull Request — master (#56)
by Théo
02:16
created

Configuration::retrieveBootstrapFile()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 17
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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