Completed
Push — master ( 9e5240...db4f23 )
by
unknown
02:08
created

PassFactory::setPassValidator()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 6
rs 9.4285
cc 1
eloc 3
nc 1
nop 1
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\Exception\PassInvalidException;
20
use Passbook\Pass\Image;
21
use RecursiveDirectoryIterator;
22
use RecursiveIteratorIterator;
23
use SplFileObject;
24
use ZipArchive;
25
26
/**
27
 * PassFactory - Creates .pkpass files
28
 *
29
 * @author Eymen Gunay <[email protected]>
30
 */
31
class PassFactory
32
{
33
    /**
34
     * Output path for generated pass files
35
     *
36
     * @var string
37
     */
38
    protected $outputPath = '';
39
40
    /**
41
     * Overwrite if pass exists
42
     *
43
     * @var bool
44
     */
45
    protected $overwrite = false;
46
47
    /**
48
     * Pass type identifier
49
     *
50
     * @var string
51
     */
52
    protected $passTypeIdentifier;
53
54
    /**
55
     * Team identifier
56
     *
57
     * @var string
58
     */
59
    protected $teamIdentifier;
60
61
    /**
62
     * Organization name
63
     *
64
     * @var string
65
     */
66
    protected $organizationName;
67
68
    /**
69
     * P12 file
70
     * 
71
     * @var \Passbook\Certificate\P12Interface
72
     */
73
    protected $p12;
74
75
    /**
76
     * WWDR file
77
     * 
78
     * @var \Passbook\Certificate\WWDRInterface
79
     */
80
    protected $wwdr;
81
82
    /**
83
     * @var bool - skip signing the pass; should only be used for testing
84
     */
85
    protected $skipSignature;
86
87
    /**
88
     * @var PassValidatorInterface
89
     */
90
    private $passValidator;
91
92
    /**
93
     * Pass file extension
94
     *
95
     * @var string
96
     */
97
    const PASS_EXTENSION = '.pkpass';
98
99
    public function __construct($passTypeIdentifier, $teamIdentifier, $organizationName, $p12File, $p12Pass, $wwdrFile)
100
    {
101
        // Required pass information
102
        $this->passTypeIdentifier = $passTypeIdentifier;
103
        $this->teamIdentifier = $teamIdentifier;
104
        $this->organizationName = $organizationName;
105
106
        // Create certificate objects
107
        $this->p12 = new P12($p12File, $p12Pass);
108
        $this->wwdr = new WWDR($wwdrFile);
109
        
110
        // By default use the PassValidator
111
        $this->passValidator = new PassValidator();
112
    }
113
114
    /**
115
     * Set outputPath
116
     *
117
     * @param string
118
     *
119
     * @return $this
120
     */
121
    public function setOutputPath($outputPath)
122
    {
123
        $this->outputPath = $outputPath;
124
125
        return $this;
126
    }
127
128
    /**
129
     * Get outputPath
130
     *
131
     * @return string
132
     */
133
    public function getOutputPath()
134
    {
135
        return $this->outputPath;
136
    }
137
138
    /**
139
     * The output path with a directory separator on the end.
140
     *
141
     * @return string
142
     */
143
    public function getNormalizedOutputPath()
144
    {
145
        return rtrim($this->outputPath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
146
    }
147
148
    /**
149
     * Set overwrite
150
     *
151
     * @param boolean
152
     *
153
     * @return $this
154
     */
155
    public function setOverwrite($overwrite)
156
    {
157
        $this->overwrite = $overwrite;
158
159
        return $this;
160
    }
161
162
    /**
163
     * Get overwrite
164
     *
165
     * @return boolean
166
     */
167
    public function isOverwrite()
168
    {
169
        return $this->overwrite;
170
    }
171
172
    /**
173
     * Set skip signature
174
     *
175
     * When set, the pass will not be signed when packaged. This should only
176
     * be used for testing.
177
     *
178
     * @param boolean
179
     *
180
     * @return $this
181
     */
182
    public function setSkipSignature($skipSignature)
183
    {
184
        $this->skipSignature = $skipSignature;
185
186
        return $this;
187
    }
188
189
    /**
190
     * Get skip signature
191
     *
192
     * @return boolean
193
     */
194
    public function getSkipSignature()
0 ignored issues
show
Coding Style introduced by
function getSkipSignature() does not seem to conform to the naming convention (^(?:is|has|should|may|supports)).

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.

Loading history...
195
    {
196
        return $this->skipSignature;
197
    }
198
199
    /**
200
     * Set an implementation of PassValidatorInterface to validate the pass
201
     * before packaging. When set to null, no validation is performed when
202
     * packaging the pass.
203
     *
204
     * @param PassValidatorInterface|null $passValidator
205
     *
206
     * @return $this
207
     */
208
    public function setPassValidator(PassValidatorInterface $passValidator = null)
209
    {
210
        $this->passValidator = $passValidator;
211
212
        return $this;
213
    }
214
215
    /**
216
     * @return PassValidatorInterface
217
     */
218
    public function getPassValidator()
219
    {
220
        return $this->passValidator;
221
    }
222
223
    /**
224
     * Serialize pass
225
     *
226
     * @param  PassInterface $pass
227
     *
228
     * @return string
229
     */
230
    public static function serialize(PassInterface $pass)
231
    {
232
        return self::jsonEncode($pass->toArray());
233
    }
234
235
    /**
236
     * Creates a pkpass file
237
     *
238
     * @param  PassInterface $pass - the pass to be packaged into a .pkpass file
239
     * @param string $passName - filename to be used for the pass; if blank the serial number will be used
240
     *
241
     * @return SplFileObject If an IO error occurred
242
     * @throws Exception
243
     */
244
    public function package(PassInterface $pass, $passName = '')
245
    {
246
        if ($pass->getSerialNumber() == '') {
247
            throw new \InvalidArgumentException('Pass must have a serial number to be packaged');
248
        }
249
250
        $this->populateRequiredInformation($pass);
251
252
        if ($this->passValidator) {
253
            if (!$this->passValidator->validate($pass)){
254
                throw new PassInvalidException($this->passValidator->getErrors());
255
            };
256
        }
257
258
        $passDir = $this->preparePassDirectory($pass);
259
260
        // Serialize pass
261
        file_put_contents($passDir . 'pass.json', self::serialize($pass));
262
263
        // Images
264
        $this->prepareImages($pass, $passDir);
265
266
        // Localizations
267
        $this->prepareLocalizations($pass, $passDir);
268
269
        // Manifest.json - recursive, also add files in sub directories
270
        $manifestJSONFile = $this->prepareManifest($passDir);
271
272
        // Signature
273
        $this->sign($passDir, $manifestJSONFile);
274
275
        // Zip pass
276
        $zipFile = $this->getNormalizedOutputPath() . $this->getPassName($passName, $pass) . self::PASS_EXTENSION;
277
        $this->zip($passDir, $zipFile);
278
279
        // Remove temporary pass directory
280
        $this->rrmdir($passDir);
281
282
        return new SplFileObject($zipFile);
283
    }
284
285
    /**
286
     * @param $passDir
287
     * @param $manifestJSONFile
288
     */
289
    private function sign($passDir, $manifestJSONFile)
290
    {
291
        if ($this->getSkipSignature()) {
292
            return;
293
        }
294
295
        $signatureFile = $passDir . 'signature';
296
        $p12 = file_get_contents($this->p12->getRealPath());
297
        $certs = array();
298
        if (openssl_pkcs12_read($p12, $certs, $this->p12->getPassword()) == true) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
299
            $certdata = openssl_x509_read($certs['cert']);
300
            $privkey = openssl_pkey_get_private($certs['pkey'], $this->p12->getPassword());
301
            openssl_pkcs7_sign(
302
                $manifestJSONFile,
303
                $signatureFile,
304
                $certdata,
305
                $privkey,
306
                array(),
307
                PKCS7_BINARY | PKCS7_DETACHED,
308
                $this->wwdr->getRealPath()
309
            );
310
            // Get signature content
311
            $signature = file_get_contents($signatureFile);
312
            // Check signature content
313
            if (!$signature) {
314
                throw new FileException("Couldn't read signature file.");
315
            }
316
            // Delimiters
317
            $begin = 'filename="smime.p7s"';
318
            $end = '------';
319
            // Convert signature
320
            $signature = substr($signature, strpos($signature, $begin) + strlen($begin));
321
            $signature = substr($signature, 0, strpos($signature, $end));
322
            $signature = base64_decode($signature);
323
            // Put new signature
324
            if (!file_put_contents($signatureFile, $signature)) {
325
                throw new FileException("Couldn't write signature file.");
326
            }
327
        } else {
328
            throw new FileException("Error reading certificate file");
329
        }
330
    }
331
332
    /**
333
     * Creates a zip of a directory including all sub directories (recursive)
334
     *
335
     * @param $source - path to the source directory
336
     * @param $destination - output directory
337
     *
338
     * @return bool
339
     * @throws Exception
340
     */
341
    private function zip($source, $destination)
0 ignored issues
show
Coding Style introduced by
function zip() does not seem to conform to the naming convention (^(?:is|has|should|may|supports)).

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.

Loading history...
342
    {
343
        if (!extension_loaded('zip')) {
344
            throw new Exception("ZIP extension not available");
345
        }
346
347
        $source = realpath($source);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $source. This often makes code more readable.
Loading history...
348
        if (!is_dir($source)) {
349
            throw new FileException("Source must be a directory.");
350
        }
351
        
352
        $zip = new ZipArchive();
353
        $shouldOverwrite = $this->isOverwrite() ? ZipArchive::OVERWRITE : 0;
354
        if (!$zip->open($destination, ZipArchive::CREATE | $shouldOverwrite)) {
355
            throw new FileException("Couldn't open zip file.");
356
        }
357
358
        /* @var $iterator RecursiveIteratorIterator|RecursiveDirectoryIterator */
359
        $dirIterator = new RecursiveDirectoryIterator($source, FilesystemIterator::SKIP_DOTS);
360
        $iterator = new RecursiveIteratorIterator($dirIterator, RecursiveIteratorIterator::SELF_FIRST);
361
        while ($iterator->valid()) {
362
            if ($iterator->isDir()) {
363
                $zip->addEmptyDir($iterator->getSubPathName());
364
            } else if ($iterator->isFile()) {
365
                $zip->addFromString($iterator->getSubPathName(), file_get_contents($iterator->key()));
366
            }
367
            $iterator->next();
368
        }
369
370
        return $zip->close();
371
    }
372
373
    /**
374
     * Recursive folder remove
375
     *
376
     * @param string $dir
377
     *
378
     * @return bool
379
     */
380
    private function rrmdir($dir)
0 ignored issues
show
Coding Style introduced by
function rrmdir() does not seem to conform to the naming convention (^(?:is|has|should|may|supports)).

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.

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