Passed
Push — master ( 639186...e6325b )
by Théo
02:27
created

Box::addFile()   C

Complexity

Conditions 7
Paths 36

Size

Total Lines 37
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 37
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 21
nc 36
nop 3
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 function file_exists;
19
use KevinGH\Box\Compactor\PhpScoper;
20
use KevinGH\Box\Composer\ComposerOrchestrator;
21
use KevinGH\Box\PhpScoper\NullScoper;
22
use KevinGH\Box\PhpScoper\Scoper;
23
use Phar;
24
use RecursiveDirectoryIterator;
25
use SplFileInfo;
26
use const DIRECTORY_SEPARATOR;
27
use function Amp\ParallelFunctions\parallelMap;
28
use function Amp\Promise\wait;
29
use function array_map;
30
use function chdir;
31
use function KevinGH\Box\FileSystem\dump_file;
32
use function KevinGH\Box\FileSystem\file_contents;
33
use function KevinGH\Box\FileSystem\make_path_relative;
34
use function KevinGH\Box\FileSystem\make_tmp_dir;
35
use function KevinGH\Box\FileSystem\mkdir;
36
use function KevinGH\Box\FileSystem\remove;
37
38
/**
39
 * Box is a utility class to generate a PHAR.
40
 *
41
 * @private
42
 */
