Completed
Pull Request — master (#31)
by Théo
02:15
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 Humbug\PhpScoper\Autoload\ScoperAutoloadGenerator;
19
use KevinGH\Box\Compactor\PhpScoper;
20
use Phar;
21
use PhpScoper59f9b12e4ed48\Humbug\PhpScoper\Autoload\ScoperAutoloadGenerator;
0 ignored issues
show
Bug introduced by
A parse error occurred: Cannot use PhpScoper59f9b12e4ed48\Humbug\PhpScoper\Autoload\ScoperAutoloadGenerator as ScoperAutoloadGenerator because the name is already in use
Loading history...
22
use RecursiveDirectoryIterator;
23
use SplFileInfo;
24
use function Amp\ParallelFunctions\parallelMap;
25
use function Amp\Promise\wait;
26
use function KevinGH\Box\FileSystem\dump_file;
27
use function KevinGH\Box\FileSystem\file_contents;
28
use function KevinGH\Box\FileSystem\make_path_absolute;
29
use function KevinGH\Box\FileSystem\make_tmp_dir;
30
use function KevinGH\Box\FileSystem\mkdir;
31
use function KevinGH\Box\FileSystem\remove;
32
33
/**
34
 * Box is a utility class to generate a PHAR.
35
 */
36
final class Box
37
{
38
    /**
39
     * @var Compactor[]
40
     */
41
    private $compactors = [];
42
43
    /**
44
     * @var string The path to the PHAR file
45
     */
46
    private $file;
47
48
    /**
49
     * @var Phar The PHAR instance
50
     */
51
    private $phar;
52
53
    /**
54
     * @var scalar[] The placeholders with their values
55
     */
56
    private $placeholders = [];
57
58
    /**
59
     * @var RetrieveRelativeBasePath
60
     */
61
    private $retrieveRelativeBasePath;
62
63
    /**
64
     * @var MapFile
65
     */
66
    private $mapFile;
67
68
    private function __construct(Phar $phar, string $file)
69
    {
70
        $this->phar = $phar;
71
        $this->file = $file;
72
73
        $this->retrieveRelativeBasePath = function (string $path) { return $path; };
74
        $this->mapFile = function (): void { };
75
    }
76
77
    /**
78
     * Creates a new PHAR and Box instance.
79
     *
80
     * @param string $file  The PHAR file name
81
     * @param int    $flags Flags to pass to the Phar parent class RecursiveDirectoryIterator
82
     * @param string $alias Alias with which the Phar archive should be referred to in calls to stream functionality
83
     *
84
     * @return Box
85
     *
86
     * @see RecursiveDirectoryIterator
87
     */
88
    public static function create(string $file, int $flags = null, string $alias = null): self
89
    {
90
        // Ensure the parent directory of the PHAR file exists as `new \Phar()` does not create it and would fail
91
        // otherwise.
92
        mkdir(dirname($file));
93
94
        return new self(new Phar($file, (int) $flags, $alias), $file);
95
    }
96
97
    /**
98
     * @param Compactor[] $compactors
99
     */
100
    public function registerCompactors(array $compactors): void
101
    {
102
        Assertion::allIsInstanceOf($compactors, Compactor::class);
103
104
        $this->compactors = $compactors;
105
    }
106
107
    /**
108
     * @param scalar[] $placeholders
109
     */
110
    public function registerPlaceholders(array $placeholders): void
111
    {
112
        $message = 'Expected value "%s" to be a scalar or stringable object.';
113
114
        foreach ($placeholders as $i => $placeholder) {
115
            if (is_object($placeholder)) {
116
                Assertion::methodExists('__toString', $placeholder, $message);
117
118
                $placeholders[$i] = (string) $placeholder;
119
120
                break;
121
            }
122
123
            Assertion::scalar($placeholder, $message);
124
        }
125
126
        $this->placeholders = $placeholders;
127
    }
128
129
    public function registerFileMapping(RetrieveRelativeBasePath $retrieveRelativeBasePath, MapFile $fileMapper): void
130
    {
131
        $this->retrieveRelativeBasePath = $retrieveRelativeBasePath;
132
        $this->mapFile = $fileMapper;
133
    }
134
135
    public function registerStub(string $file): void
136
    {
137
        $contents = self::replacePlaceholders(
138
            $this->placeholders,
139
            file_contents($file)
140
        );
141
142
        $this->phar->setStub($contents);
143
    }
144
145
    /**
146
     * @param SplFileInfo[]|string[] $files
147
     * @param bool                   $binary
148
     */
149
    public function addFiles(array $files, bool $binary): void
150
    {
151
        if ($binary) {
152
            foreach ($files as $file) {
153
                $this->addFile((string) $file, null, $binary);
154
            }
155
156
            return;
157
        }
158
159
        $tmp = make_tmp_dir('box', __CLASS__);
160
161
        $filesWithContents = $this->processContents(
162
            array_map(
163
                function ($file): string {
164
                    // Convert files to string as SplFileInfo is not serializable
165
                    return (string) $file;
166
                },
167
                $files
168
            )
169
        );
170
171
        try {
172
            foreach ($filesWithContents as $fileWithContents) {
173
                [$file, $contents] = $fileWithContents;
174
175
                dump_file(
176
                    make_path_absolute($file, $tmp),
177
                    $contents
178
                );
179
            }
180
181
            $cwd = getcwd();
182
            chdir($tmp);
183
184
            foreach ($this->compactors as $compactor) {
185
                if ($compactor instanceof PhpScoper) {
186
                    $phpScoperConfig = $compactor->getConfiguration();
187
188
                    $autoload = (new ScoperAutoloadGenerator($phpScoperConfig->getWhitelist()))->dump('TODOAllowNullPrefix');
189
190
                    dump_file('vendor/scoper-autoload.php', $autoload);
191
                }
192
            }
193
194
            $x = exec('/usr/local/bin/composer dump-autoload --classmap-authoritative');
195
196
            chdir($cwd);
197
198
            $this->phar->buildFromDirectory($tmp);
199
        } finally {
200
            remove($tmp);
201
        }
202
    }
203
204
    /**
205
     * Adds the a file to the PHAR. The contents will first be compacted and have its placeholders
206
     * replaced.
207
     *
208
     * @param string      $file
209
     * @param null|string $contents If null the content of the file will be used
210
     * @param bool        $binary   When true means the file content shouldn't be processed
211
     *
212
     * @return string File local path
213
     */
214
    public function addFile(string $file, string $contents = null, bool $binary = false): string
215
    {
216
        Assertion::file($file);
217
        Assertion::readable($file);
218
219
        $contents = null === $contents ? file_get_contents($file) : $contents;
220
221
        $relativePath = ($this->retrieveRelativeBasePath)($file);
222
        $local = ($this->mapFile)($relativePath);
223
224
        if (null === $local) {
225
            $local = $relativePath;
226
        }
227
228
        if ($binary) {
229
            $this->phar->addFile($file, $local);
230
        } else {
231
            $processedContents = self::compactContents(
232
                $this->compactors,
233
                $local,
234
                self::replacePlaceholders($this->placeholders, $contents)
235
            );
236
237
            $this->phar->addFromString($local, $processedContents);
238
        }
239
240
        return $local;
241
    }
242
243
    public function getPhar(): Phar
244
    {
245
        return $this->phar;
246
    }
247
248
    /**
249
     * Signs the PHAR using a private key file.
250
     *
251
     * @param string $file     the private key file name
252
     * @param string $password the private key password
253
     */
254
    public function signUsingFile(string $file, string $password = null): void
255
    {
256
        $this->sign(file_contents($file), $password);
257
    }
258
259
    /**
260
     * Signs the PHAR using a private key.
261
     *
262
     * @param string $key      The private key
263
     * @param string $password The private key password
264
     */
265
    public function sign(string $key, ?string $password): void
266
    {
267
        $pubKey = $this->file.'.pubkey';
268
269
        Assertion::writeable(dirname($pubKey));
270
        Assertion::extensionLoaded('openssl');
271
272
        if (file_exists($pubKey)) {
273
            Assertion::file(
274
                $pubKey,
275
                'Cannot create public key: "%s" already exists and is not a file.'
276
            );
277
        }
278
279
        $resource = openssl_pkey_get_private($key, (string) $password);
280
281
        openssl_pkey_export($resource, $private);
282
283
        $details = openssl_pkey_get_details($resource);
284
285
        $this->phar->setSignatureAlgorithm(Phar::OPENSSL, $private);
286
287
        dump_file($pubKey, $details['key']);
288
    }
289
290
    /**
291
     * @param string[] $files
292
     *
293
     * @return array array of tuples where the first element is the local file path (path inside the PHAR) and the
294
     *               second element is the processed contents
295
     */
296
    private function processContents(array $files): array
297
    {
298
        $retrieveRelativeBasePath = $this->retrieveRelativeBasePath;
299
        $mapFile = $this->mapFile;
300
        $placeholders = $this->placeholders;
301
        $compactors = $this->compactors;
302
303
        $processFile = function (string $file) use ($retrieveRelativeBasePath, $mapFile, $placeholders, $compactors): array {
304
            $contents = file_contents($file);
305
306
            $relativePath = $retrieveRelativeBasePath($file);
307
            $local = $mapFile($relativePath);
308
309
            if (null === $local) {
310
                $local = $relativePath;
311
            }
312
313
            $processedContents = self::compactContents(
314
                $compactors,
315
                $local,
316
                self::replacePlaceholders($placeholders, $contents)
317
            );
318
319
            return [$local, $processedContents];
320
        };
321
322
        return wait(parallelMap($files, $processFile));
323
    }
324
325
    /**
326
     * Replaces the placeholders with their values.
327
     *
328
     * @param array  $placeholders
329
     * @param string $contents     the contents
330
     *
331
     * @return string the replaced contents
332
     */
333
    private static function replacePlaceholders(array $placeholders, string $contents): string
334
    {
335
        return str_replace(
336
            array_keys($placeholders),
337
            array_values($placeholders),
338
            $contents
339
        );
340
    }
341
342
    private static function compactContents(array $compactors, string $file, string $contents): string
343
    {
344
        return array_reduce(
345
            $compactors,
346
            function (string $contents, Compactor $compactor) use ($file): string {
347
                return $compactor->compact($file, $contents);
348
            },
349
            $contents
350
        );
351
    }
352
}
353