1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* This file is part of the Passbook package. |
5
|
|
|
* |
6
|
|
|
* (c) Eymen Gunay <[email protected]> |
7
|
|
|
* |
8
|
|
|
* For the full copyright and license information, please view the LICENSE |
9
|
|
|
* file that was distributed with this source code. |
10
|
|
|
*/ |
11
|
|
|
|
12
|
|
|
namespace Passbook; |
13
|
|
|
|
14
|
|
|
use Exception; |
15
|
|
|
use FilesystemIterator; |
16
|
|
|
use Passbook\Certificate\P12; |
17
|
|
|
use Passbook\Certificate\WWDR; |
18
|
|
|
use Passbook\Exception\FileException; |
19
|
|
|
use Passbook\Pass\Image; |
20
|
|
|
use RecursiveDirectoryIterator; |
21
|
|
|
use RecursiveIteratorIterator; |
22
|
|
|
use SplFileObject; |
23
|
|
|
use ZipArchive; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* PassFactory - Creates .pkpass files |
27
|
|
|
* |
28
|
|
|
* @author Eymen Gunay <[email protected]> |
29
|
|
|
*/ |
30
|
|
|
class PassFactory |
31
|
|
|
{ |
32
|
|
|
/** |
33
|
|
|
* Output path for generated pass files |
34
|
|
|
* |
35
|
|
|
* @var string |
36
|
|
|
*/ |
37
|
|
|
protected $outputPath = ''; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* Overwrite if pass exists |
41
|
|
|
* |
42
|
|
|
* @var bool |
43
|
|
|
*/ |
44
|
|
|
protected $overwrite = false; |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* Pass type identifier |
48
|
|
|
* |
49
|
|
|
* @var string |
50
|
|
|
*/ |
51
|
|
|
protected $passTypeIdentifier; |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* Team identifier |
55
|
|
|
* |
56
|
|
|
* @var string |
57
|
|
|
*/ |
58
|
|
|
protected $teamIdentifier; |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* Organization name |
62
|
|
|
* |
63
|
|
|
* @var string |
64
|
|
|
*/ |
65
|
|
|
protected $organizationName; |
66
|
|
|
|
67
|
|
|
/** |
68
|
|
|
* P12 file |
69
|
|
|
* |
70
|
|
|
* @var \Passbook\Certificate\P12Interface |
71
|
|
|
*/ |
72
|
|
|
protected $p12; |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* WWDR file |
76
|
|
|
* |
77
|
|
|
* @var \Passbook\Certificate\WWDRInterface |
78
|
|
|
*/ |
79
|
|
|
protected $wwdr; |
80
|
|
|
|
81
|
|
|
/** |
82
|
|
|
* @var bool - skip signing the pass; should only be used for testing |
83
|
|
|
*/ |
84
|
|
|
protected $skipSignature; |
85
|
|
|
|
86
|
|
|
/** |
87
|
|
|
* Pass file extension |
88
|
|
|
* |
89
|
|
|
* @var string |
90
|
|
|
*/ |
91
|
|
|
const PASS_EXTENSION = '.pkpass'; |
92
|
|
|
|
93
|
|
|
public function __construct($passTypeIdentifier, $teamIdentifier, $organizationName, $p12File, $p12Pass, $wwdrFile) |
94
|
|
|
{ |
95
|
|
|
// Required pass information |
96
|
|
|
$this->passTypeIdentifier = $passTypeIdentifier; |
97
|
|
|
$this->teamIdentifier = $teamIdentifier; |
98
|
|
|
$this->organizationName = $organizationName; |
99
|
|
|
// Create certificate objects |
100
|
|
|
$this->p12 = new P12($p12File, $p12Pass); |
101
|
|
|
$this->wwdr = new WWDR($wwdrFile); |
102
|
|
|
} |
103
|
|
|
|
104
|
|
|
/** |
105
|
|
|
* Set outputPath |
106
|
|
|
* |
107
|
|
|
* @param string |
108
|
|
|
* |
109
|
|
|
* @return $this |
110
|
|
|
*/ |
111
|
|
|
public function setOutputPath($outputPath) |
112
|
|
|
{ |
113
|
|
|
$this->outputPath = $outputPath; |
114
|
|
|
|
115
|
|
|
return $this; |
116
|
|
|
} |
117
|
|
|
|
118
|
|
|
/** |
119
|
|
|
* Get outputPath |
120
|
|
|
* |
121
|
|
|
* @return string |
122
|
|
|
*/ |
123
|
|
|
public function getOutputPath() |
124
|
|
|
{ |
125
|
|
|
return $this->outputPath; |
126
|
|
|
} |
127
|
|
|
|
128
|
|
|
/** |
129
|
|
|
* Set overwrite |
130
|
|
|
* |
131
|
|
|
* @param boolean |
132
|
|
|
* |
133
|
|
|
* @return $this |
134
|
|
|
*/ |
135
|
|
|
public function setOverwrite($overwrite) |
136
|
|
|
{ |
137
|
|
|
$this->overwrite = $overwrite; |
138
|
|
|
|
139
|
|
|
return $this; |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
/** |
143
|
|
|
* Get overwrite |
144
|
|
|
* |
145
|
|
|
* @return boolean |
146
|
|
|
*/ |
147
|
|
|
public function isOverwrite() |
148
|
|
|
{ |
149
|
|
|
return $this->overwrite; |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
/** |
153
|
|
|
* Set skip signature |
154
|
|
|
* |
155
|
|
|
* When set, the pass will not be signed when packaged. This should only |
156
|
|
|
* be used for testing. |
157
|
|
|
* |
158
|
|
|
* @param boolean |
159
|
|
|
* @return $this |
160
|
|
|
*/ |
161
|
|
|
public function setSkipSignature($skipSignature) |
162
|
|
|
{ |
163
|
|
|
$this->skipSignature = $skipSignature; |
164
|
|
|
|
165
|
|
|
return $this; |
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
/** |
169
|
|
|
* Get overwrite |
170
|
|
|
* @return boolean |
171
|
|
|
*/ |
172
|
|
|
public function getSkipSignature() |
|
|
|
|
173
|
|
|
{ |
174
|
|
|
return $this->skipSignature; |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
/** |
178
|
|
|
* Serialize pass |
179
|
|
|
* |
180
|
|
|
* @param PassInterface $pass |
181
|
|
|
* |
182
|
|
|
* @return string |
183
|
|
|
*/ |
184
|
|
|
public static function serialize(PassInterface $pass) |
185
|
|
|
{ |
186
|
|
|
return self::jsonEncode($pass->toArray()); |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
/** |
190
|
|
|
* Creates a pkpass file |
191
|
|
|
* |
192
|
|
|
* @param PassInterface $pass - the pass to be packaged into a .pkpass file |
193
|
|
|
* @param string $passName - filename to be used for the pass; if blank the serial number will be used |
194
|
|
|
* |
195
|
|
|
* @return SplFileObject If an IO error occurred |
196
|
|
|
* @throws Exception |
197
|
|
|
*/ |
198
|
|
|
public function package(PassInterface $pass, $passName = '') |
199
|
|
|
{ |
200
|
|
|
if ($pass->getSerialNumber() == '') { |
201
|
|
|
throw new \InvalidArgumentException('Pass must have a serial number to be packaged'); |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
$this->populateRequiredInformation($pass); |
205
|
|
|
|
206
|
|
|
// Serialize pass |
207
|
|
|
$json = self::serialize($pass); |
208
|
|
|
|
209
|
|
|
$outputPath = rtrim($this->getOutputPath(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; |
210
|
|
|
$passDir = $outputPath . $this->getPassName($passName, $pass) . DIRECTORY_SEPARATOR; |
211
|
|
|
$passDirExists = file_exists($passDir); |
212
|
|
|
if ($passDirExists && !$this->isOverwrite()) { |
213
|
|
|
throw new FileException("Temporary pass directory already exists"); |
214
|
|
|
} elseif (!$passDirExists && !mkdir($passDir, 0777, true)) { |
215
|
|
|
throw new FileException("Couldn't create temporary pass directory"); |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
// Pass.json |
219
|
|
|
$passJSONFile = $passDir . 'pass.json'; |
220
|
|
|
file_put_contents($passJSONFile, $json); |
221
|
|
|
|
222
|
|
|
// Images |
223
|
|
|
/** @var Image $image */ |
224
|
|
|
foreach ($pass->getImages() as $image) { |
225
|
|
|
$fileName = $passDir . $image->getContext(); |
226
|
|
|
if ($image->isRetina()) { |
227
|
|
|
$fileName .= '@2x'; |
228
|
|
|
} |
229
|
|
|
$fileName .= '.' . $image->getExtension(); |
230
|
|
|
copy($image->getPathname(), $fileName); |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
// Localizations |
234
|
|
|
foreach ($pass->getLocalizations() as $localization) { |
235
|
|
|
// Create dir (LANGUAGE.lproj) |
236
|
|
|
$localizationDir = $passDir . $localization->getLanguage() . '.lproj' . DIRECTORY_SEPARATOR; |
237
|
|
|
mkdir($localizationDir, 0777, true); |
238
|
|
|
|
239
|
|
|
// pass.strings File (Format: "token" = "value") |
240
|
|
|
$localizationStringsFile = $localizationDir . 'pass.strings'; |
241
|
|
|
file_put_contents($localizationStringsFile, $localization->getStringsFileOutput()); |
242
|
|
|
|
243
|
|
|
// Localization images |
244
|
|
|
foreach ($localization->getImages() as $image) { |
245
|
|
|
$fileName = $localizationDir . $image->getContext(); |
246
|
|
|
if ($image->isRetina()) { |
247
|
|
|
$fileName .= '@2x'; |
248
|
|
|
} |
249
|
|
|
$fileName .= '.' . $image->getExtension(); |
250
|
|
|
copy($image->getPathname(), $fileName); |
251
|
|
|
} |
252
|
|
|
} |
253
|
|
|
|
254
|
|
|
// Manifest.json - recursive, also add files in sub directories |
255
|
|
|
$manifestJSONFile = $passDir . 'manifest.json'; |
256
|
|
|
$manifest = array(); |
257
|
|
|
$files = new RecursiveIteratorIterator( |
258
|
|
|
new RecursiveDirectoryIterator($passDir), |
259
|
|
|
RecursiveIteratorIterator::SELF_FIRST |
260
|
|
|
); |
261
|
|
|
foreach ($files as $file) { |
262
|
|
|
// Ignore "." and ".." folders |
263
|
|
|
if (in_array(substr($file, strrpos($file, '/') + 1), array('.', '..'))) { |
264
|
|
|
continue; |
265
|
|
|
} |
266
|
|
|
// |
267
|
|
|
$filePath = realpath($file); |
268
|
|
|
if (is_file($filePath) === true) { |
269
|
|
|
$relativePathName = str_replace($passDir, '', $file->getPathname()); |
270
|
|
|
$manifest[$relativePathName] = sha1_file($filePath); |
271
|
|
|
} |
272
|
|
|
} |
273
|
|
|
file_put_contents($manifestJSONFile, $this->jsonEncode($manifest)); |
274
|
|
|
|
275
|
|
|
// Signature |
276
|
|
|
$this->sign($passDir, $manifestJSONFile); |
277
|
|
|
|
278
|
|
|
// Zip pass |
279
|
|
|
$zipFile = $outputPath . $this->getPassName($passName, $pass) . self::PASS_EXTENSION; |
280
|
|
|
$this->zip($passDir, $zipFile); |
281
|
|
|
|
282
|
|
|
// Remove temporary pass directory |
283
|
|
|
$this->rrmdir($passDir); |
284
|
|
|
|
285
|
|
|
return new SplFileObject($zipFile); |
286
|
|
|
} |
287
|
|
|
|
288
|
|
|
/** |
289
|
|
|
* @param $passDir |
290
|
|
|
* @param $manifestJSONFile |
291
|
|
|
*/ |
292
|
|
|
private function sign($passDir, $manifestJSONFile) |
293
|
|
|
{ |
294
|
|
|
if ($this->getSkipSignature()) { |
295
|
|
|
return; |
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
$signatureFile = $passDir . 'signature'; |
299
|
|
|
$p12 = file_get_contents($this->p12->getRealPath()); |
300
|
|
|
$certs = array(); |
301
|
|
|
if (openssl_pkcs12_read($p12, $certs, $this->p12->getPassword()) == true) { |
|
|
|
|
302
|
|
|
$certdata = openssl_x509_read($certs['cert']); |
303
|
|
|
$privkey = openssl_pkey_get_private($certs['pkey'], $this->p12->getPassword()); |
304
|
|
|
openssl_pkcs7_sign( |
305
|
|
|
$manifestJSONFile, |
306
|
|
|
$signatureFile, |
307
|
|
|
$certdata, |
308
|
|
|
$privkey, |
309
|
|
|
array(), |
310
|
|
|
PKCS7_BINARY | PKCS7_DETACHED, |
311
|
|
|
$this->wwdr->getRealPath() |
312
|
|
|
); |
313
|
|
|
// Get signature content |
314
|
|
|
$signature = @file_get_contents($signatureFile); |
315
|
|
|
// Check signature content |
316
|
|
|
if (!$signature) { |
317
|
|
|
throw new FileException("Couldn't read signature file."); |
318
|
|
|
} |
319
|
|
|
// Delimiters |
320
|
|
|
$begin = 'filename="smime.p7s"'; |
321
|
|
|
$end = '------'; |
322
|
|
|
// Convert signature |
323
|
|
|
$signature = substr($signature, strpos($signature, $begin) + strlen($begin)); |
324
|
|
|
$signature = substr($signature, 0, strpos($signature, $end)); |
325
|
|
|
$signature = base64_decode($signature); |
326
|
|
|
// Put new signature |
327
|
|
|
if (!file_put_contents($signatureFile, $signature)) { |
328
|
|
|
throw new FileException("Couldn't write signature file."); |
329
|
|
|
} |
330
|
|
|
} else { |
331
|
|
|
throw new FileException("Error reading certificate file"); |
332
|
|
|
} |
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
/** |
336
|
|
|
* Creates a zip of a directory including all sub directories (recursive) |
337
|
|
|
* |
338
|
|
|
* @param $source - path to the source directory |
339
|
|
|
* @param $destination - output directory |
340
|
|
|
* |
341
|
|
|
* @return bool |
342
|
|
|
* @throws Exception |
343
|
|
|
*/ |
344
|
|
|
private function zip($source, $destination) |
|
|
|
|
345
|
|
|
{ |
346
|
|
|
if (!extension_loaded('zip')) { |
347
|
|
|
throw new Exception("ZIP extension not available"); |
348
|
|
|
} |
349
|
|
|
|
350
|
|
|
$source = realpath($source); |
|
|
|
|
351
|
|
|
if (!is_dir($source)) { |
352
|
|
|
throw new FileException("Source must be a directory."); |
353
|
|
|
} |
354
|
|
|
|
355
|
|
|
$zip = new ZipArchive(); |
356
|
|
|
$shouldOverwrite = $this->isOverwrite() ? ZipArchive::OVERWRITE : 0; |
357
|
|
|
if (!$zip->open($destination, ZipArchive::CREATE | $shouldOverwrite)) { |
358
|
|
|
throw new FileException("Couldn't open zip file."); |
359
|
|
|
} |
360
|
|
|
|
361
|
|
|
/* @var $iterator RecursiveIteratorIterator|RecursiveDirectoryIterator */ |
362
|
|
|
$dirIterator = new RecursiveDirectoryIterator($source, FilesystemIterator::SKIP_DOTS); |
363
|
|
|
$iterator = new RecursiveIteratorIterator($dirIterator, RecursiveIteratorIterator::SELF_FIRST); |
364
|
|
|
while ($iterator->valid()) { |
365
|
|
|
if ($iterator->isDir()) { |
366
|
|
|
$zip->addEmptyDir($iterator->getSubPathName()); |
367
|
|
|
} else if ($iterator->isFile()) { |
368
|
|
|
$zip->addFromString($iterator->getSubPathName(), file_get_contents($iterator->key())); |
369
|
|
|
} |
370
|
|
|
$iterator->next(); |
371
|
|
|
} |
372
|
|
|
|
373
|
|
|
return $zip->close(); |
374
|
|
|
} |
375
|
|
|
|
376
|
|
|
/** |
377
|
|
|
* Recursive folder remove |
378
|
|
|
* |
379
|
|
|
* @param string $dir |
380
|
|
|
* |
381
|
|
|
* @return bool |
382
|
|
|
*/ |
383
|
|
|
private function rrmdir($dir) |
|
|
|
|
384
|
|
|
{ |
385
|
|
|
$files = array_diff(scandir($dir), array('.', '..')); |
386
|
|
|
foreach ($files as $file) { |
387
|
|
|
is_dir("$dir/$file") ? $this->rrmdir("$dir/$file") : unlink("$dir/$file"); |
388
|
|
|
} |
389
|
|
|
|
390
|
|
|
return rmdir($dir); |
391
|
|
|
} |
392
|
|
|
|
393
|
|
|
/** |
394
|
|
|
* @param PassInterface $pass |
395
|
|
|
*/ |
396
|
|
|
private function populateRequiredInformation(PassInterface $pass) |
397
|
|
|
{ |
398
|
|
|
if (!$pass->getPassTypeIdentifier()) { |
399
|
|
|
$pass->setPassTypeIdentifier($this->passTypeIdentifier); |
400
|
|
|
} |
401
|
|
|
|
402
|
|
|
if (!$pass->getTeamIdentifier()) { |
403
|
|
|
$pass->setTeamIdentifier($this->teamIdentifier); |
404
|
|
|
} |
405
|
|
|
|
406
|
|
|
if (!$pass->getOrganizationName()) { |
407
|
|
|
$pass->setOrganizationName($this->organizationName); |
408
|
|
|
} |
409
|
|
|
} |
410
|
|
|
|
411
|
|
|
/** |
412
|
|
|
* @param $array |
413
|
|
|
* |
414
|
|
|
* @return string |
415
|
|
|
*/ |
416
|
|
|
private static function jsonEncode($array) |
417
|
|
|
{ |
418
|
|
|
// Check if JSON_UNESCAPED_SLASHES is defined to support PHP 5.3. |
419
|
|
|
$options = defined('JSON_UNESCAPED_SLASHES') ? JSON_UNESCAPED_SLASHES : 0; |
420
|
|
|
return json_encode($array, $options); |
421
|
|
|
} |
422
|
|
|
|
423
|
|
|
/** |
424
|
|
|
* @param $passName |
425
|
|
|
* @param PassInterface $pass |
426
|
|
|
* |
427
|
|
|
* @return string |
428
|
|
|
*/ |
429
|
|
|
public function getPassName($passName, PassInterface $pass) |
430
|
|
|
{ |
431
|
|
|
$passNameSanitised = preg_replace("/[^a-zA-Z0-9]+/", "", $passName); |
432
|
|
|
return strlen($passNameSanitised) != 0 ? $passNameSanitised : $pass->getSerialNumber(); |
433
|
|
|
} |
434
|
|
|
|
435
|
|
|
} |
436
|
|
|
|
This check examines a number of code elements and verifies that they conform to the given naming conventions.
You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.