PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.6.4
VikBooking Hotel Booking Engine & PMS v1.6.4
1.8.16 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 All 37 releases
vikbooking / admin / helpers / widgets / virtual_terminal.php

virtual_terminal.php in VikBooking Hotel Booking Engine & PMS 1.6.4, at admin/helpers/widgets/virtual_terminal.php

1,204 lines 35.3 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 com_vikbooking
5 * @author Alessio Gaggii - E4J srl
6 * @copyright Copyright (C) 2023 E4J srl. All rights reserved.
7 * @license GNU General Public License version 2 or later; see LICENSE
8 * @link https://vikwp.com
9 */
10
11 defined('ABSPATH') or die('No script kiddies please!');
12
13 /**
14 * Class handler for admin widget "virtual terminal".
15 *
16 * @since 1.16.4 (J) - 1.6.4 (WP)
17 */
18 class VikBookingAdminWidgetVirtualTerminal extends VikBookingAdminWidget
19 {
20 /**
21 * The instance counter of this widget. Since we do not load individual parameters
22 * for each widget's instance, we use a static counter to determine its settings.
23 *
24 * @var int
25 */
26 protected static $instance_counter = -1;
27
28 /**
29 * Tells whether VCM is installed and updated.
30 *
31 * @var bool
32 */
33 protected $vcm_exists = false;
34
35 /**
36 * The payment processor record loaded.
37 *
38 * @var array
39 */
40 protected $payment_method = [];
41
42 /**
43 * Class constructor will define the widget name and identifier.
44 */
45 public function __construct()
46 {
47 // call parent constructor
48 parent::__construct();
49
50 $this->widgetName = JText::translate('VBO_W_VIRTUALTERMINAL_TITLE');
51 $this->widgetDescr = JText::translate('VBO_W_VIRTUALTERMINAL_DESCR');
52 $this->widgetId = basename(__FILE__, '.php');
53
54 // define widget and icon and style name
55 $this->widgetIcon = '<i class="' . VikBookingIcons::i('credit-card') . '"></i>';
56 $this->widgetStyleName = 'red';
57
58 // whether VCM is available
59 if (is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) {
60 $this->vcm_exists = true;
61 }
62 }
63
64 /**
65 * Custom method for this widget only to load the virtual terminal CC form.
66 * The method is called by the admin controller through an AJAX request.
67 * The visibility should be public, it should not exit the process, and
68 * any content sent to output will be returned to the AJAX response.
69 * In this case we return an array because this method requires "return":1.
70 */
71 public function loadTerminalForm()
72 {
73 $wrapper = VikRequest::getString('wrapper', '', 'request');
74 $bid = VikRequest::getInt('bid', 0, 'request');
75
76 if (!$bid) {
77 // booking ID is mandatory
78 VBOHttpDocument::getInstance()->close(500, JText::translate('VBPEDITBUSYONE'));
79 }
80
81 // get booking details
82 $booking_info = VikBooking::getBookingInfoFromID($bid);
83
84 if (!$booking_info) {
85 // booking record not found
86 VBOHttpDocument::getInstance()->close(404, JText::translate('VBPEDITBUSYONE'));
87 }
88
89 // check if direct charge is supported by the current payment processor
90 if (!$this->getPaymentProcessor($booking_info)) {
91 // unable to proceed due to unsupported transaction
92 return [
93 'html' => '<p class="err">' . JText::translate('VBO_PAY_PROCESS_NO_DIRECT_CHARGE') . '</p>',
94 ];
95 }
96
97 // build complete credit card payload, if any
98 $cc_payload_str = '';
99 $booking_info['paymentlog'] = (string)$booking_info['paymentlog'];
100 if (stripos($booking_info['paymentlog'], 'card number') !== false && strpos($booking_info['paymentlog'], '*') !== false) {
101 // matched a log for an OTA CC
102 $cc_payload_str = $booking_info['paymentlog'];
103 } elseif (preg_match("/(([\d\*]{4,4}\s*){4,4})|(([\d\*]{4,6}\s*){3,3})/", $booking_info['paymentlog'])) {
104 // matched a credit card
105 $cc_payload_str = $booking_info['paymentlog'];
106 }
107
108 // check if this is an OTA reservation with remotely decoded CC details
109 $remote_cc_data = [];
110 if ($this->vcm_exists && !empty($booking_info['idorderota']) && !empty($booking_info['channel'])) {
111 // make sure the permissions are met
112 if (!JFactory::getUser()->authorise('core.admin', 'com_vikchannelmanager')) {
113 // insufficient permissions to handle CC details
114 return [
115 'html' => '<p class="err">' . JText::translate('JERROR_ALERTNOAUTHOR') . '</p>',
116 ];
117 }
118
119 // channel source
120 $channel_source = (string)$booking_info['channel'];
121 if (strpos($booking_info['channel'], '_') !== false) {
122 $channelparts = explode('_', $booking_info['channel']);
123 $channel_source = $channelparts[0];
124 }
125
126 // only updated versions of VCM will support remote CC decoding for OTA reservations
127 if (class_exists('VCMOtaBooking')) {
128 // invoke the OTA Booking helper class from VCM
129 $cc_helper = VCMOtaBooking::getInstance([
130 'channel_source' => $channel_source,
131 'ota_id' => $booking_info['idorderota'],
132 ], $anew = true);
133
134 if (method_exists($cc_helper, 'decodeCreditCardDetails')) {
135 $remote_cc_data = $cc_helper->decodeCreditCardDetails();
136 // make sure the response was valid
137 if (!$remote_cc_data || !empty($remote_cc_data['error'])) {
138 // we ignore the error by simply resetting the array
139 $remote_cc_data = [];
140 }
141 }
142 }
143 }
144
145 // merge remotely decoded CC details to parsed payment log (if any)
146 $cc_value_pairs = array_merge($remote_cc_data, $this->parseCreditCardValuePairs($cc_payload_str, $remote_cc_data));
147
148 // currency code
149 $currency_code = !empty($booking_info['chcurrency']) ? $booking_info['chcurrency'] : VikBooking::getCurrencyName();
150 if (empty($currency_code) || strlen((string)$currency_code) != 3) {
151 // fallback to currency transaction code
152 $currency_code = VikBooking::getCurrencyCodePp();
153 }
154
155 // check known CC values to build the hidden transaction values
156 $known_tn_vals = [];
157 $hidden_tn_vals = [];
158 if (isset($cc_value_pairs['name'])) {
159 $known_tn_vals['name'] = $cc_value_pairs['name'];
160 }
161 if (isset($cc_value_pairs['card_number'])) {
162 $known_tn_vals['card_number'] = $cc_value_pairs['card_number'];
163 }
164 if (isset($cc_value_pairs['expiration_date'])) {
165 $known_tn_vals['expiration_date'] = $cc_value_pairs['expiration_date'];
166 }
167 if (isset($cc_value_pairs['cvv'])) {
168 $known_tn_vals['cvv'] = $cc_value_pairs['cvv'];
169 }
170 foreach ($cc_value_pairs as $key => $value) {
171 if (!isset($known_tn_vals[$key])) {
172 $hidden_tn_vals[$key] = $value;
173 }
174 }
175
176 // start output buffering
177 ob_start();
178
179 ?>
180
181 <div class="vbo-vterminal-cc-container">
182
183 <div class="vbo-vterminal-cc-row-group vbo-vterminal-cc-row-group-amount">
184 <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-currency">
185 <div class="vbo-vterminal-cc-lbl"><?php echo JText::translate('VBOCPARAMCURRENCY'); ?></div>
186 <div class="vbo-vterminal-cc-val">
187 <input type="text" autocomplete="off" value="<?php echo JHtml::fetch('esc_attr', $currency_code); ?>" data-vt-cc-field="currency" />
188 </div>
189 </div>
190 <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-amount">
191 <div class="vbo-vterminal-cc-lbl"><?php echo JText::translate('VBO_CC_AMOUNT'); ?></div>
192 <div class="vbo-vterminal-cc-val">
193 <input type="number" value="<?php echo $booking_info['total']; ?>" min="0" step="any" data-vt-cc-field="amount" />
194 </div>
195 </div>
196 </div>
197
198 <div class="vbo-vterminal-cc-row-group vbo-vterminal-cc-row-group-cardholder">
199 <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-cardholder">
200 <div class="vbo-vterminal-cc-lbl"><?php echo JText::translate('VBISNOMINATIVE'); ?></div>
201 <div class="vbo-vterminal-cc-val">
202 <input type="text" autocomplete="off" value="<?php echo isset($known_tn_vals['name']) ? JHtml::fetch('esc_attr', $cc_value_pairs['name']) : ''; ?>" data-vt-cc-field="cardholder" />
203 </div>
204 </div>
205 </div>
206
207 <div class="vbo-vterminal-cc-row-group vbo-vterminal-cc-row-group-ccpan">
208 <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-ccpan">
209 <div class="vbo-vterminal-cc-lbl"><?php echo JText::translate('VBO_CC_NUMBER'); ?></div>
210 <div class="vbo-vterminal-cc-val vbo-vterminal-cc-val-withlogo">
211 <input type="text" autocomplete="off" value="<?php echo isset($known_tn_vals['card_number']) ? JHtml::fetch('esc_attr', $cc_value_pairs['card_number']) : ''; ?>" data-vt-cc-field="card_number" />
212 <span class="vbo-vterminal-cc-type-logo"></span>
213 </div>
214 </div>
215 </div>
216
217 <div class="vbo-vterminal-cc-row-group vbo-vterminal-cc-row-group-ccextra">
218 <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-ccexpiry">
219 <div class="vbo-vterminal-cc-lbl"><?php echo JText::translate('VBO_CC_EXPIRY_DT'); ?></div>
220 <div class="vbo-vterminal-cc-val">
221 <input type="text" autocomplete="off" placeholder="MM/YYYY" value="<?php echo isset($known_tn_vals['expiration_date']) ? JHtml::fetch('esc_attr', $cc_value_pairs['expiration_date']) : ''; ?>" data-vt-cc-field="expiry" />
222 </div>
223 </div>
224 <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-cvc">
225 <div class="vbo-vterminal-cc-lbl"><?php echo JText::translate('VBO_CC_CVV'); ?></div>
226 <div class="vbo-vterminal-cc-val">
227 <input type="text" autocomplete="off" value="<?php echo isset($known_tn_vals['cvv']) ? JHtml::fetch('esc_attr', $cc_value_pairs['cvv']) : ''; ?>" data-vt-cc-field="cvv" />
228 </div>
229 </div>
230 </div>
231
232 <div class="vbo-vterminal-cc-row-group vbo-vterminal-cc-row-group-submit">
233 <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-submit">
234 <div class="vbo-vterminal-cc-val">
235 <button type="button" class="btn vbo-config-btn" onclick="vboWidgetVTerminalChargeCard('<?php echo $wrapper; ?>');"><?php VikBookingIcons::e('credit-card'); ?> <?php echo JText::translate('VBO_CC_DOCHARGE'); ?></button>
236 </div>
237 </div>
238 </div>
239
240 <?php
241 // print hidden transaction values, if any
242 foreach ($hidden_tn_vals as $key => $value) {
243 ?>
244 <input type="hidden" value="<?php echo JHtml::fetch('esc_attr', $value); ?>" data-vt-cc-field="<?php echo JHtml::fetch('esc_attr', $key); ?>" />
245 <?php
246 }
247 ?>
248
249 </div>
250
251 <script type="text/javascript">
252
253 // store the last card type detected
254 var vbo_vt_last_cc_type = '';
255
256 // subscribe to the keyup event for the card number field
257 var vbo_vt_cc_num_f = document.querySelector('#<?php echo $wrapper; ?>').querySelector('[data-vt-cc-field="card_number"]');
258 vbo_vt_cc_num_f.addEventListener('keyup', (e) => {
259 if (!e || !e.target) {
260 return;
261 }
262
263 // the current CC number value
264 var cc_value = e.target.value;
265
266 // invoke the helper class to handle the card number
267 const card = new VBOCreditCard(cc_value);
268
269 // detect card type and format card number
270 var card_type = card.getCardType();
271 var cc_text = card.formatCreditCard(card_type);
272
273 // set formatted CC number value
274 e.target.value = cc_text;
275
276 // handle CC logo
277 if (vbo_vt_last_cc_type != card_type) {
278 var card_logo_uri = card.getCardLogoURI(card_type);
279 var card_logo_pnode = e.target.parentNode;
280 var card_logo_wrap = card_logo_pnode.querySelector('.vbo-vterminal-cc-type-logo');
281 if (!card_type) {
282 // hide CC logo
283 card_logo_wrap.innerHTML = '';
284 card_logo_pnode.classList.remove('vbo-vterminal-cc-type-logo-detected');
285 card_logo_pnode.classList.add('vbo-vterminal-cc-type-logo-unknown');
286 } else if (card_logo_uri) {
287 // set CC logo
288 card_logo_wrap.innerHTML = '<img src="' + card_logo_uri + '" />';
289 card_logo_pnode.classList.remove('vbo-vterminal-cc-type-logo-unknown');
290 card_logo_pnode.classList.add('vbo-vterminal-cc-type-logo-detected');
291 }
292
293 // overwrite value
294 vbo_vt_last_cc_type = card_type;
295 }
296 });
297
298 // subscribe to the keydown and keyup events for the card expiration date field
299 var vbo_vt_cc_exp_f = document.querySelector('#<?php echo $wrapper; ?>').querySelector('[data-vt-cc-field="expiry"]');
300 vbo_vt_cc_exp_f.addEventListener('keydown', (e) => {
301 if (!e || !e.key) {
302 return;
303 }
304
305 if (!isNaN(e.key) || e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) {
306 return true;
307 }
308
309 switch (e.key) {
310 case "ArrowLeft":
311 case "ArrowRight":
312 case "Enter":
313 case "Escape":
314 case "Delete":
315 case "Backspace":
316 case "Tab":
317 case "/":
318 return true;
319 default:
320 event.preventDefault();
321 return false;
322 }
323 });
324
325 vbo_vt_cc_exp_f.addEventListener('keyup', (e) => {
326 if (!e || !e.target) {
327 return;
328 }
329
330 var date = e.target.value;
331
332 if (!date) {
333 return;
334 }
335
336 if (date.length === 2 && date.indexOf('/') < 0) {
337 e.target.value += '/';
338 return;
339 }
340
341 if (date.length > 7) {
342 e.target.value = e.target.value.substr(0, 7);
343 return;
344 }
345 });
346
347 // subscribe to the blur event to make sure the year is full
348 vbo_vt_cc_exp_f.addEventListener('blur', (e) => {
349 if (!e || !e.target) {
350 return;
351 }
352
353 var date = e.target.value;
354
355 if (!date || date.length >= 7 || date.indexOf('/') < 0) {
356 return;
357 }
358
359 var parts = date.split('/');
360
361 if (parts[1].length === 2) {
362 var today = new Date;
363 var year = (today.getFullYear() + '').substr(0, 2);
364
365 e.target.value = parts[0] + '/' + year + parts[1];
366 }
367
368 return;
369 });
370
371 // default state for CC number and expiry input fields
372 setTimeout(() => {
373 if (vbo_vt_cc_num_f.value) {
374 // trigger keyup event to format the current CC and display the logo
375 vbo_vt_cc_num_f.dispatchEvent(new Event('keyup'));
376 }
377 if (vbo_vt_cc_exp_f.value) {
378 // trigger blur event to format the current CC expiration date
379 vbo_vt_cc_exp_f.dispatchEvent(new Event('blur'));
380 }
381 }, 128);
382
383 </script>
384
385 <?php
386
387 // get the HTML buffer
388 $html_content = ob_get_contents();
389 ob_end_clean();
390
391 // return an associative array of values
392 return array(
393 'html' => $html_content,
394 );
395 }
396
397 /**
398 * Custom method for this widget only to debit a credit card.
399 *
400 * @return array|void
401 */
402 public function doDirectCharge()
403 {
404 $wrapper = VikRequest::getString('wrapper', '', 'request');
405 $bid = VikRequest::getInt('bid', 0, 'request');
406 $card = VikRequest::getVar('card', [], 'request');
407
408 if (!$bid || !$card) {
409 // booking ID and CC details are mandatory
410 VBOHttpDocument::getInstance()->close(500, JText::translate('VBPEDITBUSYONE'));
411 }
412
413 // get booking details
414 $booking_info = VikBooking::getBookingInfoFromID($bid);
415
416 if (!$booking_info) {
417 // booking record not found
418 VBOHttpDocument::getInstance()->close(404, JText::translate('VBPEDITBUSYONE'));
419 }
420
421 // get the eligible payment processor by passing the card details
422 $processor = $this->getPaymentProcessor($booking_info, $card);
423
424 if (!$processor) {
425 VBOHttpDocument::getInstance()->close(500, JText::translate('VBO_PAY_PROCESS_NO_DIRECT_CHARGE'));
426 }
427
428 // default transaction response
429 $array_result = [
430 'verified' => 0,
431 ];
432
433 try {
434 // perform the transaction
435 $array_result = $processor->directCharge();
436 } catch (Exception $e) {
437 // set error message
438 $array_result['log'] = sprintf(JText::translate('VBO_CC_TN_ERROR') . " \n%s", $e->getMessage());
439 }
440
441 if ($array_result['verified'] != 1) {
442 // erroneous response
443 if (!empty($array_result['log']) && is_string($array_result['log'])) {
444 VBOHttpDocument::getInstance()->close(500, $array_result['log']);
445 } else {
446 VBOHttpDocument::getInstance()->close(500, 'Operation failed');
447 }
448 }
449
450 // valid transaction response!
451
452 // update booking details
453 $dbo = JFactory::getDbo();
454
455 // get the amount paid
456 $tn_amount = isset($array_result['tot_paid']) ? (float)$array_result['tot_paid'] : null;
457
458 // get the log string, if any
459 $tn_log = !empty($array_result['log']) ? $array_result['log'] : '';
460
461 // update record
462 $upd_record = new stdClass;
463 $upd_record->id = $booking_info['id'];
464 if ($tn_amount) {
465 // update amount paid
466 $upd_record->totpaid = $booking_info['totpaid'] + $tn_amount;
467 // update payable amount (if needed)
468 $new_payable = $booking_info['payable'] - $tn_amount;
469 $new_payable = $new_payable < 0 ? 0 : $new_payable;
470 $upd_record->payable = $new_payable;
471 }
472 if ($tn_log) {
473 $upd_record->paymentlog = $booking_info['paymentlog'] . "\n\n" . date('c') . "\n" . $tn_log;
474 }
475 $upd_record->paymcount = ((int)$booking_info['paymcount'] + 1);
476
477 // update reservation record
478 $dbo->updateObject('#__vikbooking_orders', $upd_record, 'id');
479
480 // payment processor name
481 $pay_process_name = $this->payment_method ? $this->payment_method['name'] : 'CC Direct Charge';
482
483 // handle transaction data to eventually support a later transaction of type refund
484 $tn_data = isset($array_result['transaction']) ? $array_result['transaction'] : null;
485 if ($tn_amount) {
486 // check event data payload to store
487 if (is_array($tn_data)) {
488 // set key
489 $tn_data['amount_paid'] = $tn_amount;
490 } elseif (is_object($tn_data)) {
491 // set property
492 $tn_data->amount_paid = $tn_amount;
493 } elseif (!$tn_data) {
494 // build an array (we add the payment name because we know there is no other transaction data)
495 $tn_data = [
496 'amount_paid' => $tn_amount,
497 'payment_method' => $pay_process_name,
498 ];
499 }
500 }
501
502 // current admin-user
503 $now_user = JFactory::getUser();
504
505 // Booking History
506 $ev_descr = JText::translate('VBO_W_VIRTUALTERMINAL_TITLE') . " - {$pay_process_name} ({$now_user->name})";
507 VikBooking::getBookingHistoryInstance()->setBid($booking_info['id'])->setExtraData($tn_data)->store('P' . ($booking_info['paymcount'] > 0 ? 'N' : '0'), $ev_descr);
508
509 return [
510 'success' => 1,
511 'log' => $tn_log,
512 ];
513 }
514
515 /**
516 * Preload the necessary assets.
517 *
518 * @return void
519 */
520 public function preload()
521 {
522 // JS lang def
523 JText::script('VBOUPLOADFILEDONE');
524 }
525
526 /**
527 * Main method to invoke the widget. Contents will be loaded
528 * through AJAX requests, not via PHP when the page loads.
529 *
530 * @param VBOMultitaskData $data
531 *
532 * @return void
533 */
534 public function render(VBOMultitaskData $data = null)
535 {
536 // increase widget's instance counter
537 static::$instance_counter++;
538
539 // check whether the widget is being rendered via AJAX when adding it through the customizer
540 $is_ajax = $this->isAjaxRendering();
541
542 // generate a unique ID for the sticky notes wrapper instance
543 $wrapper_instance = !$is_ajax ? static::$instance_counter : rand();
544 $wrapper_id = 'vbo-widget-vterminal-' . $wrapper_instance;
545
546 // get permissions
547 $vbo_auth_bookings = JFactory::getUser()->authorise('core.vbo.bookings', 'com_vikbooking');
548 if (!$vbo_auth_bookings) {
549 // display nothing
550 return;
551 }
552
553 // check multitask data
554 $page_bid = 0;
555 $js_modal_id = '';
556 if ($data) {
557 $is_modal_rendering = $data->isModalRendering();
558 $page_bid = $data->getBookingID();
559 if ($is_modal_rendering) {
560 // get modal JS identifier
561 $js_modal_id = $data->getModalJsIdentifier();
562 }
563 }
564
565 if (!$page_bid) {
566 // unable to continue from a page outside the booking details or booking ID set
567 ?>
568 <p class="warn"><?php echo JText::translate('VBPEDITBUSYONE'); ?></p>
569 <?php
570 return;
571 }
572
573 ?>
574 <div id="<?php echo $wrapper_id; ?>" class="vbo-admin-widget-wrapper" data-instance="<?php echo $wrapper_instance; ?>" data-pagebid="<?php echo $page_bid; ?>" data-modalid="<?php echo $js_modal_id; ?>">
575 <div class="vbo-admin-widget-head">
576 <div class="vbo-admin-widget-head-inline">
577 <h4><?php echo $this->widgetIcon; ?> <span><?php echo $this->widgetName; ?></span></h4>
578 </div>
579 </div>
580 <div class="vbo-widget-vterminal-wrap">
581 <div class="vbo-widget-vterminal-inner">
582
583 <div class="vbo-widget-vterminal-form"></div>
584
585 </div>
586 </div>
587 </div>
588 <?php
589
590 if (static::$instance_counter === 0 || $is_ajax) {
591 /**
592 * Print the JS code only once for all instances of this widget.
593 * The real rendering is made through AJAX, not when the page loads.
594 */
595 ?>
596
597 <script type="text/javascript">
598
599 /**
600 * Default icons for status.
601 */
602 var vbo_vt_icon_error = '<?php VikBookingIcons::e('exclamation-circle'); ?>';
603 var vbo_vt_icon_success = '<?php VikBookingIcons::e('check-circle'); ?>';
604
605 /**
606 * Perform the request to load the virtual terminal form.
607 */
608 function vboWidgetVTerminalFormLoad(wrapper, page_bid) {
609 var widget_instance = jQuery('#' + wrapper);
610 if (!widget_instance.length) {
611 return false;
612 }
613
614 // check if multitask data passed a booking ID for the current page
615 var force_bid = 0;
616 if (typeof page_bid !== 'undefined' && page_bid && !isNaN(page_bid)) {
617 force_bid = page_bid;
618 }
619
620 // the widget method to call
621 var call_method = 'loadTerminalForm';
622
623 // make a request to load the bookings calendar
624 VBOCore.doAjax(
625 "<?php echo $this->getExecWidgetAjaxUri(); ?>",
626 {
627 widget_id: "<?php echo $this->getIdentifier(); ?>",
628 call: call_method,
629 return: 1,
630 bid: force_bid,
631 wrapper: wrapper,
632 tmpl: "component"
633 },
634 (response) => {
635 try {
636 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
637 if (!obj_res.hasOwnProperty(call_method)) {
638 console.error('Unexpected JSON response', obj_res);
639 return false;
640 }
641
642 // replace HTML content
643 widget_instance.find('.vbo-widget-vterminal-form').html(obj_res[call_method]['html']);
644 } catch(err) {
645 console.error('could not parse JSON response', err, response);
646 }
647 },
648 (error) => {
649 // remove the skeleton loading
650 widget_instance.find('.vbo-widget-vterminal-form').find('.vbo-skeleton-loading').remove();
651 // display error message
652 alert(error.responseText);
653 }
654 );
655 }
656
657 /**
658 * Performs the request to charge the given card details.
659 */
660 function vboWidgetVTerminalChargeCard(wrapper) {
661 var widget_instance = jQuery('#' + wrapper);
662 if (!widget_instance.length) {
663 return false;
664 }
665
666 // the trigger button
667 var charge_cmd = widget_instance.find('.vbo-vterminal-cc-row-submit').find('button');
668
669 // disable button
670 charge_cmd.prop('disabled', true);
671
672 if (VBOCore.options.default_loading_body) {
673 // show loading spinner
674 charge_cmd.find('i').replaceWith(VBOCore.options.default_loading_body);
675 }
676
677 // gather all CC fields
678 var cc_fields = {};
679 widget_instance.find('.vbo-widget-vterminal-form').find('[data-vt-cc-field]').each(function() {
680 var cc_f_key = jQuery(this).attr('data-vt-cc-field');
681 cc_fields[cc_f_key] = jQuery(this).val();
682 });
683
684 // get the booking ID for the transaction
685 var force_bid = widget_instance.attr('data-pagebid');
686
687 // the widget method to call
688 var call_method = 'doDirectCharge';
689
690 // make a request to load the bookings calendar
691 VBOCore.doAjax(
692 "<?php echo $this->getExecWidgetAjaxUri(); ?>",
693 {
694 widget_id: "<?php echo $this->getIdentifier(); ?>",
695 call: call_method,
696 return: 1,
697 bid: force_bid,
698 card: cc_fields,
699 wrapper: wrapper,
700 tmpl: "component"
701 },
702 (response) => {
703 try {
704 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
705 if (!obj_res.hasOwnProperty(call_method)) {
706 console.error('Unexpected JSON response', obj_res);
707 return false;
708 }
709
710 // turn flag on
711 vbo_widget_vt_last_tn = 1;
712
713 // update button status
714 charge_cmd.removeClass('vbo-config-btn').addClass('btn-success').html(vbo_vt_icon_success + ' ' + Joomla.JText._('VBOUPLOADFILEDONE'));
715
716 // check if we need to dismiss the modal widget
717 var js_modal_id = widget_instance.attr('data-modalid');
718 if (js_modal_id) {
719 setTimeout(() => {
720 // dismiss modal widget
721 VBOCore.emitEvent('vbo-dismiss-widget-modal' + js_modal_id);
722 }, 1500);
723 }
724 } catch(err) {
725 console.error('could not parse JSON response', err, response);
726 }
727 },
728 (error) => {
729 // display error message
730 alert(error.responseText);
731 // restore button
732 charge_cmd.prop('disabled', false);
733 charge_cmd.find('i').replaceWith(vbo_vt_icon_error);
734 }
735 );
736 }
737
738 /**
739 * Generate the HTML skeleton loading string to build the form.
740 */
741 function vboWidgetVTerminalFormSkeleton(wrapper) {
742 var widget_instance = jQuery('#' + wrapper);
743 if (!widget_instance.length) {
744 return false;
745 }
746
747 var def_loading = '';
748 def_loading += '<div class="vbo-dashboard-guest-activity vbo-dashboard-guest-activity-skeleton">';
749 def_loading += ' <div class="vbo-dashboard-guest-activity-avatar">';
750 def_loading += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-avatar"></div>';
751 def_loading += ' </div>';
752 def_loading += ' <div class="vbo-dashboard-guest-activity-content">';
753 def_loading += ' <div class="vbo-dashboard-guest-activity-content-head">';
754 def_loading += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-title"></div>';
755 def_loading += ' </div>';
756 def_loading += ' <div class="vbo-dashboard-guest-activity-content-subhead">';
757 def_loading += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-subtitle"></div>';
758 def_loading += ' </div>';
759 def_loading += ' <div class="vbo-dashboard-guest-activity-content-info-msg">';
760 def_loading += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-content"></div>';
761 def_loading += ' </div>';
762 def_loading += ' </div>';
763 def_loading += '</div>';
764
765 widget_instance.find('.vbo-widget-vterminal-form').html(def_loading);
766 }
767
768 /**
769 * Triggers when the multitask panel opens.
770 */
771 function vboWidgetVTerminalMultitaskOpen(wrapper) {
772 var widget_instance = jQuery('#' + wrapper);
773 if (!widget_instance.length) {
774 return false;
775 }
776
777 // check if a booking ID was set for this page
778 var page_bid = widget_instance.attr('data-pagebid');
779 if (!page_bid || page_bid < 1) {
780 return false;
781 }
782
783 // show loading skeletons
784 vboWidgetVTerminalFormSkeleton(wrapper);
785
786 // load data by injecting the current booking ID
787 vboWidgetVTerminalFormLoad(wrapper, page_bid);
788 }
789
790 /**
791 * Credit Card Detector Class.
792 */
793 function VBOCreditCard(card) {
794 this.set(card);
795 }
796
797 VBOCreditCard.prototype.set = function(card) {
798 this.cards_logo_uri_base = '<?php echo VBO_ADMIN_URI . 'resources/js_upload/images/'; ?>';
799 this.card = [];
800 for (var i = 0; i < card.length; i++) {
801 var ch = card.charCodeAt(i);
802 if (ch >= 48 && ch <= 57) {
803 this.card.push(ch-48);
804 }
805 }
806 }
807
808 VBOCreditCard.prototype.get = function() {
809 return this.card;
810 }
811
812 VBOCreditCard.prototype.isEnoughSpace = function(len) {
813 return ( this.card.length >= len );
814 }
815
816 VBOCreditCard.prototype.isEmpty = function() {
817 return !this.isEnoughSpace(1);
818 }
819
820 VBOCreditCard.prototype.isValid = function() {
821 const type = this.getCardType();
822
823 if (type.length && this.card.length == VBOCreditCard.properties[type].size) {
824 return true;
825 }
826
827 return false;
828 }
829
830 VBOCreditCard.prototype.getNumberToIndex = function(i) {
831 var n = 0;
832 var factor = 1;
833 for (i = i-1 ; i >= 0; i--) {
834 n += factor * this.card[i];
835 factor *= 10;
836 }
837 return n;
838 }
839
840 VBOCreditCard.prototype.isVisa = function() {
841 return this.matchBrandRanges([
842 [4]
843 ]);
844 }
845
846 VBOCreditCard.prototype.isMasterCard = function() {
847 return this.matchBrandRanges([
848 [51, 55],
849 [2221, 2720]
850 ]);
851 }
852
853 VBOCreditCard.prototype.isAmericanExpress = function() {
854 return this.matchBrandRanges([
855 [34],
856 [37]
857 ]);
858 }
859
860 VBOCreditCard.prototype.isDiners = function() {
861 return this.matchBrandRanges([
862 [300, 305],
863 [36],
864 [38, 39]
865 ]);
866 }
867
868 VBOCreditCard.prototype.isDiscover = function() {
869 return this.matchBrandRanges([
870 [6011],
871 [65],
872 [622126, 622925],
873 [644, 649]
874 ]);
875 }
876
877 VBOCreditCard.prototype.isJCB = function() {
878 return this.matchBrandRanges([
879 [3528, 3589]
880 ]);
881 }
882
883 VBOCreditCard.prototype.getCardType = function() {
884 if (this.isVisa()) {
885 return VBOCreditCard.VISA;
886 } else if (this.isMasterCard()) {
887 return VBOCreditCard.MASTERCARD;
888 } else if (this.isAmericanExpress()) {
889 return VBOCreditCard.AMERICAN_EXPRESS;
890 } else if (this.isDiners()) {
891 return VBOCreditCard.DINERS;
892 } else if (this.isDiscover()) {
893 return VBOCreditCard.DISCOVER;
894 } else if (this.isJCB()) {
895 return VBOCreditCard.JCB;
896 }
897
898 return '';
899 }
900
901 VBOCreditCard.prototype.getCardLogoURI = function(type) {
902 if (!type) {
903 return '';
904 }
905
906 return this.cards_logo_uri_base + type + '.png';
907 }
908
909 VBOCreditCard.prototype.matchBrandRanges = function(ranges) {
910
911 for (var i = 0; i < ranges.length; i++) {
912 var r = ranges[i];
913
914 if (r.length == 1) {
915
916 if (this.isEnoughSpace((''+r[0]).length) && this.getNumberToIndex((''+r[0]).length) == r[0]) {
917 return true;
918 }
919
920 } else if (r.length == 2) {
921
922 var len = Math.max( (''+r[0]).length, (''+r[1]).length );
923
924 if (this.isEnoughSpace(len)) {
925
926 var val = this.getNumberToIndex(len);
927
928 if (r[0] <= val && val <= r[1]) {
929 return true;
930 }
931
932 }
933
934 }
935
936 }
937
938 return false;
939 }
940
941 VBOCreditCard.prototype.formatCreditCard = function(card_type) {
942 if (card_type === undefined) {
943 card_type = this.getCardType();
944 }
945
946 var blank_spaces = [];
947 if (card_type.length > 0) {
948 blank_spaces = VBOCreditCard.properties[card_type]['blank'];
949 }
950
951 var cc_str = '';
952 for (var i = 0; i < this.card.length; i++) {
953 cc_str += this.card[i];
954 if (blank_spaces.indexOf(i+1) != -1) {
955 cc_str += ' ';
956 }
957 }
958
959 return cc_str;
960 }
961
962 VBOCreditCard.properties = {
963 'visa': {
964 'size': 16,
965 'blank': [4, 8, 12]
966 },
967 'mastercard': {
968 'size': 16,
969 'blank': [4, 8, 12]
970 },
971 'amex': {
972 'size': 15,
973 'blank': [4, 10]
974 },
975 'discover': {
976 'size': 16,
977 'blank': [4, 8, 12]
978 },
979 'diners': {
980 'size': 14,
981 'blank': [4, 8, 12]
982 },
983 'jcb': {
984 'size': 16,
985 'blank': [4, 8, 12]
986 },
987 };
988
989 VBOCreditCard.VISA = 'visa';
990 VBOCreditCard.MASTERCARD = 'mastercard';
991 VBOCreditCard.AMERICAN_EXPRESS = 'amex';
992 VBOCreditCard.DINERS = 'diners';
993 VBOCreditCard.DISCOVER = 'discover';
994 VBOCreditCard.JCB = 'jcb';
995
996 </script>
997 <?php
998 }
999 ?>
1000
1001 <script type="text/javascript">
1002
1003 // store the last processed transaction
1004 var vbo_widget_vt_last_tn = null;
1005
1006 jQuery(function() {
1007
1008 // show loading skeletons
1009 vboWidgetVTerminalFormSkeleton('<?php echo $wrapper_id; ?>');
1010
1011 // when document is ready, load bookings calendar for this widget's instance
1012 vboWidgetVTerminalFormLoad('<?php echo $wrapper_id; ?>', '<?php echo $page_bid; ?>');
1013
1014 // subscribe to the multitask-panel-open event
1015 document.addEventListener(VBOCore.multitask_open_event, function() {
1016 vboWidgetVTerminalMultitaskOpen('<?php echo $wrapper_id; ?>');
1017 });
1018
1019 // subscribe to the multitask-panel-close event to emit the event for the lastly created booking ID
1020 document.addEventListener(VBOCore.multitask_close_event, function() {
1021 if (vbo_widget_vt_last_tn) {
1022 // emit the event with data for anyone who is listening to it
1023 VBOCore.emitEvent('vbo_new_payment_transaction', {
1024 tn: vbo_widget_vt_last_tn
1025 });
1026 }
1027 });
1028
1029 <?php
1030 if ($js_modal_id) {
1031 // widget can be dismissed through the modal
1032 ?>
1033 // subscribe to the modal-dismissed event to emit the event for the lastly created booking ID
1034 document.addEventListener(VBOCore.widget_modal_dismissed + '<?php echo $js_modal_id; ?>', function() {
1035 if (vbo_widget_vt_last_tn) {
1036 // emit the event with data for anyone who is listening to it
1037 VBOCore.emitEvent('vbo_new_payment_transaction', {
1038 tn: vbo_widget_vt_last_tn
1039 });
1040 }
1041 });
1042 <?php
1043 }
1044 ?>
1045
1046 });
1047
1048 </script>
1049
1050 <?php
1051 }
1052
1053 /**
1054 * Attempts to invoke the eligible payment processor assigned to the given reservation.
1055 *
1056 * @param array $booking the reservation record as an associative array.
1057 * @param array $card the card details collected through the Virtual Terminal.
1058 *
1059 * @return object|null the payment processor dispatcher instance or null.
1060 */
1061 protected function getPaymentProcessor(array $booking, array $card = [])
1062 {
1063 $processor = null;
1064 $payment = [];
1065
1066 if (!empty($booking['idpayment'])) {
1067 $payment = VikBooking::getPayment($booking['idpayment']);
1068 }
1069
1070 if ($card) {
1071 // inject CC details for the payment processor
1072 $booking['card'] = $card;
1073 }
1074
1075 if ($payment && VBOPlatformDetection::isWordPress()) {
1076 /**
1077 * @wponly The payment gateway is loaded
1078 * through the apposite dispatcher.
1079 */
1080 JLoader::import('adapter.payment.dispatcher');
1081 $processor = JPaymentDispatcher::getInstance('vikbooking', $payment['file'], $booking, $payment['params']);
1082 } elseif ($payment && VBOPlatformDetection::isJoomla()) {
1083 /**
1084 * @joomlaonly The Payment Factory library will invoke the gateway.
1085 */
1086 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'payments' . DIRECTORY_SEPARATOR . 'libraries' . DIRECTORY_SEPARATOR . 'factory.php';
1087 $processor = VBOPaymentFactory::getPaymentInstance($payment['file'], $booking, $payment['params']);
1088 }
1089
1090 if ($processor && method_exists($processor, 'isDirectChargeSupported') && $processor->isDirectChargeSupported()) {
1091 // set payment method flag loaded
1092 $this->payment_method = $payment;
1093
1094 // return the valid payment processor instance
1095 return $processor;
1096 }
1097
1098 return null;
1099 }
1100
1101 /**
1102 * Given a raw string of credit card key-value pairs from payments log,
1103 * parse the corresponding keys and values into an associative array.
1104 * In case of conflicting keys with the remotely decoded CC details,
1105 * attempts to replace the masked numbers with asterisks.
1106 *
1107 * @param string $cc_payload the raw CC details from payment logs.
1108 * @param array $remote_cc_data associative array of decoded CC data.
1109 *
1110 * @return array associative or empty array.
1111 */
1112 protected function parseCreditCardValuePairs($cc_payload, array $remote_cc_data = [])
1113 {
1114 $cc_value_pairs = [];
1115
1116 if (empty($cc_payload)) {
1117 return $cc_value_pairs;
1118 }
1119
1120 $cc_lines = preg_split("/(\r\n|\n|\r)/", $cc_payload);
1121
1122 foreach ($cc_lines as $cc_line) {
1123 if (strpos($cc_line, ':') === false) {
1124 continue;
1125 }
1126
1127 $cc_line_parts = explode(':', $cc_line);
1128
1129 if (empty($cc_line_parts[0]) || !strlen(trim($cc_line_parts[1]))) {
1130 continue;
1131 }
1132
1133 $key = str_replace(' ', '_', strtolower($cc_line_parts[0]));
1134 $value = trim($cc_line_parts[1]);
1135
1136 if (!empty($remote_cc_data[$key]) && is_string($remote_cc_data[$key]) && strpos($value, '*') !== false) {
1137 // replace masked numbers with remote content
1138 $value = $this->replaceMaskedNumbers($value, $remote_cc_data[$key]);
1139 }
1140
1141 $cc_value_pairs[$key] = $value;
1142 }
1143
1144 return $cc_value_pairs;
1145 }
1146
1147 /**
1148 * Given a local and a remote credit card number string with
1149 * masked symbols, replaces the values in the corresponding
1150 * positions with the unmasked numbers.
1151 *
1152 * @param string $local current string with masked values.
1153 * @param string $remote remote string with unmasked values.
1154 *
1155 * @return string the local string with unmasked values.
1156 */
1157 protected function replaceMaskedNumbers($local, $remote)
1158 {
1159 // split anything but numbers
1160 $numbers = preg_split("/([^0-9]+)/", trim($remote));
1161
1162 if ($numbers) {
1163 // filter empty values
1164 $numbers = array_filter($numbers);
1165 }
1166
1167 if (!$numbers) {
1168 // unable to proceed
1169 return $local;
1170 }
1171
1172 // split anything but stars (asterisks)
1173 $stars = preg_split("/([^\*]+)/", trim($local));
1174
1175 if ($stars) {
1176 // filter empty values
1177 $stars = array_filter($stars);
1178 }
1179
1180 if (!$stars) {
1181 // unable to proceed
1182 return $local;
1183 }
1184
1185 // replace masked symbols with numbers at their first occurrence
1186 foreach ($numbers as $k => $unmasked) {
1187 if (!isset($stars[$k])) {
1188 continue;
1189 }
1190
1191 $masked_pos = strpos($local, $stars[$k]);
1192
1193 if ($masked_pos === false) {
1194 continue;
1195 }
1196
1197 $local = substr_replace($local, $unmasked, $masked_pos, strlen($stars[$k]));
1198 }
1199
1200 // return the string with possibly unmasked values
1201 return $local;
1202 }
1203 }
1204