widgetName = JText::translate('VBO_W_VIRTUALTERMINAL_TITLE'); $this->widgetDescr = JText::translate('VBO_W_VIRTUALTERMINAL_DESCR'); $this->widgetId = basename(__FILE__, '.php'); // define widget and icon and style name $this->widgetIcon = ''; $this->widgetStyleName = 'red'; // whether VCM is available if (is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) { $this->vcm_exists = true; } } /** * Custom method for this widget only to load the virtual terminal CC form. * The method is called by the admin controller through an AJAX request. * The visibility should be public, it should not exit the process, and * any content sent to output will be returned to the AJAX response. * In this case we return an array because this method requires "return":1. */ public function loadTerminalForm() { $wrapper = VikRequest::getString('wrapper', '', 'request'); $bid = VikRequest::getInt('bid', 0, 'request'); if (!$bid) { // booking ID is mandatory VBOHttpDocument::getInstance()->close(500, JText::translate('VBPEDITBUSYONE')); } // get booking details $booking_info = VikBooking::getBookingInfoFromID($bid); if (!$booking_info) { // booking record not found VBOHttpDocument::getInstance()->close(404, JText::translate('VBPEDITBUSYONE')); } // check if direct charge is supported by the current payment processor if (!$this->getPaymentProcessor($booking_info)) { // unable to proceed due to unsupported transaction return [ 'html' => '

' . JText::translate('VBO_PAY_PROCESS_NO_DIRECT_CHARGE') . '

', ]; } // build complete credit card payload, if any $cc_payload_str = ''; $booking_info['paymentlog'] = (string)$booking_info['paymentlog']; if (stripos($booking_info['paymentlog'], 'card number') !== false && strpos($booking_info['paymentlog'], '*') !== false) { // matched a log for an OTA CC $cc_payload_str = $booking_info['paymentlog']; } elseif (preg_match("/(([\d\*]{4,4}\s*){4,4})|(([\d\*]{4,6}\s*){3,3})/", $booking_info['paymentlog'])) { // matched a credit card $cc_payload_str = $booking_info['paymentlog']; } // check if this is an OTA reservation with remotely decoded CC details $remote_cc_data = []; if ($this->vcm_exists && !empty($booking_info['idorderota']) && !empty($booking_info['channel'])) { // make sure the permissions are met if (!JFactory::getUser()->authorise('core.admin', 'com_vikchannelmanager')) { // insufficient permissions to handle CC details return [ 'html' => '

' . JText::translate('JERROR_ALERTNOAUTHOR') . '

