PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.6.7
VikBooking Hotel Booking Engine & PMS v1.6.7
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.7, at admin/helpers/widgets/virtual_terminal.php

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