PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.15
VikBooking Hotel Booking Engine & PMS v1.8.15
1.8.15 1.8.14 1.8.13 1.8.12 1.8.11 1.8.10 1.8.9 1.8.6 1.8.7 1.8.8 trunk 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 1.7.3 All 36 releases
vikbooking / admin / helpers / src / checkin / paxfields / mrz / map.php

map.php in VikBooking Hotel Booking Engine & PMS 1.8.15, at admin/helpers/src/checkin/paxfields/mrz/map.php

685 lines 19.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package VikBooking
4 * @subpackage core
5 * @author Alessio Gaggii - E4J s.r.l.
6 * @copyright Copyright (C) 2026 E4J s.r.l. All Rights Reserved.
7 * @license http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL
8 * @link https://vikwp.com
9 */
10
11 // No direct access
12 defined('ABSPATH') or die('No script kiddies please!');
13
14 /**
15 * Defines an abstract implementation for pax fields MRZ mapping.
16 *
17 * @since 1.18.6 (J) - 1.8.6 (WP)
18 */
19 abstract class VBOCheckinPaxfieldsMrzMap
20 {
21 /**
22 * @var string
23 */
24 protected string $collector_id = '';
25
26 /**
27 * @var array
28 */
29 protected array $fieldsList = [];
30
31 /**
32 * @var array
33 */
34 protected array $fieldsMap = [];
35
36 /**
37 * @var array
38 */
39 protected array $mrzProperties = [];
40
41 /**
42 * @var bool
43 */
44 protected bool $checkDigitVerified = false;
45
46 /**
47 * @var array
48 */
49 protected array $propertyList = [
50 'documentCode',
51 'documentType',
52 'issuer',
53 'documentNumber',
54 'dateOfBirth',
55 'sex',
56 'dateOfExpiry',
57 'nationality',
58 'lastName',
59 'firstName',
60 ];
61
62 /**
63 * Class constructor will bind collector and pax fields.
64 *
65 * @param array $fields Default pax fields.
66 */
67 public function __construct(string $collector, array $fields)
68 {
69 // bind collector ID
70 $this->collector_id = $collector ?: preg_replace('/^VBOCheckinPaxfieldsMrzMap/i', '', strtolower(get_class($this)));
71
72 // bind default check-in fields for data collector
73 $this->setPaxFields($fields);
74 }
75
76 /**
77 * Given a list of MRZ detected properties, obtains the assoc list of
78 * known and supported field identifiers within the data collector.
79 *
80 * @param array $propertyList List of MRZ detected properties.
81 *
82 * @return array Associative list of known and supported properties.
83 */
84 abstract public function getKnownFields(array $propertyList);
85
86 /**
87 * Sets current check-in fields for data collector.
88 *
89 * @param array $fields Default pax fields.
90 *
91 * @return self
92 *
93 * @throws Exception
94 */
95 public function setPaxFields(array $fields)
96 {
97 // ensure the fields list contains labels and attributes
98 if (count($fields) !== 2 || !is_array($fields[0] ?? null) || !is_array($fields[1] ?? null)) {
99 throw new InvalidArgumentException('Invalid check-in pax fields provided.', 400);
100 }
101
102 // bind current check-in fields list for data collector
103 $this->fieldsList = $fields;
104
105 return $this;
106 }
107
108 /**
109 * Tells whether the MRZ check-digit validation was verified.
110 *
111 * @return bool
112 */
113 public function isVerified()
114 {
115 return $this->checkDigitVerified;
116 }
117
118 /**
119 * Sets whether the MRZ check-digit validation was verified.
120 *
121 * @param bool $verified True if check-digit was verified.
122 *
123 * @return self
124 */
125 public function setVerified(bool $verified)
126 {
127 $this->checkDigitVerified = $verified;
128
129 return $this;
130 }
131
132 /**
133 * Returns the current MRZ properties detected from the ID document.
134 *
135 * @return array
136 */
137 public function getMrzProperties()
138 {
139 return $this->mrzProperties;
140 }
141
142 /**
143 * Binds the MRZ properties detected from the ID document.
144 *
145 * @param array $properties The raw MRZ detected properties.
146 *
147 * @return self
148 */
149 public function setMrzProperties(array $properties)
150 {
151 $this->mrzProperties = $properties;
152
153 return $this;
154 }
155
156 /**
157 * Returns the requested MRZ propery from raw data detected.
158 *
159 * @param string $type The raw property type identifier.
160 *
161 * @return ?string
162 */
163 public function getRawMrzProperty(string $type)
164 {
165 return $this->mrzProperties[$type] ?? null;
166 }
167
168 /**
169 * Returns the current collector identifier.
170 *
171 * @return string
172 */
173 public function getCollector()
174 {
175 return $this->collector_id;
176 }
177
178 /**
179 * Returns the list of mapped MRZ properties to fields.
180 *
181 * @return array
182 */
183 public function getMappedFields()
184 {
185 return $this->fieldsMap;
186 }
187
188 /**
189 * Given an associative list of MRZ properties detected from an ID document,
190 * maps the values according to the pax data collector supported fields.
191 *
192 * @param array $properties Associative list of raw MRZ properties from ID document.
193 *
194 * @return self
195 */
196 public function mapDetectedProperties(array $properties)
197 {
198 // bind the raw MRZ detected properties from ID document
199 $this->setMrzProperties($properties);
200
201 // obtain the associative list of known and supported properties
202 $knownProperties = $this->getKnownFields(array_intersect(array_keys($properties), $this->propertyList));
203
204 // iterate all property types and values
205 foreach ($properties as $type => $value) {
206 // ensure the value is supported
207 if (is_null($value) || !is_scalar($value)) {
208 // unsupported field value
209 continue;
210 }
211
212 // ensure the value is not empty
213 $value = (string) $value;
214 if ($value === '') {
215 // ignore empty values
216 continue;
217 }
218
219 if (!($knownProperties[$type] ?? null)) {
220 // unsupported property by data collector
221 continue;
222 }
223
224 // check property type to map
225 switch ($type) {
226 case 'documentCode':
227 // map value
228 $this->mapProperty(
229 (array) $knownProperties[$type],
230 $this->mapDocumentCode($value)
231 );
232 break;
233
234 case 'documentType':
235 // map value
236 $this->mapProperty(
237 (array) $knownProperties[$type],
238 $this->mapDocumentType($value)
239 );
240 break;
241
242 case 'issuer':
243 // map value
244 $this->mapProperty(
245 (array) $knownProperties[$type],
246 $this->mapDocumentIssuer($this->convertCountryValue($value))
247 );
248 break;
249
250 case 'documentNumber':
251 // map value
252 $this->mapProperty(
253 (array) $knownProperties[$type],
254 $this->mapDocumentNumber($value)
255 );
256 break;
257
258 case 'dateOfBirth':
259 // map value
260 $this->mapProperty(
261 (array) $knownProperties[$type],
262 // ensure the argument is passed in Y-m-d format
263 $this->mapDateOfBirth($this->convertDateOfBirth($value))
264 );
265 break;
266
267 case 'sex':
268 // map value
269 $this->mapProperty(
270 (array) $knownProperties[$type],
271 $this->mapGender($value)
272 );
273 break;
274
275 case 'dateOfExpiry':
276 // map value
277 $this->mapProperty(
278 (array) $knownProperties[$type],
279 // ensure the argument is passed in Y-m-d format
280 $this->mapDateOfExpiry($this->convertDateOfExpiry($value))
281 );
282 break;
283
284 case 'nationality':
285 // map value
286 $this->mapProperty(
287 (array) $knownProperties[$type],
288 $this->mapNationality($this->convertCountryValue($value))
289 );
290 break;
291
292 case 'lastName':
293 // map value
294 $this->mapProperty(
295 (array) $knownProperties[$type],
296 $this->mapLastName($value)
297 );
298 break;
299
300 case 'firstName':
301 // map value
302 $this->mapProperty(
303 (array) $knownProperties[$type],
304 $this->mapFirstName($value)
305 );
306 break;
307
308 default:
309 // nothing to map
310 break;
311 }
312 }
313
314 return $this;
315 }
316
317 /**
318 * Returns the associative list of pax field labels.
319 *
320 * @return array Pax fields associative labels.
321 */
322 protected function getLabels()
323 {
324 return $this->fieldsList[0];
325 }
326
327 /**
328 * Returns the associative list of pax field attributes.
329 *
330 * @return array Pax fields associative attributes.
331 */
332 protected function getAttributes()
333 {
334 return $this->fieldsList[1];
335 }
336
337 /**
338 * Helper method to allow the MRZ pax fields map implementor
339 * to call methods declared by the data collection driver.
340 *
341 * @param string $method The method to call from the collector.
342 *
343 * @return mixed
344 */
345 protected function callCollector($method)
346 {
347 // access the collector class
348 $collector = VBOCheckinPax::getInstance($this->getCollector());
349
350 if (!$collector || empty($method) || !is_callable([$collector, $method])) {
351 return null;
352 }
353
354 // build extra arguments, if any
355 $args = func_get_args();
356 unset($args[0]);
357
358 // invoke the collector's method
359 return call_user_func_array([$collector, $method], $args);
360 }
361
362 /**
363 * The MRZ standard includes dates in "ymd" format.
364 * For easier handling, we convert it to "Y-m-d" format.
365 *
366 * @param string $ymd The date string in "ymd" format.
367 *
368 * @return string The converted date in "Y-m-d" format, or empty string.
369 */
370 protected function convertDateOfBirth(string $ymd)
371 {
372 if (!preg_match('/^[0-9]{2}(0[1-9]|1[0-2])(0[1-9]|[12][0-9]|3[01])$/', $ymd)) {
373 // invalid date, expected format is "ymd"
374 return '';
375 }
376
377 // convert "ymd" into "y-m-d" by adding a dash every two characters
378 $ymd = implode('-', str_split($ymd, 2));
379
380 // first two digits of current year
381 $yearFirstDigits = (int) substr(date('Y'), 0, 2);
382
383 // prepend first two digits of current year to date
384 $fullDate = $yearFirstDigits . $ymd;
385
386 if (strtotime($fullDate) > time()) {
387 // the date of birth cannot be in the future
388
389 // decrease by one century
390 $yearFirstDigits -= 1;
391
392 // prepend first two digits of current year to date
393 $fullDate = $yearFirstDigits . $ymd;
394 }
395
396 return $fullDate;
397 }
398
399 /**
400 * The MRZ standard includes dates in "ymd" format.
401 * For easier handling, we convert it to "Y-m-d" format.
402 *
403 * @param string $ymd The date string in "ymd" format.
404 *
405 * @return string The converted date in "Y-m-d" format, or empty string.
406 */
407 protected function convertDateOfExpiry(string $ymd)
408 {
409 if (!preg_match('/^[0-9]{2}(0[1-9]|1[0-2])(0[1-9]|[12][0-9]|3[01])$/', $ymd)) {
410 // invalid date, expected format is "ymd"
411 return '';
412 }
413
414 // convert "ymd" into "y-m-d" by adding a dash every two characters
415 $ymd = implode('-', str_split($ymd, 2));
416
417 // first two digits of current year
418 $yearFirstDigits = (int) substr(date('Y'), 0, 2);
419
420 // first two digits of last century
421 $lastCenturyYear = $yearFirstDigits - 1;
422
423 // first two digits of next century
424 $nextCenturyYear = $yearFirstDigits + 1;
425
426 // prepend first two digits of current year to date
427 $fullDateCurrent = $yearFirstDigits . $ymd;
428
429 // prepend first two digits of last century year to date
430 $fullDateLast = $lastCenturyYear . $ymd;
431
432 // prepend first two digits of next century year to date
433 $fullDateNext = $nextCenturyYear . $ymd;
434
435 // construct date-timezone object
436 $tz = new DateTimeZone(date_default_timezone_get());
437
438 // construct nowadays date-time object
439 $nowadays = new DateTime('now', $tz);
440
441 // construct date-time object with current year full date
442 $currentDt = new DateTime($fullDateCurrent, $tz);
443
444 // construct date-time object with last century full date
445 $lastCenturyDt = new DateTime($fullDateLast, $tz);
446
447 // construct date-time object with next century full date
448 $nextCenturyDt = new DateTime($fullDateNext, $tz);
449
450 // check what date is closest to nowadays to determine the best expiration year
451 $yearDistances = [
452 'current' => (int) $nowadays->diff($currentDt)->y,
453 'last' => (int) $nowadays->diff($lastCenturyDt)->y,
454 'next' => (int) $nowadays->diff($nextCenturyDt)->y,
455 ];
456
457 // sort in ascending order
458 asort($yearDistances);
459
460 // get closest expiration date to nowadays
461 $closestDateType = key($yearDistances);
462
463 if ($closestDateType === 'next') {
464 return $fullDateNext;
465 }
466
467 if ($closestDateType === 'last') {
468 return $fullDateLast;
469 }
470
471 return $fullDateCurrent;
472 }
473
474 /**
475 * Given the country code identifier extracted from the document, checks if
476 * the value is known to be normalized to an existing country ISO code.
477 *
478 * @param string $countryCode The raw country code read from MRZ.
479 *
480 * @return string The normalized (or same) country code.
481 */
482 protected function convertCountryValue(string $countryCode)
483 {
484 if ($countryCode === 'D<<') {
485 // normaly country code for Germany
486 return 'DEU';
487 }
488
489 if ($countryCode === 'GB<') {
490 // normaly country code for UK
491 return 'GBR';
492 }
493
494 if ($countryCode === 'ZIM') {
495 // normaly country code for Zimbabwe
496 return 'ZWE';
497 }
498
499 if ($countryCode === 'RKS') {
500 // normaly country code for Kosovo (2-char)
501 return 'KV';
502 }
503
504 return $countryCode;
505 }
506
507 /**
508 * Maps the value of an ID document property to the list
509 * of known and supported pax fields of that type.
510 *
511 * @param array $fieldIds List of pax field identifiers.
512 * @param string $value The normalized value for the field(s).
513 *
514 * @return self
515 */
516 protected function mapProperty(array $fieldIds, string $value)
517 {
518 foreach (array_filter($fieldIds) as $fieldId) {
519 if (!is_string($fieldId)) {
520 // unexpected pax field identifier
521 continue;
522 }
523
524 if ($value === '') {
525 // skip empty values
526 continue;
527 }
528
529 // push field property data
530 $this->fieldsMap[] = [
531 'id' => $fieldId,
532 'value' => $value,
533 ];
534 }
535
536 return $this;
537 }
538
539 /**
540 * Maps the MRZ value for "document code" ("P", "I", "A", "C", "V").
541 *
542 * @param string $value The value to normalize.
543 *
544 * @return string The normalized value.
545 */
546 protected function mapDocumentCode(string $value)
547 {
548 return str_replace('<', '', $value);
549 }
550
551 /**
552 * Maps the MRZ value for "document type" (document variant, often "<").
553 *
554 * @param string $value The value to normalize.
555 *
556 * @return string The normalized value.
557 */
558 protected function mapDocumentType(string $value)
559 {
560 return $value;
561 }
562
563 /**
564 * Maps the MRZ value for "document issuer" (country code).
565 *
566 * @param string $value The value to normalize.
567 *
568 * @return string The normalized value.
569 */
570 protected function mapDocumentIssuer(string $value)
571 {
572 return strtoupper(str_replace('<', '', $value));
573 }
574
575 /**
576 * Maps the MRZ value for "document number".
577 *
578 * @param string $value The value to normalize.
579 *
580 * @return string The normalized value.
581 */
582 protected function mapDocumentNumber(string $value)
583 {
584 return str_replace('<', '', $value);
585 }
586
587 /**
588 * Maps the MRZ value for "date of birth" (converted in Y-m-d format).
589 *
590 * @param string $value The value to normalize (Y-m-d format).
591 *
592 * @return string The normalized value.
593 */
594 protected function mapDateOfBirth(string $value)
595 {
596 if (empty($value)) {
597 return '';
598 }
599
600 $nowdf = VikBooking::getDateFormat();
601 if ($nowdf == "%d/%m/%Y") {
602 $df = 'd/m/Y';
603 } elseif ($nowdf == "%m/%d/%Y") {
604 $df = 'm/d/Y';
605 } else {
606 $df = 'Y/m/d';
607 }
608
609 return date($df, strtotime($value));
610 }
611
612 /**
613 * Maps the MRZ value for "gender".
614 *
615 * @param string $value The value to normalize.
616 *
617 * @return string The normalized value.
618 */
619 protected function mapGender(string $value)
620 {
621 return strtoupper($value) === 'F' ? 'F' : 'M';
622 }
623
624 /**
625 * Maps the MRZ value for "document expiry date" (converted in Y-m-d format).
626 *
627 * @param string $value The value to normalize (Y-m-d format).
628 *
629 * @return string The normalized value.
630 */
631 protected function mapDateOfExpiry(string $value)
632 {
633 if (empty($value)) {
634 return '';
635 }
636
637 $nowdf = VikBooking::getDateFormat();
638 if ($nowdf == "%d/%m/%Y") {
639 $df = 'd/m/Y';
640 } elseif ($nowdf == "%m/%d/%Y") {
641 $df = 'm/d/Y';
642 } else {
643 $df = 'Y/m/d';
644 }
645
646 return date($df, strtotime($value));
647 }
648
649 /**
650 * Maps the MRZ value for "nationality" (country code).
651 *
652 * @param string $value The ISO country code value to normalize.
653 *
654 * @return string The normalized value.
655 */
656 protected function mapNationality(string $value)
657 {
658 return strtoupper(str_replace('<', '', $value));
659 }
660
661 /**
662 * Maps the MRZ value for "last name".
663 *
664 * @param string $value The value to normalize.
665 *
666 * @return string The normalized value.
667 */
668 protected function mapLastName(string $value)
669 {
670 return ucwords(trim(str_replace('<', ' ', strtolower($value))));
671 }
672
673 /**
674 * Maps the MRZ value for "first name".
675 *
676 * @param string $value The value to normalize.
677 *
678 * @return string The normalized value.
679 */
680 protected function mapFirstName(string $value)
681 {
682 return ucwords(trim(str_replace('<', ' ', strtolower($value))));
683 }
684 }
685