Completed
Pull Request — master (#50)
by
unknown
02:01
created

PassFactory::getPassName()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
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 5
rs 9.4285
cc 2
eloc 3
nc 2
nop 2
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
193
     *
194
     * @throws FileException          If an IO error occurred
195
     * @return SplFileObject
196
     */
197
    public function package(PassInterface $pass, $passName = '')
198
    {
199
        if ($pass->getSerialNumber() == '') {
200
            throw new \InvalidArgumentException('Pass must have a serial number to be packaged');
201
        }
202
203
        $this->populateRequiredInformation($pass);
204
205
        // Serialize pass
206
        $json = self::serialize($pass);
207
208
        $outputPath = rtrim($this->getOutputPath(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
209
        $passDir = $outputPath . $this->getPassName($passName, $pass) . DIRECTORY_SEPARATOR;
210
        $passDirExists = file_exists($passDir);
211
        if ($passDirExists && !$this->isOverwrite()) {
212
            throw new FileException("Temporary pass directory already exists");
213
        } elseif (!$passDirExists && !mkdir($passDir, 0777, true)) {
214
            throw new FileException("Couldn't create temporary pass directory");
215
        }
216
217
        // Pass.json
218
        $passJSONFile = $passDir . 'pass.json';
219
        file_put_contents($passJSONFile, $json);
220
221
        // Images
222
        /** @var Image $image */
223
        foreach ($pass->getImages() as $image) {
224
            $fileName = $passDir . $image->getContext();
225
            if ($image->isRetina()) {
226
                $fileName .= '@2x';
227
            }
228
            $fileName .= '.' . $image->getExtension();
229
            copy($image->getPathname(), $fileName);
230
        }
231
232
        // Localizations
233
        foreach ($pass->getLocalizations() as $localization) {
234
            // Create dir (LANGUAGE.lproj)
235
            $localizationDir = $passDir . $localization->getLanguage() . '.lproj' . DIRECTORY_SEPARATOR;
236
            mkdir($localizationDir, 0777, true);
237
238
            // pass.strings File (Format: "token" = "value")
239
            $localizationStringsFile = $localizationDir . 'pass.strings';
240
            file_put_contents($localizationStringsFile, $localization->getStringsFileOutput());
241
242
            // Localization images
243
            foreach ($localization->getImages() as $image) {
244
                $fileName = $localizationDir . $image->getContext();
245
                if ($image->isRetina()) {
246
                    $fileName .= '@2x';
247
                }
248
                $fileName .= '.' . $image->getExtension();
249
                copy($image->getPathname(), $fileName);
250
            }
251
        }
252
253
        // Manifest.json - recursive, also add files in sub directories
254
        $manifestJSONFile = $passDir . 'manifest.json';
255
        $manifest = array();
256
        $files = new RecursiveIteratorIterator(
257
            new RecursiveDirectoryIterator($passDir),
258
            RecursiveIteratorIterator::SELF_FIRST
259
        );
260
        foreach ($files as $file) {
261
            // Ignore "." and ".." folders
262
            if (in_array(substr($file, strrpos($file, '/') + 1), array('.', '..'))) {
263
                continue;
264
            }
265
            //
266
            $filePath = realpath($file);
267
            if (is_file($filePath) === true) {
268
                $relativePathName = str_replace($passDir, '', $file->getPathname());
269
                $manifest[$relativePathName] = sha1_file($filePath);
270
            }
271
        }
272
        file_put_contents($manifestJSONFile, $this->jsonEncode($manifest));
273
274
        // Signature
275
        $this->sign($passDir, $manifestJSONFile);
276
277
        // Zip pass
278
        $zipFile = $outputPath . $this->getPassName($passName, $pass) . self::PASS_EXTENSION;
279
        $this->zip($passDir, $zipFile);
280
281
        // Remove temporary pass directory
282
        $this->rrmdir($passDir);
283
284
        return new SplFileObject($zipFile);
285
    }
286
287
    /**
288
     * @param $passDir
289
     * @param $manifestJSONFile
290
     */
291
    private function sign($passDir, $manifestJSONFile)
292
    {
293
        if ($this->getSkipSignature()) {
294
            return;
295
        }
296
297
        $signatureFile = $passDir . 'signature';
298
        $p12 = file_get_contents($this->p12->getRealPath());
299
        $certs = array();
300
        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...
301
            $certdata = openssl_x509_read($certs['cert']);
302
            $privkey = openssl_pkey_get_private($certs['pkey'], $this->p12->getPassword());
303
            openssl_pkcs7_sign(
304
                $manifestJSONFile,
305
                $signatureFile,
306
                $certdata,
307
                $privkey,
308
                array(),
309
                PKCS7_BINARY | PKCS7_DETACHED,
310
                $this->wwdr->getRealPath()
311
            );
312
            // Get signature content
313
            $signature = @file_get_contents($signatureFile);
314
            // Check signature content
315
            if (!$signature) {
316
                throw new FileException("Couldn't read signature file.");
317
            }
318
            // Delimiters
319
            $begin = 'filename="smime.p7s"';
320
            $end = '------';
321
            // Convert signature
322
            $signature = substr($signature, strpos($signature, $begin) + strlen($begin));
323
            $signature = substr($signature, 0, strpos($signature, $end));
324
            $signature = base64_decode($signature);
325
            // Put new signature
326
            if (!file_put_contents($signatureFile, $signature)) {
327
                throw new FileException("Couldn't write signature file.");
328
            }
329
        } else {
330
            throw new FileException("Error reading certificate file");
331
        }
332
    }
333
334
    /**
335
     * Creates a zip of a directory including all sub directories (recursive)
336
     *
337
     * @param $source - path to the source directory
338
     * @param $destination - output directory
339
     *
340
     * @return bool
341
     * @throws Exception
342
     */
343
    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...
344
    {
345
        if (!extension_loaded('zip')) {
346
            throw new Exception("ZIP extension not available");
347
        }
348
349
        $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...
350
        if (!is_dir($source)) {
351
            throw new FileException("Source must be a directory.");
352
        }
353
        
354
        $zip = new ZipArchive();
355
        $shouldOverwrite = $this->isOverwrite() ? ZipArchive::OVERWRITE : 0;
356
        if (!$zip->open($destination, ZipArchive::CREATE | $shouldOverwrite)) {
357
            throw new FileException("Couldn't open zip file.");
358
        }
359
360
        /* @var $iterator RecursiveIteratorIterator|RecursiveDirectoryIterator */
361
        $dirIterator = new RecursiveDirectoryIterator($source, FilesystemIterator::SKIP_DOTS);
362
        $iterator = new RecursiveIteratorIterator($dirIterator, RecursiveIteratorIterator::SELF_FIRST);
363
        while ($iterator->valid()) {
364
            if ($iterator->isDir()) {
365
                $zip->addEmptyDir($iterator->getSubPathName());
366
            } else if ($iterator->isFile()) {
367
                $zip->addFromString($iterator->getSubPathName(), file_get_contents($iterator->key()));
368
            }
369
            $iterator->next();
370
        }
371
372
        return $zip->close();
373
    }
374
375
    /**
376
     * Recursive folder remove
377
     *
378
     * @param string $dir
379
     *
380
     * @return bool
381
     */
382
    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...
383
    {
384
        $files = array_diff(scandir($dir), array('.', '..'));
385
        foreach ($files as $file) {
386
            is_dir("$dir/$file") ? $this->rrmdir("$dir/$file") : unlink("$dir/$file");
387
        }
388
389
        return rmdir($dir);
390
    }
391
392
    /**
393
     * @param PassInterface $pass
394
     */
395
    private function populateRequiredInformation(PassInterface $pass)
396
    {
397
        if (!$pass->getPassTypeIdentifier()) {
398
            $pass->setPassTypeIdentifier($this->passTypeIdentifier);
399
        }
400
401
        if (!$pass->getTeamIdentifier()) {
402
            $pass->setTeamIdentifier($this->teamIdentifier);
403
        }
404
405
        if (!$pass->getOrganizationName()) {
406
            $pass->setOrganizationName($this->organizationName);
407
        }
408
    }
409
410
    /**
411
     * @param $array
412
     *
413
     * @return string
414
     */
415
    private static function jsonEncode($array)
416
    {
417
        // Check if JSON_UNESCAPED_SLASHES is defined to support PHP 5.3.
418
        $options = defined('JSON_UNESCAPED_SLASHES') ? JSON_UNESCAPED_SLASHES : 0;
419
        return json_encode($array, $options);
420
    }
421
    
422
    /**
423
     * @param $passName
424
     * @param PassInterface $pass
425
     *
426
     * @return string
427
     */
428
    public function getPassName($passName, PassInterface $pass)
429
    {
430
        $passNameSanitised = preg_replace("/[^a-zA-Z0-9]+/", "", $passName);
431
        return strlen($passNameSanitised) != 0 ? $passNameSanitised : $pass->getSerialNumber();
432
    }
433
434
}
435