', ]; } // channel source $channel_source = (string)$booking_info['channel']; if (strpos($booking_info['channel'], '_') !== false) { $channelparts = explode('_', $booking_info['channel']); $channel_source = $channelparts[0]; } // only updated versions of VCM will support remote CC decoding for OTA reservations if (class_exists('VCMOtaBooking')) { // invoke the OTA Booking helper class from VCM $cc_helper = VCMOtaBooking::getInstance([ 'channel_source' => $channel_source, 'ota_id' => $booking_info['idorderota'], ], $anew = true); if (method_exists($cc_helper, 'decodeCreditCardDetails')) { $remote_cc_data = $cc_helper->decodeCreditCardDetails(); // make sure the response was valid if (!$remote_cc_data || !empty($remote_cc_data['error'])) { // we ignore the error by simply resetting the array $remote_cc_data = []; } } } } // merge remotely decoded CC details to parsed payment log (if any) $cc_value_pairs = array_merge($remote_cc_data, $this->parseCreditCardValuePairs($cc_payload_str, $remote_cc_data)); // currency code $currency_code = !empty($booking_info['chcurrency']) ? $booking_info['chcurrency'] : VikBooking::getCurrencyName(); if (empty($currency_code) || strlen((string)$currency_code) != 3) { // fallback to currency transaction code $currency_code = VikBooking::getCurrencyCodePp(); } // check known CC values to build the hidden transaction values $known_tn_vals = []; $hidden_tn_vals = []; if (isset($cc_value_pairs['name'])) { $known_tn_vals['name'] = $cc_value_pairs['name']; } if (isset($cc_value_pairs['card_number'])) { $known_tn_vals['card_number'] = $cc_value_pairs['card_number']; } if (isset($cc_value_pairs['expiration_date'])) { $known_tn_vals['expiration_date'] = $cc_value_pairs['expiration_date']; } if (isset($cc_value_pairs['cvv'])) { $known_tn_vals['cvv'] = $cc_value_pairs['cvv']; } foreach ($cc_value_pairs as $key => $value) { if (!isset($known_tn_vals[$key])) { $hidden_tn_vals[$key] = $value; } } // start output buffering ob_start(); ?>
$value) { ?>
$html_content, ); } /** * Custom method for this widget only to debit a credit card. * * @return array|void */ public function doDirectCharge() { $wrapper = VikRequest::getString('wrapper', '', 'request'); $bid = VikRequest::getInt('bid', 0, 'request'); $card = VikRequest::getVar('card', [], 'request'); if (!$bid || !$card) { // booking ID and CC details are mandatory VBOHttpDocument::getInstance()->close(500, JText::translate('VBPEDITBUSYONE')); } // get booking details $booking_info = VikBooking::getBookingInfoFromID($bid); if (!$booking_info) { // booking record not found VBOHttpDocument::getInstance()->close(404, JText::translate('VBPEDITBUSYONE')); } // get the eligible payment processor by passing the card details $processor = $this->getPaymentProcessor($booking_info, $card); if (!$processor) { VBOHttpDocument::getInstance()->close(500, JText::translate('VBO_PAY_PROCESS_NO_DIRECT_CHARGE')); } // default transaction response $array_result = [ 'verified' => 0, ]; try { // perform the transaction $array_result = $processor->directCharge(); } catch (Exception $e) { // set error message $array_result['log'] = sprintf(JText::translate('VBO_CC_TN_ERROR') . " \n%s", $e->getMessage()); } if ($array_result['verified'] != 1) { // erroneous response if (!empty($array_result['log']) && is_string($array_result['log'])) { VBOHttpDocument::getInstance()->close(500, $array_result['log']); } else { VBOHttpDocument::getInstance()->close(500, 'Operation failed'); } } // valid transaction response! // update booking details $dbo = JFactory::getDbo(); // get the amount paid $tn_amount = isset($array_result['tot_paid']) ? (float)$array_result['tot_paid'] : null; // get the log string, if any $tn_log = !empty($array_result['log']) ? $array_result['log'] : ''; // update record $upd_record = new stdClass; $upd_record->id = $booking_info['id']; if ($tn_amount) { // update amount paid $upd_record->totpaid = $booking_info['totpaid'] + $tn_amount; // update payable amount (if needed) $new_payable = $booking_info['payable'] - $tn_amount; $new_payable = $new_payable < 0 ? 0 : $new_payable; $upd_record->payable = $new_payable; } if ($tn_log) { $upd_record->paymentlog = $booking_info['paymentlog'] . "\n\n" . date('c') . "\n" . $tn_log; } $upd_record->paymcount = ((int)$booking_info['paymcount'] + 1); // update reservation record $dbo->updateObject('#__vikbooking_orders', $upd_record, 'id'); // payment processor name $pay_process_name = $this->payment_method ? $this->payment_method['name'] : 'CC Direct Charge'; // handle transaction data to eventually support a later transaction of type refund $tn_data = isset($array_result['transaction']) ? $array_result['transaction'] : null; if ($tn_amount) { // check event data payload to store if (is_array($tn_data)) { // set key $tn_data['amount_paid'] = $tn_amount; } elseif (is_object($tn_data)) { // set property $tn_data->amount_paid = $tn_amount; } elseif (!$tn_data) { // build an array (we add the payment name because we know there is no other transaction data) $tn_data = [ 'amount_paid' => $tn_amount, 'payment_method' => $pay_process_name, ]; } } // current admin-user $now_user = JFactory::getUser(); // Booking History $ev_descr = JText::translate('VBO_W_VIRTUALTERMINAL_TITLE') . " - {$pay_process_name} ({$now_user->name})"; VikBooking::getBookingHistoryInstance()->setBid($booking_info['id'])->setExtraData($tn_data)->store('P' . ($booking_info['paymcount'] > 0 ? 'N' : '0'), $ev_descr); return [ 'success' => 1, 'log' => $tn_log, ]; } /** * Preload the necessary assets. * * @return void */ public function preload() { // JS lang def JText::script('VBOUPLOADFILEDONE'); } /** * Main method to invoke the widget. Contents will be loaded * through AJAX requests, not via PHP when the page loads. * * @param VBOMultitaskData $data * * @return void */ public function render(VBOMultitaskData $data = null) { // increase widget's instance counter static::$instance_counter++; // check whether the widget is being rendered via AJAX when adding it through the customizer $is_ajax = $this->isAjaxRendering(); // generate a unique ID for the sticky notes wrapper instance $wrapper_instance = !$is_ajax ? static::$instance_counter : rand(); $wrapper_id = 'vbo-widget-vterminal-' . $wrapper_instance; // get permissions $vbo_auth_bookings = JFactory::getUser()->authorise('core.vbo.bookings', 'com_vikbooking'); if (!$vbo_auth_bookings) { // display nothing return; } // check multitask data $page_bid = 0; $js_modal_id = ''; if ($data) { $is_modal_rendering = $data->isModalRendering(); $page_bid = $data->getBookingID(); if ($is_modal_rendering) { // get modal JS identifier $js_modal_id = $data->getModalJsIdentifier(); } } if (!$page_bid) { // unable to continue from a page outside the booking details or booking ID set ?>

widgetIcon; ?> widgetName; ?>

isDirectChargeSupported()) { // set payment method flag loaded $this->payment_method = $payment; // return the valid payment processor instance return $processor; } return null; } /** * Given a raw string of credit card key-value pairs from payments log, * parse the corresponding keys and values into an associative array. * In case of conflicting keys with the remotely decoded CC details, * attempts to replace the masked numbers with asterisks. * * @param string $cc_payload the raw CC details from payment logs. * @param array $remote_cc_data associative array of decoded CC data. * * @return array associative or empty array. */ protected function parseCreditCardValuePairs($cc_payload, array $remote_cc_data = []) { $cc_value_pairs = []; if (empty($cc_payload)) { return $cc_value_pairs; } $cc_lines = preg_split("/(\r\n|\n|\r)/", $cc_payload); foreach ($cc_lines as $cc_line) { if (strpos($cc_line, ':') === false) { continue; } $cc_line_parts = explode(':', $cc_line); if (empty($cc_line_parts[0]) || !strlen(trim($cc_line_parts[1]))) { continue; } $key = str_replace(' ', '_', strtolower($cc_line_parts[0])); $value = trim($cc_line_parts[1]); if (!empty($remote_cc_data[$key]) && is_string($remote_cc_data[$key]) && strpos($value, '*') !== false) { // replace masked numbers with remote content $value = $this->replaceMaskedNumbers($value, $remote_cc_data[$key]); } $cc_value_pairs[$key] = $value; } return $cc_value_pairs; } /** * Given a local and a remote credit card number string with * masked symbols, replaces the values in the corresponding * positions with the unmasked numbers. * * @param string $local current string with masked values. * @param string $remote remote string with unmasked values. * * @return string the local string with unmasked values. */ protected function replaceMaskedNumbers($local, $remote) { // split anything but numbers $numbers = preg_split("/([^0-9]+)/", trim($remote)); if ($numbers) { // filter empty values $numbers = array_filter($numbers); } if (!$numbers) { // unable to proceed return $local; } // split anything but stars (asterisks) $stars = preg_split("/([^\*]+)/", trim($local)); if ($stars) { // filter empty values $stars = array_filter($stars); } if (!$stars) { // unable to proceed return $local; } // replace masked symbols with numbers at their first occurrence foreach ($numbers as $k => $unmasked) { if (!isset($stars[$k])) { continue; } $masked_pos = strpos($local, $stars[$k]); if ($masked_pos === false) { continue; } $local = substr_replace($local, $unmasked, $masked_pos, strlen($stars[$k])); } // return the string with possibly unmasked values return $local; } }