Completed
Push — master ( c103b4...a1e452 )
by
unknown
11s
created

PassValidator::validateRequiredFields()   C

Complexity

Conditions 7
Paths 64

Size

Total Lines 26
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 26
c 0
b 0
f 0
rs 6.7272
cc 7
eloc 13
nc 64
nop 1
1
<?php
2
3
namespace Passbook;
4
5
use Passbook\Pass\Barcode;
6
use Passbook\Pass\Beacon;
7
use Passbook\Pass\Location;
8
9
/**
10
 * Validates the contents of a pass
11
 *
12
 * This attempts to find errors with a pass that would prevent it from working
13
 * properly with Apple Wallet. Passes with errors often just fail to load and
14
 * do not provide any feedback as to the error. Additionally, some issues (such
15
 * as a pass not having an icon) are not well documented and potentially
16
 * difficult to identify. This class aims to help identify and prevent these
17
 * issues.
18
 */
19
class PassValidator implements PassValidatorInterface
20
{
21
    private $errors;
22
23
    const DESCRIPTION_REQUIRED = 'description is required and cannot be blank';
24
    const FORMAT_VERSION_REQUIRED = 'formatVersion is required and must be 1';
25
    const ORGANIZATION_NAME_REQUIRED = 'organizationName is required and cannot be blank';
26
    const PASS_TYPE_IDENTIFIER_REQUIRED = 'passTypeIdentifier is required and cannot be blank';
27
    const SERIAL_NUMBER_REQUIRED = 'serialNumber is required and cannot be blank';
28
    const TEAM_IDENTIFIER_REQUIRED = 'teamIdentifier is required and cannot be blank';
29
    const ICON_REQUIRED = 'pass must have an icon image';
30
    const BARCODE_FORMAT_INVALID = 'barcode format is invalid';
31
    const BARCODE_MESSAGE_INVALID = 'barcode message is invalid; must be a string';
32
    const LOCATION_LATITUDE_REQUIRED = 'location latitude is required';
33
    const LOCATION_LONGITUDE_REQUIRED = 'location longitude is required';
34
    const LOCATION_LATITUDE_INVALID = 'location latitude is invalid; must be numeric';
35
    const LOCATION_LONGITUDE_INVALID = 'location longitude is invalid; must be numeric';
36
    const LOCATION_ALTITUDE_INVALID = 'location altitude is invalid; must be numeric';
37
    const BEACON_PROXIMITY_UUID_REQUIRED = 'beacon proximityUUID is required';
38
    const BEACON_MAJOR_INVALID = 'beacon major is invalid; must be 16-bit unsigned integer';
39
    const BEACON_MINOR_INVALID = 'beacon minor is invalid; must be 16-bit unsigned integer';
40
    const WEB_SERVICE_URL_INVALID = 'webServiceURL is invalid; must start with https (or http for development)';
41
    const WEB_SERVICE_AUTHENTICATION_TOKEN_REQUIRED = 'authenticationToken required with webServiceURL and cannot be blank';
42
    const WEB_SERVICE_AUTHENTICATION_TOKEN_INVALID = 'authenticationToken is invalid; must be at least 16 characters';
43
    const ASSOCIATED_STORE_IDENTIFIER_INVALID = 'associatedStoreIdentifiers is invalid; must be an integer';
44
    const ASSOCIATED_STORE_IDENTIFIER_REQUIRED = 'appLaunchURL is required when associatedStoreIdentifiers is present';
45
    const IMAGE_TYPE_INVALID = 'image files must be PNG format';
46
    const GROUPING_IDENTITY_INVALID = 'the grouping identity may only be used on boarding pass and event ticket types';
47
48
    /**
49
     * {@inheritdoc}
50
     */
51
    public function validate(PassInterface $pass)
52
    {
53
        $this->errors = array();
54
55
        $this->validateRequiredFields($pass);
56
        $this->validateBeaconKeys($pass);
57
        $this->validateLocationKeys($pass);
58
        $this->validateBarcodeKeys($pass);
59
        $this->validateWebServiceKeys($pass);
60
        $this->validateIcon($pass);
61
        $this->validateImageType($pass);
62
        $this->validateAssociatedStoreIdentifiers($pass);
63
        $this->validateGroupingIdentity($pass);
64
65
        return count($this->errors) === 0;
66
    }
67
68
    /**
69
     * {@inheritdoc}
70
     */
71
    public function getErrors()
72
    {
73
        return $this->errors;
74
    }
75
76
    private function validateRequiredFields(PassInterface $pass)
77
    {
78
        if ($this->isBlankOrNull($pass->getDescription())) {
79
            $this->addError(self::DESCRIPTION_REQUIRED);
80
        }
81
82
        if ($pass->getFormatVersion() !== 1) {
83
            $this->addError(self::FORMAT_VERSION_REQUIRED);
84
        }
85
86
        if ($this->isBlankOrNull($pass->getOrganizationName())) {
87
            $this->addError(self::ORGANIZATION_NAME_REQUIRED);
88
        }
89
90
        if ($this->isBlankOrNull($pass->getPassTypeIdentifier())) {
91
            $this->addError(self::PASS_TYPE_IDENTIFIER_REQUIRED);
92
        }
93
94
        if ($this->isBlankOrNull($pass->getSerialNumber())) {
95
            $this->addError(self::SERIAL_NUMBER_REQUIRED);
96
        }
97
98
        if ($this->isBlankOrNull($pass->getTeamIdentifier())) {
99
            $this->addError(self::TEAM_IDENTIFIER_REQUIRED);
100
        }
101
    }
102
103
    private function validateBeaconKeys(PassInterface $pass)
104
    {
105
        $beacons = $pass->getBeacons();
106
107
        foreach ($beacons as $beacon) {
108
            $this->validateBeacon($beacon);
109
        }
110
    }
111
112
    private function validateBeacon(Beacon $beacon)
113
    {
114
        if ($this->isBlankOrNull($beacon->getProximityUUID())) {
115
            $this->addError(self::BEACON_PROXIMITY_UUID_REQUIRED);
116
        }
117
118
        if (null !== $beacon->getMajor()) {
119
            if (!is_int($beacon->getMajor()) || $beacon->getMajor() < 0 || $beacon->getMajor() > 65535) {
120
                $this->addError(self::BEACON_MAJOR_INVALID);
121
            }
122
        }
123
124
        if (null !== $beacon->getMinor()) {
125
            if (!is_int($beacon->getMinor()) || $beacon->getMinor() < 0 || $beacon->getMinor() > 65535) {
126
                $this->addError(self::BEACON_MINOR_INVALID);
127
            }
128
        }
129
    }
130
131
    private function validateLocationKeys(PassInterface $pass)
132
    {
133
        $locations = $pass->getLocations();
134
135
        foreach ($locations as $location) {
136
            $this->validateLocation($location);
137
        }
138
    }
139
140
    private function validateLocation(Location $location)
141
    {
142
        if ($this->isBlankOrNull($location->getLatitude())) {
143
            $this->addError(self::LOCATION_LATITUDE_REQUIRED);
144
        }
145
146
        if (!is_numeric($location->getLatitude())) {
147
            $this->addError(self::LOCATION_LATITUDE_INVALID);
148
        }
149
150
        if ($this->isBlankOrNull($location->getLongitude())) {
151
            $this->addError(self::LOCATION_LONGITUDE_REQUIRED);
152
        }
153
154
        if (!is_numeric($location->getLongitude())) {
155
            $this->addError(self::LOCATION_LONGITUDE_INVALID);
156
        }
157
158
        if (!is_numeric($location->getAltitude()) && null !== $location->getAltitude()) {
159
            $this->addError(self::LOCATION_ALTITUDE_INVALID);
160
        }
161
    }
162
163
    private function validateBarcodeKeys(PassInterface $pass)
164
    {
165
        $validBarcodeFormats = array(Barcode::TYPE_QR, Barcode::TYPE_AZTEC, Barcode::TYPE_PDF_417, Barcode::TYPE_CODE_128);
166
167
        $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...
168
169
        if (!$barcode) {
170
            return;
171
        }
172
173
        if (!in_array($barcode->getFormat(), $validBarcodeFormats)) {
174
            $this->addError(self::BARCODE_FORMAT_INVALID);
175
        }
176
177
        if (!is_string($barcode->getMessage())) {
178
            $this->addError(self::BARCODE_MESSAGE_INVALID);
179
        }
180
    }
181
182
    private function validateWebServiceKeys(PassInterface $pass)
183
    {
184
        if (null === $pass->getWebServiceURL()) {
185
            return;
186
        }
187
188
        if (strpos($pass->getWebServiceURL(), 'http') !== 0) {
189
            $this->addError(self::WEB_SERVICE_URL_INVALID);
190
        }
191
192
        if ($this->isBlankOrNull($pass->getAuthenticationToken())) {
193
            $this->addError(self::WEB_SERVICE_AUTHENTICATION_TOKEN_REQUIRED);
194
        }
195
196
        if (strlen($pass->getAuthenticationToken()) < 16) {
197
            $this->addError(self::WEB_SERVICE_AUTHENTICATION_TOKEN_INVALID);
198
        }
199
    }
200
201
    private function validateIcon(PassInterface $pass)
202
    {
203
        foreach ($pass->getImages() as $image) {
204
            if ($image->getContext() === 'icon') {
205
                return;
206
            }
207
        }
208
209
        $this->addError(self::ICON_REQUIRED);
210
    }
211
212
    private function validateImageType(PassInterface $pass)
213
    {
214
        foreach ($pass->getImages() as $image) {
215
            $ext = pathinfo($image->getFilename(), PATHINFO_EXTENSION);
216
            if (strcasecmp('png', $ext)) {
217
                $this->addError(self::IMAGE_TYPE_INVALID);
218
            }
219
        }
220
    }
221
    
222
    private function validateAssociatedStoreIdentifiers(PassInterface $pass)
223
    {
224
        //appLaunchURL
225
226
        $associatedStoreIdentifiers = $pass->getAssociatedStoreIdentifiers();
227
228
        if (null !== $pass->getAppLaunchURL() && count($associatedStoreIdentifiers) == 0) {
229
            $this->addError(self::ASSOCIATED_STORE_IDENTIFIER_REQUIRED);
230
        }
231
232
233
        foreach ($associatedStoreIdentifiers as $associatedStoreIdentifier) {
234
            if (!is_int($associatedStoreIdentifier)) {
235
                $this->addError(self::ASSOCIATED_STORE_IDENTIFIER_INVALID);
236
237
                return;
238
            }
239
        }
240
    }
241
242
    private function validateGroupingIdentity(PassInterface $pass)
243
    {
244
        if (null !== $pass->getType() && !in_array($pass->getType(), ['boardingPass', 'eventTicket'])) {
245
            $this->addError(self::GROUPING_IDENTITY_INVALID);
246
247
            return;
248
        }
249
    }
250
251
    private function isBlankOrNull($text)
252
    {
253
        return '' === $text || null === $text;
254
    }
255
256
    private function addError($string)
257
    {
258
        $this->errors[] = $string;
259
    }
260
261
}
262