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();
?>
$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
?>
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;
}
}