Passed
Push — master ( 205641...2a3534 )
by Théo
02:09
created

Box::sign()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 23
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 23
rs 9.0856
c 0
b 0
f 0
cc 2
eloc 12
nc 2
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 KevinGH\Box\Compactor\PhpScoper;
19
use KevinGH\Box\Composer\ComposerOrchestrator;
20
use KevinGH\Box\PhpScoper\NullScoper;
21
use KevinGH\Box\PhpScoper\Scoper;
22
use Phar;
23
use RecursiveDirectoryIterator;
24
use SplFileInfo;
25
use const DIRECTORY_SEPARATOR;
26
use function Amp\ParallelFunctions\parallelMap;
27
use function Amp\Promise\wait;
28
use function array_map;
29
use function chdir;
30
use function file_exists;
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_dump';
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
            $this->phar->addFromString($local, $contents);
245
        } else {
246
            $processedContents = self::compactContents(
247
                $this->compactors,
248
                $local,
249
                self::replacePlaceholders($this->placeholders, $contents)
250
            );
251
252
            $this->phar->addFromString($local, $processedContents);
253
        }
254
255
        if (is_debug_enabled()) {
256
            if (false === isset($processedContents)) {
257
                $processedContents = $contents;
258
            }
259
260
            dump_file(self::DEBUG_DIR.DIRECTORY_SEPARATOR.$relativePath, $processedContents);
261
        }
262
263
        return $local;
264
    }
265
266
    public function getPhar(): Phar
267
    {
268
        return $this->phar;
269
    }
270
271
    /**
272
     * Signs the PHAR using a private key file.
273
     *
274
     * @param string $file     the private key file name
275
     * @param string $password the private key password
276
     */
277
    public function signUsingFile(string $file, string $password = null): void
278
    {
279
        $this->sign(file_contents($file), $password);
280
    }
281
282
    /**
283
     * Signs the PHAR using a private key.
284
     *
285
     * @param string $key      The private key
286
     * @param string $password The private key password
287
     */
288
    public function sign(string $key, ?string $password): void
289
    {
290
        $pubKey = $this->file.'.pubkey';
291
292
        Assertion::writeable(dirname($pubKey));
293
        Assertion::extensionLoaded('openssl');
294
295
        if (file_exists($pubKey)) {
296
            Assertion::file(
297
                $pubKey,
298
                'Cannot create public key: "%s" already exists and is not a file.'
299
            );
300
        }
301
302
        $resource = openssl_pkey_get_private($key, (string) $password);
303
304
        openssl_pkey_export($resource, $private);
305
306
        $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

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