PluginProbe
VikBooking Hotel Booking Engine & PMS / trunk
VikBooking Hotel Booking Engine & PMS vtrunk
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 / dooraccess / integration / aware.php

aware.php in VikBooking Hotel Booking Engine & PMS trunk, at admin/helpers/src/dooraccess/integration/aware.php

802 lines 23.9 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 E4J s.r.l.
6 * @copyright Copyright (C) 2025 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 * Door Access integration abstract class of any provider.
16 *
17 * @since 1.18.4 (J) - 1.8.4 (WP)
18 */
19 abstract class VBODooraccessIntegrationAware
20 {
21 /**
22 * @var array
23 */
24 protected $record = [];
25
26 /**
27 * Proxy to construct the door access integration object.
28 *
29 * @return VBODooraccessIntegrationAware
30 */
31 public static function getInstance()
32 {
33 return new static;
34 }
35
36 /**
37 * Class constructor.
38 */
39 public function __construct()
40 {}
41
42 /**
43 * Returns the integration alias identifier.
44 *
45 * @return string
46 */
47 public function getAlias()
48 {
49 return preg_replace('/^VBODooraccessProvider/i', '', strtolower(get_class($this)));
50 }
51
52 /**
53 * Returns the integration name.
54 *
55 * @return string
56 */
57 abstract public function getName();
58
59 /**
60 * Returns the integration short name.
61 *
62 * @return string
63 */
64 public function getShortName()
65 {
66 // return the provider integration name by default
67 return $this->getName();
68 }
69
70 /**
71 * Returns the integration icon, either an image URL or an HTML icon.
72 *
73 * @return string
74 */
75 public function getIcon()
76 {
77 return '';
78 }
79
80 /**
81 * Tells if the integration can unlock devices for a booking.
82 *
83 * @return bool
84 */
85 public function canUnlockDevices()
86 {
87 // providers should eventually override this method
88 return false;
89 }
90
91 /**
92 * Tells if the integration can watch for device passcode first access.
93 *
94 * @return bool
95 */
96 public function canWatchFirstAccess()
97 {
98 // providers should eventually override this method
99 return false;
100 }
101
102 /**
103 * Tells if the integration can clean expired passcodes.
104 *
105 * @return bool
106 *
107 * @since 1.18.7 (J) - 1.8.7 (WP)
108 */
109 public function canCleanExpiredPasscodes()
110 {
111 // providers should eventually override this method
112 return true;
113 }
114
115 /**
116 * Returns the integration parameters.
117 *
118 * @return array
119 */
120 public function getParams()
121 {
122 return [];
123 }
124
125 /**
126 * Returns the current integration record (profile) settings.
127 *
128 * @return array
129 */
130 public function getSettings()
131 {
132 return (array) ($this->record['settings'] ?? []);
133 }
134
135 /**
136 * Returns the current integration record (profile) devices.
137 *
138 * @return VBODooraccessIntegrationDevice[] List of device objects.
139 */
140 public function getDevices()
141 {
142 return (array) ($this->record['devices'] ?? []);
143 }
144
145 /**
146 * Tells if a specific device ID exists in the current integration record.
147 *
148 * @param string $deviceId The device identifier value to find.
149 *
150 * @return bool
151 */
152 public function deviceExists(string $deviceId)
153 {
154 foreach ($this->getDevices() as $device) {
155 if ($device->getID() == $deviceId) {
156 return true;
157 }
158 }
159
160 return false;
161 }
162
163 /**
164 * Returns a specific device ID from the current integration record.
165 *
166 * @param string $deviceId The device identifier value to find.
167 *
168 * @return VBODooraccessIntegrationDevice
169 *
170 * @throws Exception
171 */
172 public function getDeviceById(string $deviceId)
173 {
174 foreach ($this->getDevices() as $device) {
175 if ($device->getID() == $deviceId) {
176 return $device;
177 }
178 }
179
180 throw new Exception(sprintf('Could not access the requested device ID: %s.', $deviceId), 404);
181 }
182
183 /**
184 * Returns the current integration record (profile) processed-data.
185 *
186 * @return array
187 */
188 public function getData()
189 {
190 return (array) ($this->record['data'] ?? []);
191 }
192
193 /**
194 * Tells if a booking ID was previously processed by the current integration record.
195 *
196 * @param int $bookingId The booking ID to evaluate.
197 *
198 * @return bool
199 */
200 public function getBookingAccessProcessed(int $bookingId)
201 {
202 return in_array($bookingId, (array) ($this->record['data']['bookings'] ?? []));
203 }
204
205 /**
206 * Pushes a booking ID as processed by the current integration record.
207 *
208 * @param int $bookingId The booking ID to set as processed.
209 *
210 * @return self
211 */
212 public function setBookingAccessProcessed(int $bookingId)
213 {
214 if (!isset($this->record['data']['bookings'])) {
215 $this->record['data']['bookings'] = [];
216 }
217
218 // push booking
219 $this->record['data']['bookings'][] = $bookingId;
220
221 // ensure the pool is not too large
222 if (count($this->record['data']['bookings']) > 2000) {
223 // shorten the pool by cutting off the older (first) array elements
224 $this->record['data']['bookings'] = array_slice($this->record['data']['bookings'], -2000);
225 }
226
227 return $this;
228 }
229
230 /**
231 * Returns the current integration record (profile) ID.
232 *
233 * @return int
234 */
235 public function getProfileID()
236 {
237 return (int) ($this->record['id'] ?? 0);
238 }
239
240 /**
241 * Returns the current integration record (profile) name.
242 *
243 * @return string
244 */
245 public function getProfileName()
246 {
247 return (string) ($this->record['name'] ?? '');
248 }
249
250 /**
251 * Returns the current integration record (profile) provider alias.
252 *
253 * @return string
254 */
255 public function getProfileProvider()
256 {
257 return (string) ($this->record['provider_alias'] ?? '');
258 }
259
260 /**
261 * Returns the current integration record (profile) generation type enum.
262 *
263 * @return string
264 */
265 public function getProfileGenerationType()
266 {
267 return (string) ($this->record['gentype'] ?? 'booking');
268 }
269
270 /**
271 * Returns the current integration record (profile) generation period.
272 *
273 * @return string
274 */
275 public function getProfileGenerationPeriod()
276 {
277 return (string) ($this->record['genperiod'] ?? '');
278 }
279
280 /**
281 * Returns an array with two timestamps for the configured generation period.
282 *
283 * @return array
284 */
285 public function getNextGenerationPeriodTimestamps()
286 {
287 $genperiod = $this->getProfileGenerationPeriod() ?: '0H';
288
289 $genNumber = null;
290 $genOperator = null;
291 $allowedOperators = ['H', 'D'];
292
293 if (preg_match('/^([0-9]+)(H|D)$/i', $genperiod, $matches)) {
294 $genNumber = abs((int) $matches[1]);
295 $genOperator = strtoupper((string) $matches[2]);
296 if (!in_array($genOperator, $allowedOperators)) {
297 $genOperator = null;
298 }
299 }
300
301 if (is_null($genNumber) || is_null($genOperator)) {
302 // fallback to default period
303 $genNumber = 0;
304 $genOperator = 'H';
305 }
306
307 // calculate the target timestamp
308 $targetTs = $genNumber ? strtotime(sprintf('+%d %s', $genNumber, ($genOperator === 'D' ? 'days' : 'hours'))) : time();
309
310 // return the targeted timestamp intervals within a one-hour range
311 return [
312 strtotime(date('Y-m-d H:00:00', $targetTs)),
313 strtotime(date('Y-m-d H:59:59', $targetTs)),
314 ];
315 }
316
317 /**
318 * Gets the current integration profile record.
319 *
320 * @return array
321 */
322 public function getProfileRecord()
323 {
324 return $this->record;
325 }
326
327 /**
328 * Sets the current integration profile record.
329 *
330 * @param array $record The integration profile record.
331 *
332 * @return self
333 */
334 public function setProfileRecord(array $record)
335 {
336 $this->record = $record;
337
338 return $this;
339 }
340
341 /**
342 * Sets a property to the current integration profile record (i.e. "devices").
343 *
344 * @param string $prop The integration profile record property to set.
345 * @param mixed $value The value to set for the given property.
346 *
347 * @return self
348 */
349 public function setProfileRecordProp(string $prop, $value)
350 {
351 $this->record[$prop] = $value;
352
353 return $this;
354 }
355
356 /**
357 * Tells whether the integration profile record is available.
358 *
359 * @return bool
360 */
361 public function hasProfileRecord()
362 {
363 return !empty($this->record);
364 }
365
366 /**
367 * Destroys (deletes) the current integration profile record.
368 *
369 * @return true
370 *
371 * @throws Exception
372 */
373 public function destroyProfileRecord()
374 {
375 if (!$this->hasProfileRecord() || !$this->getProfileID()) {
376 throw new Exception('Missing integration profile record.', 500);
377 }
378
379 // delete profile record from database
380 VBOFactory::getDoorAccessControl()->deleteIntegrationRecord($this);
381
382 // reset internal profile record data
383 $this->record = [];
384
385 return true;
386 }
387
388 /**
389 * Returns the special tag string identifying the current profile record.
390 *
391 * @return ?string Special tag string to be used for contents, or null.
392 */
393 public function getProfileSpecialTag()
394 {
395 $profileId = $this->getProfileID();
396 $profileName = $this->getProfileName();
397 $providerName = $this->getShortName();
398
399 if (empty($profileId) || empty($profileName) || empty($providerName)) {
400 // invalid or incomplete integration profile record
401 return null;
402 }
403
404 // build short/safe provider name
405 $safeProviderName = preg_replace('/[^0-9a-z]/', '', strtolower($providerName));
406
407 // build short/safe profile name
408 $safeProfileName = preg_replace('/[^0-9a-z]/', '', strtolower($profileName));
409
410 if (strlen($safeProviderName) > 8) {
411 // shorten the string
412 $safeProviderName = substr($safeProviderName, 0, 4) . '-' . substr($safeProviderName, -3, 3);
413 }
414
415 if (strlen($safeProfileName) > 8) {
416 // shorten the string
417 $safeProfileName = substr($safeProfileName, 0, 4) . '-' . substr($safeProfileName, -3, 3);
418 }
419
420 // build integration profile identifier short string
421 $shortIdentifier = sprintf('%s_%s', $safeProviderName, $safeProfileName);
422
423 return sprintf('{door_access: p%d_%s}', $profileId, $shortIdentifier);
424 }
425
426 /**
427 * Detects the passcode that was set during a booking history event from a capability result.
428 *
429 * @param array $resultProperties The properties binded to the device capability result.
430 *
431 * @return ?string Passcode string value if found, or null.
432 */
433 public function getPasscodeFromHistoryResult(array $resultProperties)
434 {
435 // providers should implement this method according to what properties they bind with cap result objects
436 return null;
437 }
438
439 /**
440 * Default implementation for letting a provider create a door access upon a new booking event.
441 *
442 * @param VBODooraccessIntegrationDevice $device The provider integration device.
443 * @param int $listingId The involved listing ID.
444 * @param VBOBookingRegistry $registry The booking registry containing all room related details.
445 *
446 * @return ?VBODooraccessDeviceCapabilityResult Device capability execution result, or null.
447 */
448 public function createBookingDoorAccess(VBODooraccessIntegrationDevice $device, int $listingId, VBOBookingRegistry $registry)
449 {
450 // integration providers should implement this method
451 return null;
452 }
453
454 /**
455 * Default implementation for letting a provider modify a door access upon a booking modification event.
456 *
457 * @param VBODooraccessIntegrationDevice $device The provider integration device.
458 * @param int $listingId The involved listing ID.
459 * @param VBOBookingRegistry $registry The booking registry containing all room related details.
460 *
461 * @return ?VBODooraccessDeviceCapabilityResult Device capability execution result, or null.
462 */
463 public function modifyBookingDoorAccess(VBODooraccessIntegrationDevice $device, int $listingId, VBOBookingRegistry $registry)
464 {
465 // integration providers should implement this method
466 return null;
467 }
468
469 /**
470 * Default implementation for letting a provider cancel a door access upon a booking cancellation event.
471 *
472 * @param VBODooraccessIntegrationDevice $device The provider integration device.
473 * @param int $listingId The involved listing ID.
474 * @param VBOBookingRegistry $registry The booking registry containing all room related details.
475 *
476 * @return ?VBODooraccessDeviceCapabilityResult Device capability execution result, or null.
477 */
478 public function cancelBookingDoorAccess(VBODooraccessIntegrationDevice $device, int $listingId, VBOBookingRegistry $registry)
479 {
480 // integration providers should implement this method
481 return null;
482 }
483
484 /**
485 * Default implementation for letting a provider unlock a specific device.
486 *
487 * @param VBODooraccessIntegrationDevice $device The provider integration device.
488 *
489 * @return ?VBODooraccessDeviceCapabilityResult Device capability execution result, or null.
490 */
491 public function handleUnlockDevice(VBODooraccessIntegrationDevice $device)
492 {
493 // integration providers should implement this method
494 return null;
495 }
496
497 /**
498 * Default implementation for letting a provider detect the first door access for a given booking.
499 *
500 * @param VBODooraccessIntegrationDevice $device The provider integration device.
501 * @param int $listingId The involved listing ID.
502 * @param VBOBookingRegistry $registry The booking registry containing all room related details.
503 *
504 * @return ?VBODooraccessDeviceCapabilityResult Device capability execution result, or null.
505 *
506 * @since 1.18.6 (J) - 1.8.6 (WP)
507 */
508 public function detectFirstAccess(VBODooraccessIntegrationDevice $device, int $listingId, VBOBookingRegistry $registry)
509 {
510 // integration providers should implement this method
511 return null;
512 }
513
514 /**
515 * Creates a new device capability object with the given properties.
516 *
517 * @param array $properties Associative list of capability properties.
518 *
519 * @return VBODooraccessDeviceCapability
520 */
521 public function createDeviceCapability(array $properties)
522 {
523 $capability = new VBODooraccessDeviceCapability;
524
525 foreach ($properties as $property => $value) {
526 // build setter method name
527 $method = 'set' . ucfirst($property);
528
529 // bind property value
530 $capability->{$method}($value);
531 }
532
533 return $capability;
534 }
535
536 /**
537 * Fetches the integration devices and parses them internally.
538 * Integration providers will actually fetch the remote devices.
539 *
540 * @return VBODooraccessIntegrationDevice[] List of integration device objects.
541 *
542 * @throws Exception
543 */
544 public function fetchDevices()
545 {
546 // access the integration settings
547 $settings = $this->getSettings();
548
549 if (!$settings && $this->getParams()) {
550 throw new Exception('Missing integration provider settings.', 500);
551 }
552
553 try {
554 // let the integration provider fetch the list of remote devices
555 $remoteDevicesList = $this->fetchRemoteDevices();
556 } catch (Exception $e) {
557 // propagate the error
558 throw $e;
559 }
560
561 if (!is_array($remoteDevicesList) || !$remoteDevicesList) {
562 return [];
563 }
564
565 // map every remote device payload into a decorated device object
566 return array_values(array_filter(array_map(function($device) {
567 // cast device payload to array
568 $device = (array) $device;
569
570 if (!$device) {
571 // empty device payload
572 return null;
573 }
574
575 // start device decorator
576 $decorator = new VBODooraccessIntegrationDevice($device);
577
578 // let the integration provider decorate the device properties
579 $this->decorateDeviceProperties($decorator, $device);
580
581 if (!$decorator->isComplete()) {
582 // invalid device object properties decorated
583 return null;
584 }
585
586 // return the decorated device object
587 return $decorator;
588 }, $remoteDevicesList)));
589 }
590
591 /**
592 * Builds the routed Webhook endpoint URL for the
593 * current integration record and profile ID to spawn.
594 *
595 * @param ?array $data Optional assoc list of URL data.
596 *
597 * @return string The routed Webhook endpoint URL.
598 *
599 * @since 1.18.6 (J) - 1.8.6 (WP)
600 */
601 public function buildWebhookURL(?array $data = null)
602 {
603 // extract possibly injected options
604 $options = (array) ($data['_options'] ?? []);
605
606 // remove options from URL data
607 unset($data['_options']);
608
609 // build base URL params to spawn the current integration and profile
610 $urlParams = [
611 'option' => 'com_vikbooking',
612 'task' => 'apps.webhook',
613 'env' => 'dac',
614 'provider' => $this->getProfileProvider(),
615 'profile' => $this->getProfileID(),
616 ];
617
618 if ($data) {
619 // merge custom URL data
620 $urlParams = array_merge($urlParams, $data);
621 }
622
623 // access the platform URI
624 $platformUri = VBOFactory::getPlatform()->getUri();
625
626 if ($options['route'] ?? false) {
627 // route the final URI
628 $finalUri = $platformUri->route('index.php?' . http_build_query($urlParams));
629 } else {
630 // construct root URI with query string arguments
631 $finalUri = JUri::root() . '?' . http_build_query($urlParams);
632 }
633
634 if ($options['csrf'] ?? null) {
635 // add CSRF token to final URL
636 $finalUri = $platformUri->addCSRF($finalUri);
637 }
638
639 // return the routed URI to spawn the integration Webhook endpoint URL
640 return $finalUri;
641 }
642
643 /**
644 * Builds the routed OAuth authorization URL for the
645 * current integration record and profile ID to spawn.
646 *
647 * @param ?array $data Optional assoc list of URL data.
648 *
649 * @return string The routed OAuth 2 auth URL.
650 *
651 * @since 1.18.6 (J) - 1.8.6 (WP)
652 */
653 public function buildOAuthURL(?array $data = null)
654 {
655 // extract possibly injected options
656 $options = (array) ($data['_options'] ?? []);
657
658 // remove options from URL data
659 unset($data['_options']);
660
661 // build base URL params to spawn the current integration and profile
662 $urlParams = [
663 'option' => 'com_vikbooking',
664 'task' => 'apps.oauth',
665 'env' => 'dac',
666 'provider' => $this->getProfileProvider(),
667 'profile' => $this->getProfileID(),
668 ];
669
670 if ($data) {
671 // merge custom URL data
672 $urlParams = array_merge($urlParams, $data);
673 }
674
675 // access the platform URI
676 $platformUri = VBOFactory::getPlatform()->getUri();
677
678 if ($options['route'] ?? false) {
679 // route the final URI
680 $finalUri = $platformUri->route('index.php?' . http_build_query($urlParams));
681 } else {
682 // construct root URI with query string arguments
683 $finalUri = JUri::root() . '?' . http_build_query($urlParams);
684 }
685
686 if ($options['csrf'] ?? null) {
687 // add CSRF token to final URL
688 $finalUri = $platformUri->addCSRF($finalUri);
689 }
690
691 // return the routed URI to spawn the integration authorization callback URL
692 return $finalUri;
693 }
694
695 /**
696 * Returns an active OAuth code to verify the authenticity of the request.
697 * To be used for CSRF prevention or similar purposes during OAuth authentications.
698 *
699 * @param ?string $suffix Optional configuration parameter suffix.
700 *
701 * @return string
702 *
703 * @since 1.18.6 (J) - 1.8.6 (WP)
704 */
705 public function getOAuthCode(?string $suffix = null, ?array $options = null)
706 {
707 // access configuration object
708 $config = VBOFactory::getConfig();
709
710 // build param name
711 $paramName = 'dac_oauth_code' . ($suffix ? '_' . $suffix : '');
712
713 // access current setting from database
714 $currentSetting = (array) $config->getArray($paramName, []);
715
716 // access current expiration timestamp, if any
717 $currentExpiryTs = $currentSetting['expiry_ts'] ?? null;
718
719 // a new OAuth code will be generated if unavailable or expired
720 if (!($currentSetting['code'] ?? null) || ($currentExpiryTs !== null && $currentExpiryTs < time())) {
721 // generate a new OAuth code with a default validity of one hour
722 $oauthCode = VikBooking::getCPinInstance()->generateSerialCode(
723 (int) ($options['length'] ?? 10),
724 (is_array($options['map'] ?? null) ? $options['map'] : null)
725 );
726
727 if ($options['insensitive'] ?? 1) {
728 // case-insensitive by default
729 $oauthCode = strtolower($oauthCode);
730 }
731
732 // build expiration timestamp
733 $expiryTs = ($options['expiry_ts'] ?? 0) ?: strtotime('+1 hour');
734
735 // store OAuth code data
736 $config->set($paramName, [
737 'code' => $oauthCode,
738 'expiry_ts' => $expiryTs,
739 ]);
740
741 // return the currently active OAuth code
742 return $oauthCode;
743 }
744
745 // return the existing OAuth code because still valid
746 return $currentSetting['code'];
747 }
748
749 /**
750 * OAuth authorization callback is triggered to allow the integration
751 * to obtain the data upon authorising the application.
752 *
753 * @param ?array $data Optional spawn data to parse.
754 *
755 * @return void
756 *
757 * @throws Exception
758 *
759 * @since 1.18.6 (J) - 1.8.6 (WP)
760 */
761 public function spawnOAuthCallback(?array $data = null)
762 {
763 // do nothing by default, integrations should override this method
764 return;
765 }
766
767 /**
768 * Webhook endpoint callback is triggered to allow the integration
769 * to obtain webhook data from the provider.
770 *
771 * @param ?array $data Optional spawn data to parse.
772 *
773 * @return void
774 *
775 * @throws Exception
776 *
777 * @since 1.18.6 (J) - 1.8.6 (WP)
778 */
779 public function spawnWebhookCallback(?array $data = null)
780 {
781 // do nothing by default, integrations should override this method
782 return;
783 }
784
785 /**
786 * Fetches the integration remote devices.
787 *
788 * @return array List of remote device associative arrays or objects.
789 */
790 abstract protected function fetchRemoteDevices();
791
792 /**
793 * Decorates the properties of a remote device fetched.
794 *
795 * @param VBODooraccessIntegrationDevice $decorator The integration device decorator object.
796 * @param array $device The remote device associative array fetched.
797 *
798 * @return void
799 */
800 abstract protected function decorateDeviceProperties(VBODooraccessIntegrationDevice $decorator, array $device);
801 }
802