Test Failed
Push — google-wallet-support ( de9354 )
by Razvan
15:21
created

ApplePassFactory::getSkipSignature()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
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 InvalidArgumentException;
17
use Passbook\Certificate\P12;
18
use Passbook\Certificate\WWDR;
19
use Passbook\Exception\FileException;
20
use Passbook\Exception\PassInvalidException;
21
use Passbook\Pass\Image;
22
use RecursiveDirectoryIterator;
23
use RecursiveIteratorIterator;
24
use SplFileObject;
25
use ZipArchive;
26
27
/**
28
 * ApplePassFactory - Creates (signed) .pkpass files
29
 *
30
 * @author Eymen Gunay <[email protected]>
31
 */
32
class ApplePassFactory
33
{
34
    /**
35
     * Output path for generated pass files
36
     *
37
     * @var string
38
     */
39
    protected $outputPath = '';
40
41
    /**
42
     * Overwrite if pass exists
43
     *
44
     * @var bool
45
     */
46
    protected $overwrite = false;
47
48
    /**
49
     * Pass type identifier
50
     *
51
     * @var string
52
     */
53
    protected $passTypeIdentifier;
54
55
    /**
56
     * Team identifier
57
     *
58
     * @var string
59
     */
60
    protected $teamIdentifier;
61
62
    /**
63
     * Organization name
64
     *
65
     * @var string
66
     */
67
    protected $organizationName;
68
69
    /**
70
     * P12 file
71
     *
72
     * @var \Passbook\Certificate\P12Interface
73
     */
74
    protected $p12;
75
76
    /**
77
     * WWDR file
78
     *
79
     * @var \Passbook\Certificate\WWDRInterface
80
     */
81
    protected $wwdr;
82
83
    /**
84
     * @var bool - skip signing the pass; should only be used for testing
85
     */
86
    protected $skipSignature;
87
88
    /**
89
     * @var PassValidatorInterface
90
     */
91
    private $passValidator;
92
93
    /**
94
     * Pass file extension
95
     *
96
     * @var string
97
     */
98
    public const PASS_EXTENSION = '.pkpass';
99
100
    public function __construct($passTypeIdentifier, $teamIdentifier, $organizationName, $p12File, $p12Pass, $wwdrFile)
101
    {
102
        // Required pass information
103
        $this->passTypeIdentifier = $passTypeIdentifier;
104
        $this->teamIdentifier = $teamIdentifier;
105
        $this->organizationName = $organizationName;
106
107
        // Create certificate objects
108
        $this->p12 = new P12($p12File, $p12Pass);
109
        $this->wwdr = new WWDR($wwdrFile);
110
111
        // By default use the PassValidator
112
        $this->passValidator = new PassValidator();
113
    }
114
115
    /**
116
     * Set outputPath
117
     *
118
     * @param string $outputPath
119
     *
120
     * @return $this
121
     */
122
    public function setOutputPath($outputPath)
123
    {
124
        $this->outputPath = $outputPath;
125
126
        return $this;
127
    }
128
129
    /**
130
     * Get outputPath
131
     *
132
     * @return string
133
     */
134
    public function getOutputPath()
135
    {
136
        return $this->outputPath;
137
    }
138
139
    /**
140
     * The output path with a directory separator on the end.
141
     *
142
     * @return string
143
     */
144
    public function getNormalizedOutputPath()
145
    {
146
        return rtrim($this->outputPath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
147
    }
148
149
    /**
150
     * Set overwrite
151
     *
152
     * @param boolean $overwrite
153
     *
154
     * @return $this
155
     */
156
    public function setOverwrite($overwrite)
157
    {
158
        $this->overwrite = $overwrite;
159
160
        return $this;
161
    }
162
163
    /**
164
     * Get overwrite
165
     *
166
     * @return boolean
167
     */
168
    public function isOverwrite()
169
    {
170
        return $this->overwrite;
171
    }
172
173
    /**
174
     * Set skip signature
175
     *
176
     * When set, the pass will not be signed when packaged. This should only
177
     * be used for testing.
178
     *
179
     * @param boolean $skipSignature
180
     *
181
     * @return $this
182
     */
183
    public function setSkipSignature($skipSignature)
184
    {
185
        $this->skipSignature = $skipSignature;
186
187
        return $this;
188
    }
189
190
    /**
191
     * Get skip signature
192
     *
193
     * @return boolean
194
     */
195
    public function getSkipSignature()
196
    {
197
        return $this->skipSignature;
198
    }
199
200
    /**
201
     * Set an implementation of PassValidatorInterface to validate the pass
202
     * before packaging. When set to null, no validation is performed when
203
     * packaging the pass.
204
     *
205
     * @param PassValidatorInterface|null $passValidator
206
     *
207
     * @return $this
208
     */
209
    public function setPassValidator(PassValidatorInterface $passValidator = null)
210
    {
211
        $this->passValidator = $passValidator;
212
213
        return $this;
214
    }
215
216
    /**
217
     * @return PassValidatorInterface
218
     */
219
    public function getPassValidator()
220
    {
221
        return $this->passValidator;
222
    }
223
224
    /**
225
     * Serialize pass
226
     *
227
     * @param  PassInterface $pass
228
     *
229
     * @return string
230
     */
231
    public static function serialize(PassInterface $pass)
232
    {
233
        return self::jsonEncode($pass->toArray());
234
    }
235
236
    /**
237
     * Creates a pkpass file
238
     *
239
     * @param  PassInterface $pass - the pass to be packaged into a .pkpass file
240
     * @param string $passName - filename to be used for the pass; if blank the serial number will be used
241
     *
242
     * @return SplFileObject If an IO error occurred
243
     * @throws InvalidArgumentException|PassInvalidException|Exception
244
     */
245
    public function package(PassInterface $pass, $passName = '')
246
    {
247
        if ($pass->getSerialNumber() == '') {
248
            throw new InvalidArgumentException('Pass must have a serial number to be packaged');
249
        }
250
251
        $this->populateRequiredInformation($pass);
252
253
        if ($this->passValidator) {
254
            if (!$this->passValidator->validate($pass)) {
255
                throw new PassInvalidException('Failed to validate passbook', $this->passValidator->getErrors());
256
            };
257
        }
258
259
        $passDir = $this->preparePassDirectory($pass);
260
261
        // Serialize pass
262
        file_put_contents($passDir . 'pass.json', self::serialize($pass));
263
264
        // Images
265
        $this->prepareImages($pass, $passDir);
266
267
        // Localizations
268
        $this->prepareLocalizations($pass, $passDir);
269
270
        // Manifest.json - recursive, also add files in sub directories
271
        $manifestJSONFile = $this->prepareManifest($passDir);
272
273
        // Signature
274
        $this->sign($passDir, $manifestJSONFile);
275
276
        // Zip pass
277
        $zipFile = $this->getNormalizedOutputPath() . $this->getPassName($passName, $pass) . self::PASS_EXTENSION;
278
        $this->zip($passDir, $zipFile);
279
280
        // Remove temporary pass directory
281
        $this->rrmdir($passDir);
282
283
        return new SplFileObject($zipFile);
284
    }
285
286
    /**
287
     * @param $passDir
288
     * @param $manifestJSONFile
289
     */
290
    private function sign($passDir, $manifestJSONFile): void
291
    {
292
        if ($this->getSkipSignature()) {
293
            return;
294
        }
295
296
        $signatureFile = $passDir . 'signature';
297
        $p12 = file_get_contents($this->p12->getRealPath());
298
        $certs = [];
299
        if (openssl_pkcs12_read($p12, $certs, $this->p12->getPassword()) === true) {
300
            $certdata = openssl_x509_read($certs['cert']);
301
            $privkey = openssl_pkey_get_private($certs['pkey'], $this->p12->getPassword());
302
            openssl_pkcs7_sign(
303
                $manifestJSONFile,
304
                $signatureFile,
305
                $certdata,
306
                $privkey,
307
                [],
308
                PKCS7_BINARY | PKCS7_DETACHED,
309
                $this->wwdr->getRealPath()
310
            );
311
            // Get signature content
312
            $signature = file_get_contents($signatureFile);
313
            // Check signature content
314
            if (!$signature) {
315
                throw new FileException("Couldn't read signature file.");
316
            }
317
            // Delimiters
318
            $begin = 'filename="smime.p7s"';
319
            $end = '------';
320
            // Convert signature
321
            $signature = substr($signature, strpos($signature, $begin) + strlen($begin));
322
            $signature = substr($signature, 0, strpos($signature, $end));
323
            $signature = base64_decode($signature);
324
            // Put new signature
325
            if (!file_put_contents($signatureFile, $signature)) {
326
                throw new FileException("Couldn't write signature file.");
327
            }
328
        } else {
329
            throw new FileException('Error reading certificate file');
330
        }
331
    }
332
333
    /**
334
     * Creates a zip of a directory including all sub directories (recursive)
335
     *
336
     * @param $source - path to the source directory
0 ignored issues
show
Documentation Bug introduced by
The doc comment - at position 0 could not be parsed: Unknown type name '-' at position 0 in -.
Loading history...
337
     * @param $destination - output directory
338
     *
339
     * @return bool
340
     * @throws Exception
341
     */
342
    private function zip($source, $destination)
343
    {
344
        if (!extension_loaded('zip')) {
345
            throw new Exception('ZIP extension not available');
346
        }
347
348
        $source = realpath($source);
349
        if (!is_dir($source)) {
350
            throw new FileException('Source must be a directory.');
351
        }
352
353
        $zip = new ZipArchive();
354
        $shouldOverwrite = $this->isOverwrite() ? ZipArchive::OVERWRITE : 0;
355
        if (!$zip->open($destination, ZipArchive::CREATE | $shouldOverwrite)) {
356
            throw new FileException("Couldn't open zip file.");
357
        }
358
359
        /* @var $iterator RecursiveIteratorIterator|RecursiveDirectoryIterator */
360
        $dirIterator = new RecursiveDirectoryIterator($source, FilesystemIterator::SKIP_DOTS);
361
        $iterator = new RecursiveIteratorIterator($dirIterator, RecursiveIteratorIterator::SELF_FIRST);
362
        while ($iterator->valid()) {
363
            if ($iterator->isDir()) {
0 ignored issues
show
Bug introduced by
The method isDir() does not exist on RecursiveIteratorIterator. ( Ignorable by Annotation )

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

363
            if ($iterator->/** @scrutinizer ignore-call */ isDir()) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
364
                $zip->addEmptyDir($iterator->getSubPathName());
0 ignored issues
show
Bug introduced by
The method getSubPathName() does not exist on RecursiveIteratorIterator. ( Ignorable by Annotation )

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

364
                $zip->addEmptyDir($iterator->/** @scrutinizer ignore-call */ getSubPathName());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
365
            } elseif ($iterator->isFile()) {
0 ignored issues
show
Bug introduced by
The method isFile() does not exist on RecursiveIteratorIterator. ( Ignorable by Annotation )

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

365
            } elseif ($iterator->/** @scrutinizer ignore-call */ isFile()) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
366
                $zip->addFromString($iterator->getSubPathName(), file_get_contents($iterator->key()));
0 ignored issues
show
Bug introduced by
It seems like $iterator->key() can also be of type null and true; however, parameter $filename of file_get_contents() does only seem to accept string, 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

366
                $zip->addFromString($iterator->getSubPathName(), file_get_contents(/** @scrutinizer ignore-type */ $iterator->key()));
Loading history...
367
            }
368
            $iterator->next();
369
        }
370
371
        return $zip->close();
372
    }
373
374
    /**
375
     * Recursive folder remove
376
     *
377
     * @param string $dir
378
     *
379
     * @return bool
380
     */
381
    private function rrmdir($dir)
382
    {
383
        $files = array_diff(scandir($dir), ['.', '..']);
384
        foreach ($files as $file) {
385
            is_dir("$dir/$file") ? $this->rrmdir("$dir/$file") : unlink("$dir/$file");
386
        }
387
388
        return rmdir($dir);
389
    }
390
391
    /**
392
     * @param PassInterface $pass
393
     */
394
    private function populateRequiredInformation(PassInterface $pass): void
395
    {
396
        if (!$pass->getPassTypeIdentifier()) {
397
            $pass->setPassTypeIdentifier($this->passTypeIdentifier);
398
        }
399
400
        if (!$pass->getTeamIdentifier()) {
401
            $pass->setTeamIdentifier($this->teamIdentifier);
402
        }
403
404
        if (!$pass->getOrganizationName()) {
405
            $pass->setOrganizationName($this->organizationName);
406
        }
407
    }
408
409
    /**
410
     * @param $array
411
     *
412
     * @return string
413
     */
414
    private static function jsonEncode($array)
415
    {
416
        // Check if JSON_UNESCAPED_SLASHES is defined to support PHP 5.3.
417
        $options = defined('JSON_UNESCAPED_SLASHES') ? JSON_UNESCAPED_SLASHES : 0;
418
        return json_encode($array, $options);
419
    }
420
421
    /**
422
     * @param $passName
423
     * @param PassInterface $pass
424
     *
425
     * @return string
426
     */
427
    public function getPassName($passName, PassInterface $pass)
428
    {
429
        $passNameSanitised = preg_replace('/[^a-zA-Z0-9]+/', '', $passName);
430
        return strlen($passNameSanitised) != 0 ? $passNameSanitised : $pass->getSerialNumber();
431
    }
432
433
    /**
434
     * @param $passDir
435
     *
436
     * @return string
437
     */
438
    private function prepareManifest($passDir)
439
    {
440
        $manifestJSONFile = $passDir . 'manifest.json';
441
        $manifest = [];
442
        $files = new RecursiveIteratorIterator(
443
            new RecursiveDirectoryIterator($passDir),
444
            RecursiveIteratorIterator::SELF_FIRST
445
        );
446
        foreach ($files as $file) {
447
            // Ignore "." and ".." folders
448
            if (in_array(substr($file, strrpos($file, '/') + 1), ['.', '..'])) {
449
                continue;
450
            }
451
            //
452
            $filePath = realpath($file);
453
            if (is_file($filePath) === true) {
454
                $relativePathName = str_replace($passDir, '', $file->getPathname());
455
                $manifest[$relativePathName] = sha1_file($filePath);
456
            }
457
        }
458
        file_put_contents($manifestJSONFile, $this->jsonEncode($manifest));
459
460
        return $manifestJSONFile;
461
    }
462
463
    /**
464
     * @param PassInterface $pass
465
     *
466
     * @return string
467
     */
468
    private function preparePassDirectory(PassInterface $pass)
469
    {
470
        $passDir = $this->getNormalizedOutputPath() . $pass->getSerialNumber() . DIRECTORY_SEPARATOR;
471
        $passDirExists = file_exists($passDir);
472
        if ($passDirExists && !$this->isOverwrite()) {
473
            throw new FileException('Temporary pass directory already exists');
474
        } elseif (!$passDirExists && !mkdir($passDir, 0777, true)) {
475
            throw new FileException("Couldn't create temporary pass directory");
476
        }
477
478
        return $passDir;
479
    }
480
481
    /**
482
     * @param PassInterface $pass
483
     * @param string        $passDir
484
     */
485
    private function prepareImages(PassInterface $pass, $passDir): void
486
    {
487
        /** @var Image $image */
488
        foreach ($pass->getImages() as $image) {
489
            $fileName = $passDir . $image->getContext();
490
            if ($image->getDensity() === 2) {
491
                $fileName .= '@2x';
492
            } elseif ($image->getDensity() === 3) {
493
                $fileName .= '@3x';
494
            }
495
496
            $fileName .= '.' . $image->getExtension();
497
            copy($image->getPathname(), $fileName);
498
        }
499
    }
500
501
    /**
502
     * @param PassInterface $pass
503
     * @param string        $passDir
504
     */
505
    private function prepareLocalizations(PassInterface $pass, $passDir): void
506
    {
507
        foreach ($pass->getLocalizations() as $localization) {
508
            // Create dir (LANGUAGE.lproj)
509
            $localizationDir = $passDir . $localization->getLanguage() . '.lproj' . DIRECTORY_SEPARATOR;
510
            $localizationDirExists = file_exists($localizationDir);
511
            if ($localizationDirExists && !$this->isOverwrite()) {
512
                throw new FileException("Temporary pass localization directory already exists ({$localization->getLanguage()})");
513
            } elseif (!$localizationDirExists && !mkdir($localizationDir, 0777, true)) {
514
                throw new FileException("Couldn't create temporary pass localization directory ({$localization->getLanguage()})");
515
            }
516
517
            // pass.strings File (Format: "token" = "value")
518
            $localizationStringsFile = $localizationDir . 'pass.strings';
519
            file_put_contents($localizationStringsFile, $localization->getStringsFileOutput());
520
521
            // Localization images
522
            foreach ($localization->getImages() as $image) {
523
                $fileName = $localizationDir . $image->getContext();
524
                if ($image->getDensity() === 2) {
525
                    $fileName .= '@2x';
526
                } elseif ($image->getDensity() === 3) {
527
                    $fileName .= '@3x';
528
                }
529
                $fileName .= '.' . $image->getExtension();
530
                copy($image->getPathname(), $fileName);
531
            }
532
        }
533
    }
534
}
535