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

1,267 lines 37.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package VikBooking
4 * @subpackage 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 /**
538 * Check if the payment processor returned the information about the amount of processing fees.
539 *
540 * @since 1.16.9 (J) - 1.6.9 (WP)
541 */
542 if ($tn_data && isset($array_result['tot_fees']) && $array_result['tot_fees']) {
543 // check event data payload to store
544 if (is_array($tn_data)) {
545 // set key
546 $tn_data['processing_fees'] = (float)$array_result['tot_fees'];
547 } elseif (is_object($tn_data)) {
548 // set property
549 $tn_data->processing_fees = (float)$array_result['tot_fees'];
550 }
551 }
552
553 // current admin-user
554 $now_user = JFactory::getUser();
555
556 // Booking History
557 $ev_descr = JText::translate('VBO_W_VIRTUALTERMINAL_TITLE') . " - {$pay_process_name} ({$now_user->name})";
558 VikBooking::getBookingHistoryInstance()->setBid($booking_info['id'])->setExtraData($tn_data)->store('P' . ($booking_info['paymcount'] > 0 ? 'N' : '0'), $ev_descr);
559
560 return [
561 'success' => 1,
562 'log' => $tn_log,
563 ];
564 }
565
566 /**
567 * Preload the necessary assets.
568 *
569 * @return void
570 */
571 public function preload()
572 {
573 // JS lang def
574 JText::script('VBOUPLOADFILEDONE');
575 }
576
577 /**
578 * Main method to invoke the widget. Contents will be loaded
579 * through AJAX requests, not via PHP when the page loads.
580 *
581 * @param VBOMultitaskData $data
582 *
583 * @return void
584 */
585 public function render(VBOMultitaskData $data = null)
586 {
587 // increase widget's instance counter
588 static::$instance_counter++;
589
590 // check whether the widget is being rendered via AJAX when adding it through the customizer
591 $is_ajax = $this->isAjaxRendering();
592
593 // generate a unique ID for the sticky notes wrapper instance
594 $wrapper_instance = !$is_ajax ? static::$instance_counter : rand();
595 $wrapper_id = 'vbo-widget-vterminal-' . $wrapper_instance;
596
597 // get permissions
598 $vbo_auth_bookings = JFactory::getUser()->authorise('core.vbo.bookings', 'com_vikbooking');
599 if (!$vbo_auth_bookings) {
600 // display nothing
601 return;
602 }
603
604 // check multitask data
605 $page_bid = 0;
606 $js_modal_id = '';
607 if ($data) {
608 $is_modal_rendering = $data->isModalRendering();
609 $page_bid = $data->getBookingID() ?: $this->options()->fetchBookingId();
610 if ($is_modal_rendering) {
611 // get modal JS identifier
612 $js_modal_id = $data->getModalJsIdentifier();
613 }
614 }
615
616 if (!$page_bid) {
617 // unable to continue from a page outside the booking details or booking ID set
618 ?>
619 <p class="warn"><?php echo JText::translate('VBPEDITBUSYONE'); ?></p>
620 <?php
621 return;
622 }
623
624 ?>
625 <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; ?>">
626 <div class="vbo-admin-widget-head">
627 <div class="vbo-admin-widget-head-inline">
628 <h4><?php echo $this->widgetIcon; ?> <span><?php echo $this->widgetName; ?></span></h4>
629 </div>
630 </div>
631 <div class="vbo-widget-vterminal-wrap">
632 <div class="vbo-widget-vterminal-inner">
633
634 <div class="vbo-widget-vterminal-form"></div>
635
636 </div>
637 </div>
638 </div>
639 <?php
640
641 if (static::$instance_counter === 0 || $is_ajax) {
642 /**
643 * Print the JS code only once for all instances of this widget.
644 * The real rendering is made through AJAX, not when the page loads.
645 */
646 ?>
647
648 <script type="text/javascript">
649
650 /**
651 * Default icons for status.
652 */
653 var vbo_vt_icon_error = '<?php VikBookingIcons::e('exclamation-circle'); ?>';
654 var vbo_vt_icon_success = '<?php VikBookingIcons::e('check-circle'); ?>';
655
656 /**
657 * Perform the request to load the virtual terminal form.
658 */
659 function vboWidgetVTerminalFormLoad(wrapper, page_bid) {
660 var widget_instance = jQuery('#' + wrapper);
661 if (!widget_instance.length) {
662 return false;
663 }
664
665 // check if multitask data passed a booking ID for the current page
666 var force_bid = 0;
667 if (typeof page_bid !== 'undefined' && page_bid) {
668 force_bid = page_bid;
669 }
670
671 // the widget method to call
672 var call_method = 'loadTerminalForm';
673
674 // make a request to load the bookings calendar
675 VBOCore.doAjax(
676 "<?php echo $this->getExecWidgetAjaxUri(); ?>",
677 {
678 widget_id: "<?php echo $this->getIdentifier(); ?>",
679 call: call_method,
680 return: 1,
681 bid: force_bid,
682 wrapper: wrapper,
683 tmpl: "component"
684 },
685 (response) => {
686 try {
687 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
688 if (!obj_res.hasOwnProperty(call_method)) {
689 console.error('Unexpected JSON response', obj_res);
690 return false;
691 }
692
693 // replace HTML content
694 widget_instance.find('.vbo-widget-vterminal-form').html(obj_res[call_method]['html']);
695 } catch(err) {
696 console.error('could not parse JSON response', err, response);
697 }
698 },
699 (error) => {
700 // remove the skeleton loading
701 widget_instance.find('.vbo-widget-vterminal-form').find('.vbo-skeleton-loading').remove();
702 // display error message
703 alert(error.responseText);
704 }
705 );
706 }
707
708 /**
709 * Performs the request to charge the given card details.
710 */
711 function vboWidgetVTerminalChargeCard(wrapper) {
712 var widget_instance = jQuery('#' + wrapper);
713 if (!widget_instance.length) {
714 return false;
715 }
716
717 // the trigger button
718 var charge_cmd = widget_instance.find('.vbo-vterminal-cc-row-submit').find('button');
719
720 // disable button
721 charge_cmd.prop('disabled', true);
722
723 if (VBOCore.options.default_loading_body) {
724 // show loading spinner
725 charge_cmd.find('i').replaceWith(VBOCore.options.default_loading_body);
726 }
727
728 // gather all CC fields
729 var cc_fields = {};
730 widget_instance.find('.vbo-widget-vterminal-form').find('[data-vt-cc-field]').each(function() {
731 var cc_f_key = jQuery(this).attr('data-vt-cc-field');
732 cc_fields[cc_f_key] = jQuery(this).val();
733 });
734
735 // get the booking ID for the transaction
736 var force_bid = widget_instance.attr('data-pagebid');
737
738 // the widget method to call
739 var call_method = 'doDirectCharge';
740
741 // make a request to load the bookings calendar
742 VBOCore.doAjax(
743 "<?php echo $this->getExecWidgetAjaxUri(); ?>",
744 {
745 widget_id: "<?php echo $this->getIdentifier(); ?>",
746 call: call_method,
747 return: 1,
748 bid: force_bid,
749 card: cc_fields,
750 wrapper: wrapper,
751 tmpl: "component"
752 },
753 (response) => {
754 try {
755 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
756 if (!obj_res.hasOwnProperty(call_method)) {
757 console.error('Unexpected JSON response', obj_res);
758 return false;
759 }
760
761 // turn flag on
762 vbo_widget_vt_last_tn = 1;
763
764 // update button status
765 charge_cmd.removeClass('vbo-config-btn').addClass('btn-success').html(vbo_vt_icon_success + ' ' + Joomla.JText._('VBOUPLOADFILEDONE'));
766
767 // check if we need to dismiss the modal widget
768 var js_modal_id = widget_instance.attr('data-modalid');
769 if (js_modal_id) {
770 setTimeout(() => {
771 // dismiss modal widget
772 VBOCore.emitEvent('vbo-dismiss-widget-modal' + js_modal_id);
773 }, 1500);
774 }
775 } catch(err) {
776 console.error('could not parse JSON response', err, response);
777 }
778 },
779 (error) => {
780 // display error message
781 alert(error.responseText);
782 // restore button
783 charge_cmd.prop('disabled', false);
784 charge_cmd.find('i').replaceWith(vbo_vt_icon_error);
785 }
786 );
787 }
788
789 /**
790 * Generate the HTML skeleton loading string to build the form.
791 */
792 function vboWidgetVTerminalFormSkeleton(wrapper) {
793 var widget_instance = jQuery('#' + wrapper);
794 if (!widget_instance.length) {
795 return false;
796 }
797
798 var def_loading = '';
799 def_loading += '<div class="vbo-dashboard-guest-activity vbo-dashboard-guest-activity-skeleton">';
800 def_loading += ' <div class="vbo-dashboard-guest-activity-avatar">';
801 def_loading += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-avatar"></div>';
802 def_loading += ' </div>';
803 def_loading += ' <div class="vbo-dashboard-guest-activity-content">';
804 def_loading += ' <div class="vbo-dashboard-guest-activity-content-head">';
805 def_loading += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-title"></div>';
806 def_loading += ' </div>';
807 def_loading += ' <div class="vbo-dashboard-guest-activity-content-subhead">';
808 def_loading += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-subtitle"></div>';
809 def_loading += ' </div>';
810 def_loading += ' <div class="vbo-dashboard-guest-activity-content-info-msg">';
811 def_loading += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-content"></div>';
812 def_loading += ' </div>';
813 def_loading += ' </div>';
814 def_loading += '</div>';
815
816 widget_instance.find('.vbo-widget-vterminal-form').html(def_loading);
817 }
818
819 /**
820 * Triggers when the multitask panel opens.
821 */
822 function vboWidgetVTerminalMultitaskOpen(wrapper) {
823 var widget_instance = jQuery('#' + wrapper);
824 if (!widget_instance.length) {
825 return false;
826 }
827
828 // check if a booking ID was set for this page
829 var page_bid = widget_instance.attr('data-pagebid');
830 if (!page_bid || page_bid < 1) {
831 return false;
832 }
833
834 // show loading skeletons
835 vboWidgetVTerminalFormSkeleton(wrapper);
836
837 // load data by injecting the current booking ID
838 vboWidgetVTerminalFormLoad(wrapper, page_bid);
839 }
840
841 /**
842 * Credit Card Detector Class.
843 */
844 function VBOCreditCard(card) {
845 this.set(card);
846 }
847
848 VBOCreditCard.prototype.set = function(card) {
849 this.cards_logo_uri_base = '<?php echo VBO_ADMIN_URI . 'resources/js_upload/images/'; ?>';
850 this.card = [];
851 for (var i = 0; i < card.length; i++) {
852 var ch = card.charCodeAt(i);
853 if (ch >= 48 && ch <= 57) {
854 this.card.push(ch-48);
855 }
856 }
857 }
858
859 VBOCreditCard.prototype.get = function() {
860 return this.card;
861 }
862
863 VBOCreditCard.prototype.isEnoughSpace = function(len) {
864 return ( this.card.length >= len );
865 }
866
867 VBOCreditCard.prototype.isEmpty = function() {
868 return !this.isEnoughSpace(1);
869 }
870
871 VBOCreditCard.prototype.isValid = function() {
872 const type = this.getCardType();
873
874 if (type.length && this.card.length == VBOCreditCard.properties[type].size) {
875 return true;
876 }
877
878 return false;
879 }
880
881 VBOCreditCard.prototype.getNumberToIndex = function(i) {
882 var n = 0;
883 var factor = 1;
884 for (i = i-1 ; i >= 0; i--) {
885 n += factor * this.card[i];
886 factor *= 10;
887 }
888 return n;
889 }
890
891 VBOCreditCard.prototype.isVisa = function() {
892 return this.matchBrandRanges([
893 [4]
894 ]);
895 }
896
897 VBOCreditCard.prototype.isMasterCard = function() {
898 return this.matchBrandRanges([
899 [51, 55],
900 [2221, 2720]
901 ]);
902 }
903
904 VBOCreditCard.prototype.isAmericanExpress = function() {
905 return this.matchBrandRanges([
906 [34],
907 [37]
908 ]);
909 }
910
911 VBOCreditCard.prototype.isDiners = function() {
912 return this.matchBrandRanges([
913 [300, 305],
914 [36],
915 [38, 39]
916 ]);
917 }
918
919 VBOCreditCard.prototype.isDiscover = function() {
920 return this.matchBrandRanges([
921 [6011],
922 [65],
923 [622126, 622925],
924 [644, 649]
925 ]);
926 }
927
928 VBOCreditCard.prototype.isJCB = function() {
929 return this.matchBrandRanges([
930 [3528, 3589]
931 ]);
932 }
933
934 VBOCreditCard.prototype.getCardType = function() {
935 if (this.isVisa()) {
936 return VBOCreditCard.VISA;
937 } else if (this.isMasterCard()) {
938 return VBOCreditCard.MASTERCARD;
939 } else if (this.isAmericanExpress()) {
940 return VBOCreditCard.AMERICAN_EXPRESS;
941 } else if (this.isDiners()) {
942 return VBOCreditCard.DINERS;
943 } else if (this.isDiscover()) {
944 return VBOCreditCard.DISCOVER;
945 } else if (this.isJCB()) {
946 return VBOCreditCard.JCB;
947 }
948
949 return '';
950 }
951
952 VBOCreditCard.prototype.getCardLogoURI = function(type) {
953 if (!type) {
954 return '';
955 }
956
957 return this.cards_logo_uri_base + type + '.png';
958 }
959
960 VBOCreditCard.prototype.matchBrandRanges = function(ranges) {
961
962 for (var i = 0; i < ranges.length; i++) {
963 var r = ranges[i];
964
965 if (r.length == 1) {
966
967 if (this.isEnoughSpace((''+r[0]).length) && this.getNumberToIndex((''+r[0]).length) == r[0]) {
968 return true;
969 }
970
971 } else if (r.length == 2) {
972
973 var len = Math.max( (''+r[0]).length, (''+r[1]).length );
974
975 if (this.isEnoughSpace(len)) {
976
977 var val = this.getNumberToIndex(len);
978
979 if (r[0] <= val && val <= r[1]) {
980 return true;
981 }
982
983 }
984
985 }
986
987 }
988
989 return false;
990 }
991
992 VBOCreditCard.prototype.formatCreditCard = function(card_type) {
993 if (card_type === undefined) {
994 card_type = this.getCardType();
995 }
996
997 var blank_spaces = [];
998 if (card_type.length > 0) {
999 blank_spaces = VBOCreditCard.properties[card_type]['blank'];
1000 }
1001
1002 var cc_str = '';
1003 for (var i = 0; i < this.card.length; i++) {
1004 cc_str += this.card[i];
1005 if (blank_spaces.indexOf(i+1) != -1) {
1006 cc_str += ' ';
1007 }
1008 }
1009
1010 return cc_str;
1011 }
1012
1013 VBOCreditCard.properties = {
1014 'visa': {
1015 'size': 16,
1016 'blank': [4, 8, 12]
1017 },
1018 'mastercard': {
1019 'size': 16,
1020 'blank': [4, 8, 12]
1021 },
1022 'amex': {
1023 'size': 15,
1024 'blank': [4, 10]
1025 },
1026 'discover': {
1027 'size': 16,
1028 'blank': [4, 8, 12]
1029 },
1030 'diners': {
1031 'size': 14,
1032 'blank': [4, 8, 12]
1033 },
1034 'jcb': {
1035 'size': 16,
1036 'blank': [4, 8, 12]
1037 },
1038 };
1039
1040 VBOCreditCard.VISA = 'visa';
1041 VBOCreditCard.MASTERCARD = 'mastercard';
1042 VBOCreditCard.AMERICAN_EXPRESS = 'amex';
1043 VBOCreditCard.DINERS = 'diners';
1044 VBOCreditCard.DISCOVER = 'discover';
1045 VBOCreditCard.JCB = 'jcb';
1046
1047 </script>
1048 <?php
1049 }
1050 ?>
1051
1052 <script type="text/javascript">
1053
1054 // store the last processed transaction
1055 var vbo_widget_vt_last_tn = null;
1056
1057 jQuery(function() {
1058
1059 // show loading skeletons
1060 vboWidgetVTerminalFormSkeleton('<?php echo $wrapper_id; ?>');
1061
1062 // when document is ready, load bookings calendar for this widget's instance
1063 vboWidgetVTerminalFormLoad('<?php echo $wrapper_id; ?>', '<?php echo $page_bid; ?>');
1064
1065 // subscribe to the multitask-panel-open event
1066 document.addEventListener(VBOCore.multitask_open_event, function() {
1067 vboWidgetVTerminalMultitaskOpen('<?php echo $wrapper_id; ?>');
1068 });
1069
1070 // subscribe to the multitask-panel-close event to emit the event for the lastly created booking ID
1071 document.addEventListener(VBOCore.multitask_close_event, function() {
1072 if (vbo_widget_vt_last_tn) {
1073 // emit the event with data for anyone who is listening to it
1074 VBOCore.emitEvent('vbo_new_payment_transaction', {
1075 tn: vbo_widget_vt_last_tn
1076 });
1077 }
1078 });
1079
1080 <?php
1081 if ($js_modal_id) {
1082 // widget can be dismissed through the modal
1083 ?>
1084 // subscribe to the modal-dismissed event to emit the event for the lastly created booking ID
1085 document.addEventListener(VBOCore.widget_modal_dismissed + '<?php echo $js_modal_id; ?>', function() {
1086 if (vbo_widget_vt_last_tn) {
1087 // emit the event with data for anyone who is listening to it
1088 VBOCore.emitEvent('vbo_new_payment_transaction', {
1089 tn: vbo_widget_vt_last_tn
1090 });
1091 }
1092 });
1093 <?php
1094 }
1095 ?>
1096
1097 });
1098
1099 </script>
1100
1101 <?php
1102 }
1103
1104 /**
1105 * Attempts to invoke the eligible payment processor assigned to the given reservation.
1106 *
1107 * @param array $booking the reservation record as an associative array.
1108 * @param array $card the card details collected through the Virtual Terminal.
1109 *
1110 * @return object|null the payment processor dispatcher instance or null.
1111 */
1112 protected function getPaymentProcessor(array $booking, array $card = [])
1113 {
1114 $processor = null;
1115 $payment = [];
1116
1117 if (!empty($booking['idpayment'])) {
1118 $payment = VikBooking::getPayment($booking['idpayment']);
1119 }
1120
1121 if ($card) {
1122 // inject CC details for the payment processor
1123 $booking['card'] = $card;
1124 }
1125
1126 if ($payment && VBOPlatformDetection::isWordPress()) {
1127 /**
1128 * @wponly The payment gateway is loaded
1129 * through the apposite dispatcher.
1130 */
1131 JLoader::import('adapter.payment.dispatcher');
1132 $processor = JPaymentDispatcher::getInstance('vikbooking', $payment['file'], $booking, $payment['params']);
1133 } elseif ($payment && VBOPlatformDetection::isJoomla()) {
1134 /**
1135 * @joomlaonly The Payment Factory library will invoke the gateway.
1136 */
1137 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'payments' . DIRECTORY_SEPARATOR . 'libraries' . DIRECTORY_SEPARATOR . 'factory.php';
1138 $processor = VBOPaymentFactory::getPaymentInstance($payment['file'], $booking, $payment['params']);
1139 }
1140
1141 if ($processor && method_exists($processor, 'isDirectChargeSupported') && $processor->isDirectChargeSupported()) {
1142 // set payment method flag loaded
1143 $this->payment_method = $payment;
1144
1145 // return the valid payment processor instance
1146 return $processor;
1147 }
1148
1149 return null;
1150 }
1151
1152 /**
1153 * Given a raw string of credit card key-value pairs from payments log,
1154 * parse the corresponding keys and values into an associative array.
1155 * In case of conflicting keys with the remotely decoded CC details,
1156 * attempts to replace the masked numbers with asterisks.
1157 *
1158 * @param string $cc_payload the raw CC details from payment logs.
1159 * @param array $remote_cc_data associative array of decoded CC data.
1160 *
1161 * @return array associative or empty array.
1162 */
1163 protected function parseCreditCardValuePairs($cc_payload, array $remote_cc_data = [])
1164 {
1165 $cc_value_pairs = [];
1166
1167 if (empty($cc_payload)) {
1168 return $cc_value_pairs;
1169 }
1170
1171 $cc_lines = preg_split("/(\r\n|\n|\r)/", $cc_payload);
1172
1173 foreach ($cc_lines as $cc_line) {
1174 if (strpos($cc_line, ':') === false) {
1175 continue;
1176 }
1177
1178 $cc_line_parts = explode(':', $cc_line);
1179
1180 if (empty($cc_line_parts[0]) || !strlen(trim($cc_line_parts[1]))) {
1181 continue;
1182 }
1183
1184 $key = str_replace(' ', '_', strtolower($cc_line_parts[0]));
1185 $value = trim($cc_line_parts[1]);
1186
1187 if (isset($cc_value_pairs[$key])) {
1188 /**
1189 * Do not overwrite existing keys because this probably means that the
1190 * credit card was updated by an OTA like Booking.com, hence the payment
1191 * logs string in VBO may contain the information of two different cards.
1192 * New credit card details are always pre-pended by VCM in the payment logs.
1193 *
1194 * @since 1.16.5 (J) - 1.6.5 (WP)
1195 */
1196 continue;
1197 }
1198
1199 if (!empty($remote_cc_data[$key]) && is_string($remote_cc_data[$key]) && strpos($value, '*') !== false) {
1200 // replace masked numbers with remote content
1201 $value = $this->replaceMaskedNumbers($value, $remote_cc_data[$key]);
1202 }
1203
1204 $cc_value_pairs[$key] = $value;
1205 }
1206
1207 return $cc_value_pairs;
1208 }
1209
1210 /**
1211 * Given a local and a remote credit card number string with
1212 * masked symbols, replaces the values in the corresponding
1213 * positions with the unmasked numbers.
1214 *
1215 * @param string $local current string with masked values.
1216 * @param string $remote remote string with unmasked values.
1217 *
1218 * @return string the local string with unmasked values.
1219 */
1220 protected function replaceMaskedNumbers($local, $remote)
1221 {
1222 // split anything but numbers
1223 $numbers = preg_split("/([^0-9]+)/", trim($remote));
1224
1225 if ($numbers) {
1226 // filter empty values
1227 $numbers = array_filter($numbers);
1228 }
1229
1230 if (!$numbers) {
1231 // unable to proceed
1232 return $local;
1233 }
1234
1235 // split anything but stars (asterisks)
1236 $stars = preg_split("/([^\*]+)/", trim($local));
1237
1238 if ($stars) {
1239 // filter empty values
1240 $stars = array_filter($stars);
1241 }
1242
1243 if (!$stars) {
1244 // unable to proceed
1245 return $local;
1246 }
1247
1248 // replace masked symbols with numbers at their first occurrence
1249 foreach ($numbers as $k => $unmasked) {
1250 if (!isset($stars[$k])) {
1251 continue;
1252 }
1253
1254 $masked_pos = strpos($local, $stars[$k]);
1255
1256 if ($masked_pos === false) {
1257 continue;
1258 }
1259
1260 $local = substr_replace($local, $unmasked, $masked_pos, strlen($stars[$k]));
1261 }
1262
1263 // return the string with possibly unmasked values
1264 return $local;
1265 }
1266 }
1267