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

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