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 FilesystemIterator; |
18
|
|
|
use KevinGH\Box\Compactor\Compactor; |
|
|
|
|
19
|
|
|
use KevinGH\Box\Compactor\CompactorInterface; |
|
|
|
|
20
|
|
|
use KevinGH\Box\Exception\FileException; |
21
|
|
|
use KevinGH\Box\Exception\InvalidArgumentException; |
22
|
|
|
use KevinGH\Box\Exception\OpenSslException; |
23
|
|
|
use KevinGH\Box\Exception\UnexpectedValueException; |
24
|
|
|
use Phar; |
25
|
|
|
use Phine\Path\Path; |
|
|
|
|
26
|
|
|
use RecursiveDirectoryIterator; |
27
|
|
|
use RecursiveIteratorIterator; |
28
|
|
|
use RegexIterator; |
29
|
|
|
use SplFileInfo; |
30
|
|
|
use SplObjectStorage; |
31
|
|
|
use Traversable; |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* Provides additional, complimentary functionality to the Phar class. |
35
|
|
|
* |
36
|
|
|
* @author Kevin Herrera <[email protected]> |
37
|
|
|
*/ |
38
|
|
|
class Box |
39
|
|
|
{ |
40
|
|
|
/** |
41
|
|
|
* The source code compactors. |
42
|
|
|
* |
43
|
|
|
* @var SplObjectStorage |
44
|
|
|
*/ |
45
|
|
|
private $compactors; |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* The path to the Phar file. |
49
|
|
|
* |
50
|
|
|
* @var string |
51
|
|
|
*/ |
52
|
|
|
private $file; |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* The Phar instance. |
56
|
|
|
* |
57
|
|
|
* @var Phar |
58
|
|
|
*/ |
59
|
|
|
private $phar; |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* The placeholder values. |
63
|
|
|
* |
64
|
|
|
* @var array |
65
|
|
|
*/ |
66
|
|
|
private $values = []; |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* Sets the Phar instance. |
70
|
|
|
* |
71
|
|
|
* @param Phar $phar the instance |
72
|
|
|
* @param string $file the path to the Phar file |
73
|
|
|
*/ |
74
|
|
|
public function __construct(Phar $phar, $file) |
75
|
|
|
{ |
76
|
|
|
$this->compactors = new SplObjectStorage(); |
77
|
|
|
$this->file = $file; |
78
|
|
|
$this->phar = $phar; |
79
|
|
|
} |
80
|
|
|
|
81
|
|
|
/** |
82
|
|
|
* Adds a file contents compactor. |
83
|
|
|
* |
84
|
|
|
* @param Compactor $compactor the compactor |
85
|
|
|
*/ |
86
|
|
|
public function addCompactor(Compactor $compactor): void |
87
|
|
|
{ |
88
|
|
|
$this->compactors->attach($compactor); |
89
|
|
|
} |
90
|
|
|
|
91
|
|
|
/** |
92
|
|
|
* Adds a file to the Phar, after compacting it and replacing its |
93
|
|
|
* placeholders. |
94
|
|
|
* |
95
|
|
|
* @param string $file the file name |
96
|
|
|
* @param string $local the local file name |
97
|
|
|
* |
98
|
|
|
* @throws Exception\Exception |
99
|
|
|
* @throws FileException if the file could not be used |
100
|
|
|
*/ |
101
|
|
|
public function addFile($file, $local = null): void |
102
|
|
|
{ |
103
|
|
|
if (null === $local) { |
104
|
|
|
$local = $file; |
105
|
|
|
} |
106
|
|
|
|
107
|
|
|
if (false === is_file($file)) { |
108
|
|
|
throw FileException::create( |
109
|
|
|
'The file "%s" does not exist or is not a file.', |
110
|
|
|
$file |
111
|
|
|
); |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
if (false === ($contents = @file_get_contents($file))) { |
115
|
|
|
throw FileException::lastError(); |
116
|
|
|
} |
117
|
|
|
|
118
|
|
|
$this->addFromString($local, $contents); |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
/** |
122
|
|
|
* Adds the contents from a file to the Phar, after compacting it and |
123
|
|
|
* replacing its placeholders. |
124
|
|
|
* |
125
|
|
|
* @param string $local the local name |
126
|
|
|
* @param string $contents the contents |
127
|
|
|
*/ |
128
|
|
|
public function addFromString($local, $contents): void |
129
|
|
|
{ |
130
|
|
|
$this->phar->addFromString( |
131
|
|
|
$local, |
132
|
|
|
$this->replaceValues($this->compactContents($local, $contents)) |
133
|
|
|
); |
134
|
|
|
} |
135
|
|
|
|
136
|
|
|
/** |
137
|
|
|
* Similar to Phar::buildFromDirectory(), except the files will be |
138
|
|
|
* compacted and their placeholders replaced. |
139
|
|
|
* |
140
|
|
|
* @param string $dir the directory |
141
|
|
|
* @param string $regex the regular expression filter |
142
|
|
|
*/ |
143
|
|
|
public function buildFromDirectory($dir, $regex = null): void |
144
|
|
|
{ |
145
|
|
|
$iterator = new RecursiveIteratorIterator( |
146
|
|
|
new RecursiveDirectoryIterator( |
147
|
|
|
$dir, |
148
|
|
|
FilesystemIterator::KEY_AS_PATHNAME |
149
|
|
|
| FilesystemIterator::CURRENT_AS_FILEINFO |
150
|
|
|
| FilesystemIterator::SKIP_DOTS |
151
|
|
|
) |
152
|
|
|
); |
153
|
|
|
|
154
|
|
|
if ($regex) { |
|
|
|
|
155
|
|
|
$iterator = new RegexIterator($iterator, $regex); |
156
|
|
|
} |
157
|
|
|
|
158
|
|
|
$this->buildFromIterator($iterator, $dir); |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
/** |
162
|
|
|
* Similar to Phar::buildFromIterator(), except the files will be compacted |
163
|
|
|
* and their placeholders replaced. |
164
|
|
|
* |
165
|
|
|
* @param Traversable $iterator the iterator |
166
|
|
|
* @param string $base the base directory path |
167
|
|
|
* |
168
|
|
|
* @throws Exception\Exception |
169
|
|
|
* @throws UnexpectedValueException if the iterator value is unexpected |
170
|
|
|
*/ |
171
|
|
|
public function buildFromIterator(Traversable $iterator, $base = null): void |
172
|
|
|
{ |
173
|
|
|
if ($base) { |
|
|
|
|
174
|
|
|
$base = canonicalize($base.DIRECTORY_SEPARATOR); |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
foreach ($iterator as $key => $value) { |
178
|
|
|
if (is_string($value)) { |
179
|
|
|
if (false === is_string($key)) { |
180
|
|
|
throw UnexpectedValueException::create( |
181
|
|
|
'The key returned by the iterator (%s) is not a string.', |
182
|
|
|
gettype($key) |
183
|
|
|
); |
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
$key = canonicalize($key); |
187
|
|
|
$value = canonicalize($value); |
188
|
|
|
|
189
|
|
|
if (is_dir($value)) { |
190
|
|
|
$this->phar->addEmptyDir($key); |
191
|
|
|
} else { |
192
|
|
|
$this->addFile($value, $key); |
193
|
|
|
} |
194
|
|
|
} elseif ($value instanceof SplFileInfo) { |
195
|
|
|
if (null === $base) { |
196
|
|
|
throw InvalidArgumentException::create( |
197
|
|
|
'The $base argument is required for SplFileInfo values.' |
198
|
|
|
); |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
/** @var $value SplFileInfo */ |
202
|
|
|
$real = $value->getRealPath(); |
203
|
|
|
|
204
|
|
|
if (0 !== strpos($real, $base)) { |
205
|
|
|
throw UnexpectedValueException::create( |
206
|
|
|
'The file "%s" is not in the base directory.', |
207
|
|
|
$real |
208
|
|
|
); |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
$local = str_replace($base, '', $real); |
212
|
|
|
|
213
|
|
|
if ($value->isDir()) { |
214
|
|
|
$this->phar->addEmptyDir($local); |
215
|
|
|
} else { |
216
|
|
|
$this->addFile($real, $local); |
217
|
|
|
} |
218
|
|
|
} else { |
219
|
|
|
throw UnexpectedValueException::create( |
220
|
|
|
'The iterator value "%s" was not expected.', |
221
|
|
|
gettype($value) |
222
|
|
|
); |
223
|
|
|
} |
224
|
|
|
} |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
/** |
228
|
|
|
* Compacts the file contents using the supported compactors. |
229
|
|
|
* |
230
|
|
|
* @param string $file the file name |
231
|
|
|
* @param string $contents the file contents |
232
|
|
|
* |
233
|
|
|
* @return string the compacted contents |
234
|
|
|
*/ |
235
|
|
|
public function compactContents($file, $contents) |
236
|
|
|
{ |
237
|
|
|
foreach ($this->compactors as $compactor) { |
238
|
|
|
/** @var $compactor CompactorInterface */ |
239
|
|
|
if ($compactor->supports($file)) { |
240
|
|
|
$contents = $compactor->compact($contents); |
241
|
|
|
} |
242
|
|
|
} |
243
|
|
|
|
244
|
|
|
return $contents; |
245
|
|
|
} |
246
|
|
|
|
247
|
|
|
/** |
248
|
|
|
* Creates a new Phar and Box instance. |
249
|
|
|
* |
250
|
|
|
* @param string $file the file name |
251
|
|
|
* @param int $flags the RecursiveDirectoryIterator flags |
252
|
|
|
* @param string $alias the Phar alias |
253
|
|
|
* |
254
|
|
|
* @return Box the Box instance |
255
|
|
|
*/ |
256
|
|
|
public static function create($file, $flags = null, $alias = null) |
257
|
|
|
{ |
258
|
|
|
return new self(new Phar($file, (int) $flags, $alias), $file); |
259
|
|
|
} |
260
|
|
|
|
261
|
|
|
/** |
262
|
|
|
* Returns the Phar instance. |
263
|
|
|
* |
264
|
|
|
* @return Phar the instance |
265
|
|
|
*/ |
266
|
|
|
public function getPhar() |
267
|
|
|
{ |
268
|
|
|
return $this->phar; |
269
|
|
|
} |
270
|
|
|
|
271
|
|
|
/** |
272
|
|
|
* Returns the signature of the phar. |
273
|
|
|
* |
274
|
|
|
* This method does not use the extension to extract the phar's signature. |
275
|
|
|
* |
276
|
|
|
* @param string $path the phar file path |
277
|
|
|
* |
278
|
|
|
* @return array the signature |
279
|
|
|
*/ |
280
|
|
|
public static function getSignature($path) |
281
|
|
|
{ |
282
|
|
|
return Signature::create($path)->get(); |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
/** |
286
|
|
|
* Replaces the placeholders with their values. |
287
|
|
|
* |
288
|
|
|
* @param string $contents the contents |
289
|
|
|
* |
290
|
|
|
* @return string the replaced contents |
291
|
|
|
*/ |
292
|
|
|
public function replaceValues($contents) |
293
|
|
|
{ |
294
|
|
|
return str_replace( |
295
|
|
|
array_keys($this->values), |
296
|
|
|
array_values($this->values), |
297
|
|
|
$contents |
298
|
|
|
); |
299
|
|
|
} |
300
|
|
|
|
301
|
|
|
/** |
302
|
|
|
* Sets the bootstrap loader stub using a file. |
303
|
|
|
* |
304
|
|
|
* @param string $file the file path |
305
|
|
|
* @param bool $replace Replace placeholders? |
306
|
|
|
* |
307
|
|
|
* @throws Exception\Exception |
308
|
|
|
* @throws FileException if the stub file could not be used |
309
|
|
|
*/ |
310
|
|
View Code Duplication |
public function setStubUsingFile($file, $replace = false): void |
|
|
|
|
311
|
|
|
{ |
312
|
|
|
if (false === is_file($file)) { |
313
|
|
|
throw FileException::create( |
314
|
|
|
'The file "%s" does not exist or is not a file.', |
315
|
|
|
$file |
316
|
|
|
); |
317
|
|
|
} |
318
|
|
|
|
319
|
|
|
if (false === ($contents = @file_get_contents($file))) { |
320
|
|
|
throw FileException::lastError(); |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
if ($replace) { |
324
|
|
|
$contents = $this->replaceValues($contents); |
325
|
|
|
} |
326
|
|
|
|
327
|
|
|
$this->phar->setStub($contents); |
328
|
|
|
} |
329
|
|
|
|
330
|
|
|
/** |
331
|
|
|
* Sets the placeholder values. |
332
|
|
|
* |
333
|
|
|
* @param array $values the values |
334
|
|
|
* |
335
|
|
|
* @throws Exception\Exception |
336
|
|
|
* @throws InvalidArgumentException if a non-scalar value is used |
337
|
|
|
*/ |
338
|
|
|
public function setValues(array $values): void |
339
|
|
|
{ |
340
|
|
|
foreach ($values as $value) { |
341
|
|
|
if (false === is_scalar($value)) { |
342
|
|
|
throw InvalidArgumentException::create( |
343
|
|
|
'Non-scalar values (such as %s) are not supported.', |
344
|
|
|
gettype($value) |
345
|
|
|
); |
346
|
|
|
} |
347
|
|
|
} |
348
|
|
|
|
349
|
|
|
$this->values = $values; |
350
|
|
|
} |
351
|
|
|
|
352
|
|
|
/** |
353
|
|
|
* Signs the Phar using a private key. |
354
|
|
|
* |
355
|
|
|
* @param string $key the private key |
356
|
|
|
* @param string $password the private key password |
357
|
|
|
* |
358
|
|
|
* @throws Exception\Exception |
359
|
|
|
* @throws OpenSslException if the "openssl" extension could not be used |
360
|
|
|
* or has generated an error |
361
|
|
|
*/ |
362
|
|
|
public function sign($key, $password = null): void |
363
|
|
|
{ |
364
|
|
|
OpenSslException::reset(); |
365
|
|
|
|
366
|
|
|
if (false === extension_loaded('openssl')) { |
367
|
|
|
// @codeCoverageIgnoreStart |
368
|
|
|
throw OpenSslException::create( |
369
|
|
|
'The "openssl" extension is not available.' |
370
|
|
|
); |
371
|
|
|
// @codeCoverageIgnoreEnd |
372
|
|
|
} |
373
|
|
|
|
374
|
|
|
if (false === ($resource = openssl_pkey_get_private($key, $password))) { |
375
|
|
|
// @codeCoverageIgnoreStart |
376
|
|
|
throw OpenSslException::lastError(); |
377
|
|
|
// @codeCoverageIgnoreEnd |
378
|
|
|
} |
379
|
|
|
|
380
|
|
|
if (false === openssl_pkey_export($resource, $private)) { |
381
|
|
|
// @codeCoverageIgnoreStart |
382
|
|
|
throw OpenSslException::lastError(); |
383
|
|
|
// @codeCoverageIgnoreEnd |
384
|
|
|
} |
385
|
|
|
|
386
|
|
|
if (false === ($details = openssl_pkey_get_details($resource))) { |
387
|
|
|
// @codeCoverageIgnoreStart |
388
|
|
|
throw OpenSslException::lastError(); |
389
|
|
|
// @codeCoverageIgnoreEnd |
390
|
|
|
} |
391
|
|
|
|
392
|
|
|
$this->phar->setSignatureAlgorithm(Phar::OPENSSL, $private); |
393
|
|
|
|
394
|
|
|
if (false === @file_put_contents($this->file.'.pubkey', $details['key'])) { |
395
|
|
|
throw FileException::lastError(); |
396
|
|
|
} |
397
|
|
|
} |
398
|
|
|
|
399
|
|
|
/** |
400
|
|
|
* Signs the Phar using a private key file. |
401
|
|
|
* |
402
|
|
|
* @param string $file the private key file name |
403
|
|
|
* @param string $password the private key password |
404
|
|
|
* |
405
|
|
|
* @throws Exception\Exception |
406
|
|
|
* @throws FileException if the private key file could not be read |
407
|
|
|
*/ |
408
|
|
View Code Duplication |
public function signUsingFile($file, $password = null): void |
|
|
|
|
409
|
|
|
{ |
410
|
|
|
if (false === is_file($file)) { |
411
|
|
|
throw FileException::create( |
412
|
|
|
'The file "%s" does not exist or is not a file.', |
413
|
|
|
$file |
414
|
|
|
); |
415
|
|
|
} |
416
|
|
|
|
417
|
|
|
if (false === ($key = @file_get_contents($file))) { |
418
|
|
|
throw FileException::lastError(); |
419
|
|
|
} |
420
|
|
|
|
421
|
|
|
$this->sign($key, $password); |
422
|
|
|
} |
423
|
|
|
} |
424
|
|
|
|
Let?s assume that you have a directory layout like this:
and let?s assume the following content of
Bar.php
:If both files
OtherDir/Foo.php
andSomeDir/Foo.php
are loaded in the same runtime, you will see a PHP error such as the following:PHP Fatal error: Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php
However, as
OtherDir/Foo.php
does not necessarily have to be loaded and the error is only triggered if it is loaded beforeOtherDir/Bar.php
, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias: