Completed
Push — master ( c6286a...4a1f77 )
by Eymen
15s queued 11s
created

PassValidator::validateBarcodeKeys()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 18
rs 9.6666
c 0
b 0
f 0
cc 4
nc 5
nop 1
1
<?php
2
3
namespace Passbook;
4
5
use Passbook\Pass\Barcode;
6
use Passbook\Pass\Beacon;
7
use Passbook\Pass\Nfc;
8
use Passbook\Pass\Location;
9
10
/**
11
 * Validates the contents of a pass
12
 *
13
 * This attempts to find errors with a pass that would prevent it from working
14
 * properly with Apple Wallet. Passes with errors often just fail to load and
15
 * do not provide any feedback as to the error. Additionally, some issues (such
16
 * as a pass not having an icon) are not well documented and potentially
17
 * difficult to identify. This class aims to help identify and prevent these
18
 * issues.
19
 */
20
class PassValidator implements PassValidatorInterface
21
{
22
    private $errors;
23
24
    const DESCRIPTION_REQUIRED = 'description is required and cannot be blank';
25
    const FORMAT_VERSION_REQUIRED = 'formatVersion is required and must be 1';
26
    const ORGANIZATION_NAME_REQUIRED = 'organizationName is required and cannot be blank';
27
    const PASS_TYPE_IDENTIFIER_REQUIRED = 'passTypeIdentifier is required and cannot be blank';
28
    const SERIAL_NUMBER_REQUIRED = 'serialNumber is required and cannot be blank';
29
    const TEAM_IDENTIFIER_REQUIRED = 'teamIdentifier is required and cannot be blank';
30
    const ICON_REQUIRED = 'pass must have an icon image';
31
    const BARCODE_FORMAT_INVALID = 'barcode format is invalid';
32
    const BARCODE_MESSAGE_INVALID = 'barcode message is invalid; must be a string';
33
    const LOCATION_LATITUDE_REQUIRED = 'location latitude is required';
34
    const LOCATION_LONGITUDE_REQUIRED = 'location longitude is required';
35
    const LOCATION_LATITUDE_INVALID = 'location latitude is invalid; must be numeric';
36
    const LOCATION_LONGITUDE_INVALID = 'location longitude is invalid; must be numeric';
37
    const LOCATION_ALTITUDE_INVALID = 'location altitude is invalid; must be numeric';
38
    const BEACON_PROXIMITY_UUID_REQUIRED = 'beacon proximityUUID is required';
39
    const BEACON_MAJOR_INVALID = 'beacon major is invalid; must be 16-bit unsigned integer';
40
    const BEACON_MINOR_INVALID = 'beacon minor is invalid; must be 16-bit unsigned integer';
41
    const NFC_MESSAGE_REQUIRED = 'NFC message is required';
42
    const NFC_ENCRYPTION_PUBLIC_KEY_REQUIRED = 'NFC encryption public key is required';
43
    const WEB_SERVICE_URL_INVALID = 'webServiceURL is invalid; must start with https (or http for development)';
44
    const WEB_SERVICE_AUTHENTICATION_TOKEN_REQUIRED = 'authenticationToken required with webServiceURL and cannot be blank';
45
    const WEB_SERVICE_AUTHENTICATION_TOKEN_INVALID = 'authenticationToken is invalid; must be at least 16 characters';
46
    const ASSOCIATED_STORE_IDENTIFIER_INVALID = 'associatedStoreIdentifiers is invalid; must be an integer';
47
    const ASSOCIATED_STORE_IDENTIFIER_REQUIRED = 'appLaunchURL is required when associatedStoreIdentifiers is present';
48
    const IMAGE_TYPE_INVALID = 'image files must be PNG format';
49
    const GROUPING_IDENTITY_INVALID = 'the grouping identity may only be used on boarding pass and event ticket types';
50
51
    /**
52
     * {@inheritdoc}
53
     */
54
    public function validate(PassInterface $pass)
55
    {
56
        $this->errors = [];
57
58
        $this->validateRequiredFields($pass);
59
        $this->validateBeaconKeys($pass);
60
        $this->validateNfcKeys($pass);
61
        $this->validateLocationKeys($pass);
62
        $this->validateBarcodeKeys($pass);
63
        $this->validateWebServiceKeys($pass);
64
        $this->validateIcon($pass);
65
        $this->validateImageType($pass);
66
        $this->validateAssociatedStoreIdentifiers($pass);
67
        $this->validateGroupingIdentity($pass);
68
69
        return count($this->errors) === 0;
70
    }
71
72
    /**
73
     * {@inheritdoc}
74
     */
75
    public function getErrors()
76
    {
77
        return $this->errors;
78
    }
79
80
    private function validateRequiredFields(PassInterface $pass)
81
    {
82
        if ($this->isBlankOrNull($pass->getDescription())) {
83
            $this->addError(self::DESCRIPTION_REQUIRED);
84
        }
85
86
        if ($pass->getFormatVersion() !== 1) {
87
            $this->addError(self::FORMAT_VERSION_REQUIRED);
88
        }
89
90
        if ($this->isBlankOrNull($pass->getOrganizationName())) {
91
            $this->addError(self::ORGANIZATION_NAME_REQUIRED);
92
        }
93
94
        if ($this->isBlankOrNull($pass->getPassTypeIdentifier())) {
95
            $this->addError(self::PASS_TYPE_IDENTIFIER_REQUIRED);
96
        }
97
98
        if ($this->isBlankOrNull($pass->getSerialNumber())) {
99
            $this->addError(self::SERIAL_NUMBER_REQUIRED);
100
        }
101
102
        if ($this->isBlankOrNull($pass->getTeamIdentifier())) {
103
            $this->addError(self::TEAM_IDENTIFIER_REQUIRED);
104
        }
105
    }
106
107
    private function validateBeaconKeys(PassInterface $pass)
108
    {
109
        $beacons = $pass->getBeacons();
110
111
        foreach ($beacons as $beacon) {
112
            $this->validateBeacon($beacon);
113
        }
114
    }
115
116
    private function validateNfcKeys(PassInterface $pass)
117
    {
118
        $nfcs = $pass->getNfc();
119
        foreach ($nfcs as $nfc) {
120
            $this->validateNfc($nfc);
121
        }
122
    }
123
124
    private function validateBeacon(Beacon $beacon)
125
    {
126
        if ($this->isBlankOrNull($beacon->getProximityUUID())) {
127
            $this->addError(self::BEACON_PROXIMITY_UUID_REQUIRED);
128
        }
129
130
        if (null !== $beacon->getMajor()) {
131
            if (!is_int($beacon->getMajor()) || $beacon->getMajor() < 0 || $beacon->getMajor() > 65535) {
132
                $this->addError(self::BEACON_MAJOR_INVALID);
133
            }
134
        }
135
136
        if (null !== $beacon->getMinor()) {
137
            if (!is_int($beacon->getMinor()) || $beacon->getMinor() < 0 || $beacon->getMinor() > 65535) {
138
                $this->addError(self::BEACON_MINOR_INVALID);
139
            }
140
        }
141
    }
142
143
    private function validateNfc(Nfc $nfc)
144
    {
145
        if ($this->isBlankOrNull($nfc->getMessage())) {
146
            $this->addError(self::NFC_MESSAGE_REQUIRED);
147
        }
148
149
        if ($this->isBlankOrNull($nfc->getEncryptionPublicKey())) {
150
            $this->addError(self::NFC_ENCRYPTION_PUBLIC_KEY_REQUIRED);
151
        }
152
    }
153
154
    private function validateLocationKeys(PassInterface $pass)
155
    {
156
        $locations = $pass->getLocations();
157
158
        foreach ($locations as $location) {
159
            $this->validateLocation($location);
160
        }
161
    }
162
163
    private function validateLocation(Location $location)
164
    {
165
        if ($this->isBlankOrNull($location->getLatitude())) {
166
            $this->addError(self::LOCATION_LATITUDE_REQUIRED);
167
        }
168
169
        if (!is_numeric($location->getLatitude())) {
170
            $this->addError(self::LOCATION_LATITUDE_INVALID);
171
        }
172
173
        if ($this->isBlankOrNull($location->getLongitude())) {
174
            $this->addError(self::LOCATION_LONGITUDE_REQUIRED);
175
        }
176
177
        if (!is_numeric($location->getLongitude())) {
178
            $this->addError(self::LOCATION_LONGITUDE_INVALID);
179
        }
180
181
        if (!is_numeric($location->getAltitude()) && null !== $location->getAltitude()) {
182
            $this->addError(self::LOCATION_ALTITUDE_INVALID);
183
        }
184
    }
185
186
    private function validateBarcodeKeys(PassInterface $pass)
187
    {
188
        $validBarcodeFormats = array(Barcode::TYPE_QR, Barcode::TYPE_AZTEC, Barcode::TYPE_PDF_417, Barcode::TYPE_CODE_128);
189
190
        $barcode = $pass->getBarcode();
0 ignored issues
show
Deprecated Code introduced by
The method Passbook\PassInterface::getBarcode() has been deprecated with message: use getBarcodes instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
191
192
        if (!$barcode) {
193
            return;
194
        }
195
196
        if (!in_array($barcode->getFormat(), $validBarcodeFormats)) {
197
            $this->addError(self::BARCODE_FORMAT_INVALID);
198
        }
199
200
        if (!is_string($barcode->getMessage())) {
201
            $this->addError(self::BARCODE_MESSAGE_INVALID);
202
        }
203
    }
204
205
    private function validateWebServiceKeys(PassInterface $pass)
206
    {
207
        if (null === $pass->getWebServiceURL()) {
208
            return;
209
        }
210
211
        if (strpos($pass->getWebServiceURL(), 'http') !== 0) {
212
            $this->addError(self::WEB_SERVICE_URL_INVALID);
213
        }
214
215
        if ($this->isBlankOrNull($pass->getAuthenticationToken())) {
216
            $this->addError(self::WEB_SERVICE_AUTHENTICATION_TOKEN_REQUIRED);
217
        }
218
219
        if (strlen($pass->getAuthenticationToken()) < 16) {
220
            $this->addError(self::WEB_SERVICE_AUTHENTICATION_TOKEN_INVALID);
221
        }
222
    }
223
224
    private function validateIcon(PassInterface $pass)
225
    {
226
        foreach ($pass->getImages() as $image) {
227
            if ($image->getContext() === 'icon') {
228
                return;
229
            }
230
        }
231
232
        $this->addError(self::ICON_REQUIRED);
233
    }
234
235
    private function validateImageType(PassInterface $pass)
236
    {
237
        foreach ($pass->getImages() as $image) {
238
            if ('png' !== strtolower($image->getExtension())) {
239
                $this->addError(self::IMAGE_TYPE_INVALID);
240
            }
241
        }
242
    }
243
244
    private function validateAssociatedStoreIdentifiers(PassInterface $pass)
245
    {
246
        //appLaunchURL
247
248
        $associatedStoreIdentifiers = $pass->getAssociatedStoreIdentifiers();
249
250
        if (null !== $pass->getAppLaunchURL() && count($associatedStoreIdentifiers) == 0) {
251
            $this->addError(self::ASSOCIATED_STORE_IDENTIFIER_REQUIRED);
252
        }
253
254
255
        foreach ($associatedStoreIdentifiers as $associatedStoreIdentifier) {
256
            if (!is_int($associatedStoreIdentifier)) {
257
                $this->addError(self::ASSOCIATED_STORE_IDENTIFIER_INVALID);
258
259
                return;
260
            }
261
        }
262
    }
263
264
    private function validateGroupingIdentity(PassInterface $pass)
265
    {
266
        if (null !== $pass->getGroupingIdentifier() && !in_array($pass->getType(), ['boardingPass', 'eventTicket'])) {
267
            $this->addError(self::GROUPING_IDENTITY_INVALID);
268
269
            return;
270
        }
271
    }
272
273
    private function isBlankOrNull($text)
274
    {
275
        return '' === $text || null === $text;
276
    }
277
278
    private function addError($string)
279
    {
280
        $this->errors[] = $string;
281
    }
282
283
}
284