43
final class Box
44
{
45
    public const DEBUG_DIR = '.box';
46
47
    /**
48
     * @var Compactor[]
49
     */
50
    private $compactors = [];
51
52
    /**
53
     * @var string The path to the PHAR file
54
     */
55
    private $file;
56
57
    /**
58
     * @var Phar The PHAR instance
59
     */
60
    private $phar;
61
62
    /**
63
     * @var scalar[] The placeholders with their values
64
     */
65
    private $placeholders = [];
66
67
    /**
68
     * @var string
69
     */
70
    private $basePath;
71
72
    /**
73
     * @var MapFile
74
     */
75
    private $mapFile;
76
77
    /**
78
     * @var Scoper
79
     */
80
    private $scoper;
81
82
    private function __construct(Phar $phar, string $file)
83
    {
84
        $this->phar = $phar;
85
        $this->file = $file;
86
87
        $this->basePath = getcwd();
88
        $this->mapFile = function (): void { };
89
        $this->scoper = new NullScoper();
90
    }
91
92
    /**
93
     * Creates a new PHAR and Box instance.
94
     *
95
     * @param string $file  The PHAR file name
96
     * @param int    $flags Flags to pass to the Phar parent class RecursiveDirectoryIterator
97
     * @param string $alias Alias with which the Phar archive should be referred to in calls to stream functionality
98
     *
99
     * @return Box
100
     *
101
     * @see RecursiveDirectoryIterator
102
     */
103
    public static function create(string $file, int $flags = null, string $alias = null): self
104
    {
105
        // Ensure the parent directory of the PHAR file exists as `new \Phar()` does not create it and would fail
106
        // otherwise.
107
        mkdir(dirname($file));
108
109
        return new self(new Phar($file, (int) $flags, $alias), $file);
110
    }
111
112
    /**
113
     * @param Compactor[] $compactors
114
     */
115
    public function registerCompactors(array $compactors): void
116
    {
117
        Assertion::allIsInstanceOf($compactors, Compactor::class);
118
119
        $this->compactors = $compactors;
120
121
        foreach ($this->compactors as $compactor) {
122
            if ($compactor instanceof PhpScoper) {
123
                $this->scoper = $compactor->getScoper();
124
125
                break;
126
            }
127
        }
128
    }
129
130
    /**
131
     * @param scalar[] $placeholders
132
     */
133
    public function registerPlaceholders(array $placeholders): void
134
    {
135
        $message = 'Expected value "%s" to be a scalar or stringable object.';
136
137
        foreach ($placeholders as $i => $placeholder) {
138
            if (is_object($placeholder)) {
139
                Assertion::methodExists('__toString', $placeholder, $message);
140
141
                $placeholders[$i] = (string) $placeholder;
142
143
                break;
144
            }
145
146
            Assertion::scalar($placeholder, $message);
147
        }
148
149
        $this->placeholders = $placeholders;
150
    }
151
152
    public function registerFileMapping(string $basePath, MapFile $fileMapper): void
153
    {
154
        $this->basePath = $basePath;
155
        $this->mapFile = $fileMapper;
156
    }
157
158
    public function registerStub(string $file): void
159
    {
160
        $contents = self::replacePlaceholders(
161
            $this->placeholders,
162
            file_contents($file)
163
        );
164
165
        $this->phar->setStub($contents);
166
    }
167
168
    /**
169
     * @param SplFileInfo[]|string[] $files
170
     */
171
    public function addFiles(array $files, bool $binary, bool $dumpAutoload = false): void
172
    {
173
        $files = array_map(
174
            function ($file): string {
175
                // Convert files to string as SplFileInfo is not serializable
176
                return (string) $file;
177
            },
178
            $files
179
        );
180
181
        if ($binary) {
182
            foreach ($files as $file) {
183
                $this->addFile($file, null, $binary);
184
            }
185
186
            return;
187
        }
188
189
        $cwd = getcwd();
190
191
        $filesWithContents = $this->processContents($files, $cwd);
192
193
        $tmp = make_tmp_dir('box', __CLASS__);
194
        chdir($tmp);
195
196
        try {
197
            foreach ($filesWithContents as $fileWithContents) {
198
                [$file, $contents] = $fileWithContents;
199
200
                dump_file($file, $contents);
201
202
                if (is_debug_enabled()) {
203
                    dump_file($cwd.'/'.self::DEBUG_DIR.'/'.$file, $contents);
204
                }
205
            }
206
207
            if ($dumpAutoload) {
208
                // Dump autoload without dev dependencies
209
                ComposerOrchestrator::dumpAutoload($this->scoper->getWhitelist(), $this->scoper->getPrefix());
210
            }
211
212
            chdir($cwd);
213
214
            $this->phar->buildFromDirectory($tmp);
215
        } finally {
216
            remove($tmp);
217
        }
218
    }
219
220
    /**
221
     * Adds the a file to the PHAR. The contents will first be compacted and have its placeholders
222
     * replaced.
223
     *
224
     * @param string      $file
225
     * @param null|string $contents If null the content of the file will be used
226
     * @param bool        $binary   When true means the file content shouldn't be processed
227
     *
228
     * @return string File local path
229
     */
230
    public function addFile(string $file, string $contents = null, bool $binary = false): string
231
    {
232
        if (null === $contents) {
233
            $contents = file_contents($file);
234
        }
235
236
        $relativePath = make_path_relative($file, $this->basePath);
237
        $local = ($this->mapFile)($relativePath);
238
239
        if (null === $local) {
240
            $local = $relativePath;
241
        }
242
243
        if ($binary) {
244
            true === file_exists($file)
245
                ? $this->phar->addFile($file, $local)
246
                : $this->phar->addFromString($local, $contents)
247
            ;
248
        } else {
249
            $processedContents = self::compactContents(
250
                $this->compactors,
251
                $local,
252
                self::replacePlaceholders($this->placeholders, $contents)
253
            );
254
255
            $this->phar->addFromString($local, $processedContents);
256
        }
257
258
        if (is_debug_enabled()) {
259
            if (false === isset($processedContents)) {
260
                $processedContents = $contents;
261
            }
262
263
            dump_file(self::DEBUG_DIR.DIRECTORY_SEPARATOR.$relativePath, $processedContents);
264
        }
265
266
        return $local;
267
    }
268
269
    public function getPhar(): Phar
270
    {
271
        return $this->phar;
272
    }
273
274
    /**
275
     * Signs the PHAR using a private key file.
276
     *
277
     * @param string $file     the private key file name
278
     * @param string $password the private key password
279
     */
280
    public function signUsingFile(string $file, string $password = null): void
281
    {
282
        $this->sign(file_contents($file), $password);
283
    }
284
285
    /**
286
     * Signs the PHAR using a private key.
287
     *
288
     * @param string $key      The private key
289
     * @param string $password The private key password
290
     */
291
    public function sign(string $key, ?string $password): void
292
    {
293
        $pubKey = $this->file.'.pubkey';
294
295
        Assertion::writeable(dirname($pubKey));
296
        Assertion::extensionLoaded('openssl');
297
298
        if (file_exists($pubKey)) {
299
            Assertion::file(
300
                $pubKey,
301
                'Cannot create public key: "%s" already exists and is not a file.'
302
            );
303
        }
304
305
        $resource = openssl_pkey_get_private($key, (string) $password);
306
307
        openssl_pkey_export($resource, $private);
308
309
        $details = openssl_pkey_get_details($resource);
0 ignored issues
show
Bug introduced by
It seems like $resource can also be of type false; however, parameter $key of openssl_pkey_get_details() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

309
        $details = openssl_pkey_get_details(/** @scrutinizer ignore-type */ $resource);
Loading history...
310
311
        $this->phar->setSignatureAlgorithm(Phar::OPENSSL, $private);
312
313
        dump_file($pubKey, $details['key']);
314
    }
315
316
    /**
317
     * @param string[] $files
318
     * @param string   $cwd   Current working directory. As the processes are spawned for parallel processing, the
319
     *                        working directory may change so we pass the working directory in which the processing
320
     *                        is supposed to happen. This should not happen during regular usage as all the files are
321
     *                        absolute but it's possible this class is used with relative paths in which case this is
322
     *                        an issue.
323
     *
324
     * @return array array of tuples where the first element is the local file path (path inside the PHAR) and the
325
     *               second element is the processed contents
326
     */
327
    private function processContents(array $files, string $cwd): array
328
    {
329
        $basePath = $this->basePath;
330
        $mapFile = $this->mapFile;
331
        $placeholders = $this->placeholders;
332
        $compactors = $this->compactors;
333
        $bootstrap = $GLOBALS['bootstrap'] ?? function (): void {};
334
335
        $processFile = function (string $file) use ($cwd, $basePath, $mapFile, $placeholders, $compactors, $bootstrap): array {
336
            chdir($cwd);
337
            $bootstrap();
338
339
            $contents = file_contents($file);
340
341
            $relativePath = make_path_relative($file, $basePath);
342
            $local = $mapFile($relativePath);
343
344
            if (null === $local) {
345
                $local = $relativePath;
346
            }
347
348
            $processedContents = self::compactContents(
349
                $compactors,
350
                $local,
351
                self::replacePlaceholders($placeholders, $contents)
352
            );
353
354
            return [$local, $processedContents];
355
        };
356
357
        return is_debug_enabled()
358
            ? array_map($processFile, $files)
359
            : wait(parallelMap($files, $processFile))
360
        ;
361
    }
362
363
    /**
364
     * Replaces the placeholders with their values.
365
     *
366
     * @param array  $placeholders
367
     * @param string $contents     the contents
368
     *
369
     * @return string the replaced contents
370
     */
371
    private static function replacePlaceholders(array $placeholders, string $contents): string
372
    {
373
        return str_replace(
374
            array_keys($placeholders),
375
            array_values($placeholders),
376
            $contents
377
        );
378
    }
379
380
    private static function compactContents(array $compactors, string $file, string $contents): string
381
    {
382
        return array_reduce(
383
            $compactors,
384
            function (string $contents, Compactor $compactor) use ($file): string {
385
                return $compactor->compact($file, $contents);
386
            },
387
            $contents
388
        );
389
    }
390
}
391