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 | public const DESCRIPTION_REQUIRED = 'description is required and cannot be blank'; |
||||
25 | public const FORMAT_VERSION_REQUIRED = 'formatVersion is required and must be 1'; |
||||
26 | public const ORGANIZATION_NAME_REQUIRED = 'organizationName is required and cannot be blank'; |
||||
27 | public const PASS_TYPE_IDENTIFIER_REQUIRED = 'passTypeIdentifier is required and cannot be blank'; |
||||
28 | public const SERIAL_NUMBER_REQUIRED = 'serialNumber is required and cannot be blank'; |
||||
29 | public const TEAM_IDENTIFIER_REQUIRED = 'teamIdentifier is required and cannot be blank'; |
||||
30 | public const ICON_REQUIRED = 'pass must have an icon image'; |
||||
31 | public const BARCODE_FORMAT_INVALID = 'barcode format is invalid'; |
||||
32 | public const BARCODE_MESSAGE_INVALID = 'barcode message is invalid; must be a string'; |
||||
33 | public const LOCATION_LATITUDE_REQUIRED = 'location latitude is required'; |
||||
34 | public const LOCATION_LONGITUDE_REQUIRED = 'location longitude is required'; |
||||
35 | public const LOCATION_LATITUDE_INVALID = 'location latitude is invalid; must be numeric'; |
||||
36 | public const LOCATION_LONGITUDE_INVALID = 'location longitude is invalid; must be numeric'; |
||||
37 | public const LOCATION_ALTITUDE_INVALID = 'location altitude is invalid; must be numeric'; |
||||
38 | public const BEACON_PROXIMITY_UUID_REQUIRED = 'beacon proximityUUID is required'; |
||||
39 | public const BEACON_MAJOR_INVALID = 'beacon major is invalid; must be 16-bit unsigned integer'; |
||||
40 | public const BEACON_MINOR_INVALID = 'beacon minor is invalid; must be 16-bit unsigned integer'; |
||||
41 | public const NFC_MESSAGE_REQUIRED = 'NFC message is required'; |
||||
42 | public const NFC_ENCRYPTION_PUBLIC_KEY_REQUIRED = 'NFC encryption public key is required'; |
||||
43 | public const WEB_SERVICE_URL_INVALID = 'webServiceURL is invalid; must start with https (or http for development)'; |
||||
44 | public const WEB_SERVICE_AUTHENTICATION_TOKEN_REQUIRED = 'authenticationToken required with webServiceURL and cannot be blank'; |
||||
45 | public const WEB_SERVICE_AUTHENTICATION_TOKEN_INVALID = 'authenticationToken is invalid; must be at least 16 characters'; |
||||
46 | public const ASSOCIATED_STORE_IDENTIFIER_INVALID = 'associatedStoreIdentifiers is invalid; must be an integer'; |
||||
47 | public const ASSOCIATED_STORE_IDENTIFIER_REQUIRED = 'appLaunchURL is required when associatedStoreIdentifiers is present'; |
||||
48 | public const IMAGE_TYPE_INVALID = 'image files must be PNG format'; |
||||
49 | public const GROUPING_IDENTITY_INVALID = 'the grouping identity may only be used on boarding pass and event ticket types'; |
||||
50 | |||||
51 | /** |
||||
52 | * {@inheritdoc} |
||||
53 | */ |
||||
54 | 19 | public function validate(PassInterface $pass) |
|||
55 | { |
||||
56 | 19 | $this->errors = []; |
|||
57 | |||||
58 | 19 | $this->validateRequiredFields($pass); |
|||
59 | 19 | $this->validateBeaconKeys($pass); |
|||
60 | 19 | $this->validateNfcKeys($pass); |
|||
61 | 19 | $this->validateLocationKeys($pass); |
|||
62 | 19 | $this->validateBarcodeKeys($pass); |
|||
63 | 19 | $this->validateWebServiceKeys($pass); |
|||
64 | 19 | $this->validateIcon($pass); |
|||
65 | 19 | $this->validateImageType($pass); |
|||
66 | 19 | $this->validateAssociatedStoreIdentifiers($pass); |
|||
67 | 19 | $this->validateGroupingIdentity($pass); |
|||
68 | |||||
69 | 19 | return count($this->errors) === 0; |
|||
70 | } |
||||
71 | |||||
72 | /** |
||||
73 | * {@inheritdoc} |
||||
74 | */ |
||||
75 | 16 | public function getErrors() |
|||
76 | { |
||||
77 | 16 | return $this->errors; |
|||
78 | } |
||||
79 | |||||
80 | 19 | private function validateRequiredFields(PassInterface $pass) |
|||
81 | { |
||||
82 | 19 | if ($this->isBlankOrNull($pass->getDescription())) { |
|||
83 | 1 | $this->addError(self::DESCRIPTION_REQUIRED); |
|||
84 | } |
||||
85 | |||||
86 | 19 | if ($pass->getFormatVersion() !== 1) { |
|||
87 | 1 | $this->addError(self::FORMAT_VERSION_REQUIRED); |
|||
88 | } |
||||
89 | |||||
90 | 19 | if ($this->isBlankOrNull($pass->getOrganizationName())) { |
|||
91 | 15 | $this->addError(self::ORGANIZATION_NAME_REQUIRED); |
|||
92 | } |
||||
93 | |||||
94 | 19 | if ($this->isBlankOrNull($pass->getPassTypeIdentifier())) { |
|||
95 | 15 | $this->addError(self::PASS_TYPE_IDENTIFIER_REQUIRED); |
|||
96 | } |
||||
97 | |||||
98 | 19 | if ($this->isBlankOrNull($pass->getSerialNumber())) { |
|||
99 | 1 | $this->addError(self::SERIAL_NUMBER_REQUIRED); |
|||
100 | } |
||||
101 | |||||
102 | 19 | if ($this->isBlankOrNull($pass->getTeamIdentifier())) { |
|||
103 | 15 | $this->addError(self::TEAM_IDENTIFIER_REQUIRED); |
|||
104 | } |
||||
105 | } |
||||
106 | |||||
107 | 19 | private function validateBeaconKeys(PassInterface $pass) |
|||
108 | { |
||||
109 | 19 | $beacons = $pass->getBeacons(); |
|||
110 | |||||
111 | 19 | foreach ($beacons as $beacon) { |
|||
112 | 1 | $this->validateBeacon($beacon); |
|||
113 | } |
||||
114 | } |
||||
115 | |||||
116 | 19 | private function validateNfcKeys(PassInterface $pass) |
|||
117 | { |
||||
118 | 19 | $nfc = $pass->getNfc(); |
|||
119 | 19 | if (!$nfc) { |
|||
0 ignored issues
–
show
introduced
by
![]() |
|||||
120 | 19 | return; |
|||
121 | } |
||||
122 | $this->validateNfc($nfc); |
||||
123 | } |
||||
124 | |||||
125 | 1 | private function validateBeacon(Beacon $beacon) |
|||
126 | { |
||||
127 | 1 | if ($this->isBlankOrNull($beacon->getProximityUUID())) { |
|||
128 | $this->addError(self::BEACON_PROXIMITY_UUID_REQUIRED); |
||||
129 | } |
||||
130 | |||||
131 | 1 | if (null !== $beacon->getMajor()) { |
|||
0 ignored issues
–
show
|
|||||
132 | 1 | if (!is_int($beacon->getMajor()) || $beacon->getMajor() < 0 || $beacon->getMajor() > 65535) { |
|||
0 ignored issues
–
show
|
|||||
133 | 1 | $this->addError(self::BEACON_MAJOR_INVALID); |
|||
134 | } |
||||
135 | } |
||||
136 | |||||
137 | 1 | if (null !== $beacon->getMinor()) { |
|||
0 ignored issues
–
show
|
|||||
138 | 1 | if (!is_int($beacon->getMinor()) || $beacon->getMinor() < 0 || $beacon->getMinor() > 65535) { |
|||
0 ignored issues
–
show
|
|||||
139 | 1 | $this->addError(self::BEACON_MINOR_INVALID); |
|||
140 | } |
||||
141 | } |
||||
142 | } |
||||
143 | |||||
144 | private function validateNfc(Nfc $nfc) |
||||
145 | { |
||||
146 | if ($this->isBlankOrNull($nfc->getMessage())) { |
||||
147 | $this->addError(self::NFC_MESSAGE_REQUIRED); |
||||
148 | } |
||||
149 | |||||
150 | if ($this->isBlankOrNull($nfc->getEncryptionPublicKey())) { |
||||
151 | $this->addError(self::NFC_ENCRYPTION_PUBLIC_KEY_REQUIRED); |
||||
152 | } |
||||
153 | } |
||||
154 | |||||
155 | 19 | private function validateLocationKeys(PassInterface $pass) |
|||
156 | { |
||||
157 | 19 | $locations = $pass->getLocations(); |
|||
158 | |||||
159 | 19 | foreach ($locations as $location) { |
|||
160 | 1 | $this->validateLocation($location); |
|||
161 | } |
||||
162 | } |
||||
163 | |||||
164 | 1 | private function validateLocation(Location $location) |
|||
165 | { |
||||
166 | 1 | if ($this->isBlankOrNull($location->getLatitude())) { |
|||
167 | 1 | $this->addError(self::LOCATION_LATITUDE_REQUIRED); |
|||
168 | } |
||||
169 | |||||
170 | 1 | if (!is_numeric($location->getLatitude())) { |
|||
0 ignored issues
–
show
|
|||||
171 | 1 | $this->addError(self::LOCATION_LATITUDE_INVALID); |
|||
172 | } |
||||
173 | |||||
174 | 1 | if ($this->isBlankOrNull($location->getLongitude())) { |
|||
175 | 1 | $this->addError(self::LOCATION_LONGITUDE_REQUIRED); |
|||
176 | } |
||||
177 | |||||
178 | 1 | if (!is_numeric($location->getLongitude())) { |
|||
0 ignored issues
–
show
|
|||||
179 | 1 | $this->addError(self::LOCATION_LONGITUDE_INVALID); |
|||
180 | } |
||||
181 | |||||
182 | 1 | if (!is_numeric($location->getAltitude()) && null !== $location->getAltitude()) { |
|||
0 ignored issues
–
show
|
|||||
183 | 1 | $this->addError(self::LOCATION_ALTITUDE_INVALID); |
|||
184 | } |
||||
185 | } |
||||
186 | |||||
187 | 19 | private function validateBarcodeKeys(PassInterface $pass) |
|||
188 | { |
||||
189 | 19 | $validBarcodeFormats = [Barcode::TYPE_QR, Barcode::TYPE_AZTEC, Barcode::TYPE_PDF_417, Barcode::TYPE_CODE_128]; |
|||
190 | |||||
191 | 19 | $barcode = $pass->getBarcode(); |
|||
0 ignored issues
–
show
The function
Passbook\PassInterface::getBarcode() has been deprecated: use getBarcodes instead
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This function has been deprecated. The supplier of the function has supplied an explanatory message. The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead. ![]() |
|||||
192 | |||||
193 | 19 | if (!$barcode) { |
|||
0 ignored issues
–
show
|
|||||
194 | 18 | return; |
|||
195 | } |
||||
196 | |||||
197 | 3 | if (!in_array($barcode->getFormat(), $validBarcodeFormats)) { |
|||
198 | 2 | $this->addError(self::BARCODE_FORMAT_INVALID); |
|||
199 | } |
||||
200 | |||||
201 | 3 | if (!is_string($barcode->getMessage())) { |
|||
0 ignored issues
–
show
|
|||||
202 | $this->addError(self::BARCODE_MESSAGE_INVALID); |
||||
203 | } |
||||
204 | } |
||||
205 | |||||
206 | 19 | private function validateWebServiceKeys(PassInterface $pass) |
|||
207 | { |
||||
208 | 19 | if (null === $pass->getWebServiceURL()) { |
|||
209 | 19 | return; |
|||
210 | } |
||||
211 | |||||
212 | 1 | if (strpos($pass->getWebServiceURL(), 'http') !== 0) { |
|||
213 | 1 | $this->addError(self::WEB_SERVICE_URL_INVALID); |
|||
214 | } |
||||
215 | |||||
216 | 1 | if ($this->isBlankOrNull($pass->getAuthenticationToken())) { |
|||
217 | 1 | $this->addError(self::WEB_SERVICE_AUTHENTICATION_TOKEN_REQUIRED); |
|||
218 | } |
||||
219 | |||||
220 | 1 | if (strlen($pass->getAuthenticationToken()) < 16) { |
|||
221 | 1 | $this->addError(self::WEB_SERVICE_AUTHENTICATION_TOKEN_INVALID); |
|||
222 | } |
||||
223 | } |
||||
224 | |||||
225 | 19 | private function validateIcon(PassInterface $pass) |
|||
226 | { |
||||
227 | 19 | foreach ($pass->getImages() as $image) { |
|||
228 | 5 | if ($image->getContext() === 'icon') { |
|||
229 | 5 | return; |
|||
230 | } |
||||
231 | } |
||||
232 | |||||
233 | 16 | $this->addError(self::ICON_REQUIRED); |
|||
234 | } |
||||
235 | |||||
236 | 19 | private function validateImageType(PassInterface $pass) |
|||
237 | { |
||||
238 | 19 | foreach ($pass->getImages() as $image) { |
|||
239 | 5 | if ('png' !== strtolower($image->getExtension())) { |
|||
240 | 1 | $this->addError(self::IMAGE_TYPE_INVALID); |
|||
241 | } |
||||
242 | } |
||||
243 | } |
||||
244 | |||||
245 | 19 | private function validateAssociatedStoreIdentifiers(PassInterface $pass) |
|||
246 | { |
||||
247 | //appLaunchURL |
||||
248 | |||||
249 | 19 | $associatedStoreIdentifiers = $pass->getAssociatedStoreIdentifiers(); |
|||
250 | |||||
251 | 19 | if (null !== $pass->getAppLaunchURL() && count($associatedStoreIdentifiers) == 0) { |
|||
252 | 1 | $this->addError(self::ASSOCIATED_STORE_IDENTIFIER_REQUIRED); |
|||
253 | } |
||||
254 | |||||
255 | |||||
256 | 19 | foreach ($associatedStoreIdentifiers as $associatedStoreIdentifier) { |
|||
257 | 1 | if (!is_int($associatedStoreIdentifier)) { |
|||
258 | 1 | $this->addError(self::ASSOCIATED_STORE_IDENTIFIER_INVALID); |
|||
259 | |||||
260 | 1 | return; |
|||
261 | } |
||||
262 | } |
||||
263 | } |
||||
264 | |||||
265 | 19 | private function validateGroupingIdentity(PassInterface $pass) |
|||
266 | { |
||||
267 | 19 | if (null !== $pass->getGroupingIdentifier() && !in_array($pass->getType(), ['boardingPass', 'eventTicket'])) { |
|||
268 | 1 | $this->addError(self::GROUPING_IDENTITY_INVALID); |
|||
269 | |||||
270 | 1 | return; |
|||
271 | } |
||||
272 | } |
||||
273 | |||||
274 | 19 | private function isBlankOrNull($text) |
|||
275 | { |
||||
276 | 19 | return '' === $text || null === $text; |
|||
277 | } |
||||
278 | |||||
279 | 16 | private function addError($string) |
|||
280 | { |
||||
281 | 16 | $this->errors[] = $string; |
|||
282 | } |
||||
283 | } |
||||
284 |