Completed
Push — master ( 07cf6a...9e5240 )
by
unknown
02:11
created

PassFactory::getOutputPath()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 0
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()
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...
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) {
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...
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)
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...
345
    {
346
        if (!extension_loaded('zip')) {
347
            throw new Exception("ZIP extension not available");
348
        }
349
350
        $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...
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)
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...
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