addStyleSheet(VBO_ADMIN_URI . 'helpers/' . 'wp.css', ['version' => VIKBOOKING_SOFTWARE_VERSION], ['id' => 'vbo-wp-style']);
}
}
/**
* Includes a script URI.
*
* @param string $uri The script URI.
*
* @return void
*/
public function addScript($uri)
{
JHtml::fetch('script', $uri);
}
/**
* Sets additional commands for any methods. Like raise an error if the recipient email address is empty.
* Returns this object for chainability.
*/
public function setCommand($key, $value)
{
if (!empty($key)) {
$this->commands[$key] = $value;
}
return $this;
}
public function sendMail($from_address, $from_name, $to, $reply_address, $subject, $hmess, $is_html = true, $encoding = 'base64', $attachment = null)
{
if (!is_array($to) && strpos($to, ',') !== false) {
$all_recipients = explode(',', $to);
foreach ($all_recipients as $k => $v) {
if (empty($v)) {
unset($all_recipients[$k]);
}
}
if (count($all_recipients) > 0) {
$to = $all_recipients;
}
}
if (empty($to)) {
//Prevent Joomla Exceptions that would stop the script execution
if (isset($this->commands['print_errors'])) {
VikError::raiseWarning('', 'The recipient email address is empty. Email message could not be sent. Please check your configuration.');
}
return false;
}
if ($from_name == $from_address) {
$mainframe = JFactory::getApplication();
$attempt_fromn = $mainframe->get('fromname', '');
if (!empty($attempt_fromn)) {
$from_name = $attempt_fromn;
}
}
/**
* Conditional text rules may set extra recipients or attachments.
*
* @since 1.16.0 (J) - 1.6.0 (WP)
*/
$extra_admin_recipients = VikBooking::addAdminEmailRecipient(null);
$bcc_addresses = VikBooking::addAdminEmailRecipient(null, $bcc = true);
$extra_attachments = VikBooking::addEmailAttachment(null);
if ($extra_admin_recipients) {
// cast a possible string to array
$to = (array) $to;
// merge additional recipients
$to = array_merge($to, $extra_admin_recipients);
}
if ($extra_attachments) {
$attachment = $attachment ? (array) $attachment : [];
$attachment = array_merge($attachment, $extra_attachments);
}
/**
* We let the internal library process the email sending depending on the platform.
* This will allow us to perform the required manipulation of the content, if needed.
*
* @since 1.15.2 (J) - 1.5.5 (WP)
*/
$mail_data = new VBOMailWrapper([
'sender' => [$from_address, $from_name],
'recipient' => $to,
'bcc' => $bcc_addresses,
'reply' => $reply_address,
'subject' => $subject,
'content' => $hmess,
'attachments' => $attachment,
]);
// unset queues for the next email sending operation
VikBooking::addAdminEmailRecipient(null, false, $reset = true);
VikBooking::addEmailAttachment(null, $reset = true);
// dispatch the email sending command
return VBOFactory::getPlatform()->getMailer()->send($mail_data);
}
/**
* @param $arr_values array
* @param $current_key string
* @param $empty_value string (J3.x only)
* @param $default
* @param $input_name string
* @param $record_id = '' string
*/
public function getDropDown($arr_values, $current_key, $empty_value, $default, $input_name, $record_id = '')
{
$dropdown = '';
$dropdown .= ''."\n";
return $dropdown;
}
public function loadSelect2()
{
static $st_loaded = null;
if ($st_loaded) {
// loaded flag
return;
}
// cache loaded flag
$st_loaded = 1;
// load JS + CSS
$document = JFactory::getDocument();
$document->addStyleSheet(VBO_ADMIN_URI.'resources/select2.min.css');
$this->addScript(VBO_ADMIN_URI.'resources/select2.min.js');
}
/**
* Returns the HTML code to render a regular dropdown
* menu styled through the jQuery plugin Select2.
*
* @param $arr_values array
* @param $current_key string
* @param $input_name string
* @param $placeholder string used when the select has no selected option (it's empty)
* @param $empty_name [string] the name of the option to set an empty value to the field ()
* @param $empty_val [string] the value of the option to set an empty value to the field ()
* @param $onchange [string] javascript code for the onchange attribute
* @param $idattr [string] the identifier attribute of the select
*
* @return string
*/
public function getNiceSelect($arr_values, $current_key, $input_name, $placeholder, $empty_name = '', $empty_val = '', $onchange = 'document.adminForm.submit();', $idattr = '')
{
//load JS + CSS
$this->loadSelect2();
//attribute
$idattr = empty($idattr) ? rand(1, 999) : $idattr;
//select
$dropdown = ''."\n";
//js code
$dropdown .= ''."\n";
return $dropdown;
}
/**
* Adds the script declaration to render the Bootstrap JModal window.
* The suffix can be passed to generate other JS functions.
* Optionally pass JavaScript code for the 'show' and 'hide' events.
* For compatibility with the Joomla framework, this method should be
* echoed although it does not return anything on WordPress.
*
* @param $suffix string
* @param $hide_js string
* @param $show_js string
*
* @return void should still be echoed for compatibility with J.
*/
public function getJmodalScript($suffix = '', $hide_js = '', $show_js = '')
{
static $loaded = [];
$doc = JFactory::getDocument();
if (!isset($loaded[$suffix]))
{
$doc->addScriptDeclaration(
<< VikBooking::getCurrencySymb() ?: '$',
'position' => VikBooking::getCurrencyPosition(),
'digits' => (int) $currency_digits,
'decimals' => $currency_decimals ?: '.',
'thousands' => $currency_thousands ?: ',',
'noDecimals' => 1,
];
// merge settings and encode them in JSON
$config = json_encode(array_merge($config, $options));
// build script declaration
$decl = << {
VBOCore.getCurrency($config);
});
JAVASCRIPT;
// add script declaration to document
JFactory::getDocument()->addScriptDeclaration($decl);
}
/**
* Renders a dual slider element composed of two native range input
* elements to pick a minimum and a maximum value.
*
* @param array $options Optional list of currency settings.
*
* @return string The HTML string necessary to render the dual slider element.
*
* @since 1.18.3 (J) - 1.8.3 (WP)
*/
public function renderDualSlider(array $options = [])
{
static $dual_slider_instances = -1;
// increase instance counter
$dual_slider_instances++;
// extra container class and suffix
$extra_class = $options['class'] ?? '';
$extra_class = $extra_class ? ' ' . $extra_class : $extra_class;
$suffix_elem = preg_replace('/[^a-z0-9\-_]+/i', '', (string) ($options['suffix'] ?? ''));
if (empty($options['suffix'])) {
// the suffix is required
$options['suffix'] = uniqid('ds_');
}
// minimum and maximum range values
$min_range = floor((float) ($options['min'] ?? 0));
$max_range = ceil((float) ($options['max'] ?? 100));
$max_range = $max_range <= $min_range ? ($min_range + 1) : $max_range;
// default range values
$min_default = (float) ($options['min_def'] ?? 0);
$max_default = (float) ($options['max_def'] ?? 100);
$max_default = $max_default < $min_default ? $min_default : $max_default;
// range steps
$range_diff = $max_range - $min_range;
if (empty($options['step']) || ($options['step'] ?? '') === 'auto') {
// calculate the best step value
if ($range_diff < 50) {
$step_default = 1;
} elseif ($range_diff < 500) {
$step_default = 5;
} elseif ($range_diff < 2000) {
$step_default = 10;
} else {
$step_default = 50;
}
} else {
$step_default = (float) ($options['step'] ?? 5);
}
$step_min = (float) ($options['step_min'] ?? $step_default);
$step_max = (float) ($options['step_max'] ?? $step_default);
if (empty($options['fixed_range'])) {
// ensure ranges allow to fullfil the first or last step for both ranges
if ($remainder = ($min_range % $step_min)) {
$min_range -= $remainder;
}
if ($remainder = ($max_range % $step_max)) {
$max_range += $step_max - $remainder;
}
$min_range = $min_range < 0 ? 0 : $min_range;
$max_range = $max_range < 0 ? 0 : $max_range;
// ensure the min/max default values can fulfill the step
if ($remainder = ($min_default % $step_min)) {
// i.e. browser behavior: step 5 - 422 goes to 420 - while 423 goes to 425
if ($remainder > ($step_min / 2)) {
$min_default -= $remainder;
}
}
if ($remainder = ($max_default % $step_max)) {
// i.e. browser behavior: step 5 - 822 goes to 820 - while 823 goes to 825
if ($remainder < ($step_max / 2)) {
$max_default += $step_max - $remainder;
}
}
}
// input name attributes
$min_name = $options['min_name'] ?? '';
$max_name = $options['max_name'] ?? '';
$min_name = $min_name ? 'name="' . $min_name . '"' : $min_name;
$max_name = $max_name ? 'name="' . $max_name . '"' : $max_name;
// current range and value HTML/text templates
$value_tpl = ((string) ($options['value_format'] ?? '')) ?: '%d - %d';
$value_data = sprintf($value_tpl, $min_default, $max_default);
$value_html = <<{$value_data}
HTML;
$range_tpl = (string) ($options['range_tpl'] ?? '');
$range_html = $range_tpl ? sprintf($range_tpl, $value_html) : '';
if ($range_html) {
// enclose within fixed HTML container
$range_html = <<{$range_html}
HTML;
}
// build script content
$jscript = <<
function vboUpdateSliderTrack(trackSuffix) {
const dualSlider = document.getElementById('vbo-dual-slider-' + trackSuffix);
const minRange = dualSlider.querySelector('input[type="range"].vbo-dual-slider-range-min');
const maxRange = dualSlider.querySelector('input[type="range"].vbo-dual-slider-range-max');
const track = dualSlider.querySelector('.vbo-dual-slider-track');
const rangeValue = dualSlider.querySelector('.vbo-dual-slider-range-value');
let min = parseInt(minRange.value);
let max = parseInt(maxRange.value);
if (min > max) {
[minRange.value, maxRange.value] = [max, min];
}
let rangeMin = parseInt(minRange.min);
let rangeMax = parseInt(minRange.max);
let percent1 = ((minRange.value - rangeMin) / (rangeMax - rangeMin)) * 100;
let percent2 = ((maxRange.value - rangeMin) / (rangeMax - rangeMin)) * 100;
track.style.left = percent1 + '%';
track.style.width = (percent2 - percent1) + '%';
if (rangeValue) {
let rangeValueFormat = rangeValue.getAttribute('data-format') || '%d - %d';
let rangeValueString = rangeValueFormat.replace('%d', minRange.value).replace('%d', maxRange.value);
rangeValue.textContent = rangeValueString;
}
}
HTML;
if ($dual_slider_instances > 0) {
// restart the JS script content to avoid repeating the same function
$jscript = '';
}
// keep building script content
$jscript .= <<
var dualSlider = document.getElementById('vbo-dual-slider-{$suffix_elem}');
dualSlider
.querySelector('input[type="range"].vbo-dual-slider-range-min')
.addEventListener('input', () => {
vboUpdateSliderTrack('{$suffix_elem}');
});
dualSlider
.querySelector('input[type="range"].vbo-dual-slider-range-max')
.addEventListener('input', () => {
vboUpdateSliderTrack('{$suffix_elem}');
});
vboUpdateSliderTrack('{$suffix_elem}');
HTML;
// build final HTML string
$html = <<
{$range_html}
{$jscript}
HTML;
// return the HTML string to be displayed
return $html;
}
/**
* Renders a date-time locale input element to pick a date and time.
*
* @param array $options Associative list of element options.
*
* @return string The HTML string necessary to render the date-time picker.
*
* @since 1.18.0 (J) - 1.8.0 (WP)
*/
public function renderDateTimePicker(array $options = [])
{
if (!($options['id'] ?? null)) {
// the ID attribute is mandatory
$options['id'] = uniqid('dtp_');
}
// ensure attributes are set
if (!($options['attributes'] ?? [])) {
$options['attributes'] = [];
}
// attributes name, value, min and max can also be specified outside the "attributes" key
if (($options['name'] ?? null) && !($options['attributes']['name'] ?? null)) {
// resort the attribute inside the apposite key
$options['attributes']['name'] = $options['name'];
}
if (($options['value'] ?? null) && !($options['attributes']['value'] ?? null)) {
// resort the attribute inside the apposite key
$options['attributes']['value'] = $options['value'];
}
if (($options['min'] ?? null) && !($options['attributes']['min'] ?? null)) {
// resort the attribute inside the apposite key
$options['attributes']['min'] = $options['min'];
}
if (($options['max'] ?? null) && !($options['attributes']['max'] ?? null)) {
// resort the attribute inside the apposite key
$options['attributes']['max'] = $options['max'];
}
// check for "min" attribute, required to hide seconds from the time-picker
if (!($options['attributes']['min'] ?? null)) {
// default to 10 years in the past
$options['attributes']['min'] = JFactory::getDate('-10 years')->format('Y-m-d\TH:i');
}
// build attributes list
$attributes = array_merge([
'id' => $options['id'],
], $options['attributes']);
// build attributes string
$attr_str = implode(' ', array_map(function($name, $value) {
return $name . '="' . htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8') . '"';
}, array_keys($attributes), array_values($attributes)));
// build HTML string
$html = <<
HTML;
// return the HTML string to be displayed
return $html;
}
/**
* Renders a time input element to pick a time.
*
* @param array $options Associative list of element options.
*
* @return string The HTML string necessary to render the time picker.
*
* @since 1.18.4 (J) - 1.8.4 (WP)
*/
public function renderTimePicker(array $options = [])
{
if (!($options['id'] ?? null)) {
// the ID attribute is mandatory
$options['id'] = uniqid('tp_');
}
// ensure attributes are set
if (!($options['attributes'] ?? [])) {
$options['attributes'] = [];
}
// attributes name, value, min and max can also be specified outside the "attributes" key
if (($options['name'] ?? null) && !($options['attributes']['name'] ?? null)) {
// resort the attribute inside the apposite key
$options['attributes']['name'] = $options['name'];
}
if (($options['value'] ?? null) && !($options['attributes']['value'] ?? null)) {
// resort the attribute inside the apposite key
$options['attributes']['value'] = $options['value'];
}
if (($options['min'] ?? null) && !($options['attributes']['min'] ?? null)) {
// resort the attribute inside the apposite key
$options['attributes']['min'] = $options['min'];
}
if (($options['max'] ?? null) && !($options['attributes']['max'] ?? null)) {
// resort the attribute inside the apposite key
$options['attributes']['max'] = $options['max'];
}
if (($options['step'] ?? null) && !($options['attributes']['step'] ?? null)) {
// resort the attribute inside the apposite key
$options['attributes']['step'] = $options['step'];
}
// build attributes list
$attributes = array_merge([
'id' => $options['id'],
], $options['attributes']);
// build attributes string
$attr_str = implode(' ', array_map(function($name, $value) {
return $name . '="' . htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8') . '"';
}, array_keys($attributes), array_values($attributes)));
// build HTML string
$html = <<
HTML;
// return the HTML string to be displayed
return $html;
}
/**
* Renders a select2 component to display existing tags or to add new ones.
*
* @param array $options Associative list of dropdown options.
* @param array $elements Associative list of element records.
* @param array $groups Optional list of element groups to source.
*
* @return string The HTML string necessary to render the dropdown.
*
* @since 1.18.0 (J) - 1.8.0 (WP)
*/
public function renderTagsDropDown(array $options = [], array $elements = [], array $groups = [])
{
// load select2 assets
$this->loadSelect2();
if (!($options['id'] ?? null)) {
// the ID attribute is mandatory
$options['id'] = uniqid('tdd_');
}
// build attributes list
$attributes = array_merge([
'id' => $options['id'],
], ($options['attributes'] ?? []));
// build attributes string
$attr_str = implode(' ', array_map(function($name, $value) {
return $name . '="' . htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8') . '"';
}, array_keys($attributes), array_values($attributes)));
// build data sources
$data_sources = [];
foreach ($elements as $element) {
if (is_object($element)) {
$element = (array) $element;
}
if (empty($element['id'])) {
continue;
}
// build element data source
$data_source = [
'id' => $element['id'],
'text' => $element['name'] ?? $element['id'],
'color' => $element['color'] ?? null,
'hex' => $element['hex'] ?? null,
];
// check for option selected status
if (($options['selected_value'] ?? null) && $options['selected_value'] == $element['id']) {
$data_source['selected'] = true;
} elseif (is_array($options['selected_values'] ?? null) && in_array($element['id'], $options['selected_values'])) {
$data_source['selected'] = true;
}
// check for option disabled status
if (($options['disabled_value'] ?? null) && $options['disabled_value'] == $element['id']) {
$data_source['disabled'] = true;
} elseif (is_array($options['disabled_values'] ?? null) && in_array($element['id'], $options['disabled_values'])) {
$data_source['disabled'] = true;
}
// push element data source
$data_sources[] = $data_source;
}
// append groups to source as data elements
foreach ($groups as $group) {
if (is_object($group)) {
// always cast to array
$group = (array) $group;
}
if (!is_array($group) || empty($group['text']) || empty($group['elements'])) {
continue;
}
// filter out invalid group elements
$group['elements'] = array_filter((array) $group['elements'], function($group_element) {
return is_array($group_element) && isset($group_element['id']) && isset($group_element['text']);
});
// check for option selected status
if (($options['selected_value'] ?? null) || (is_array($options['selected_values'] ?? null) && $options['selected_values'])) {
foreach ($group['elements'] as $k => $element) {
if (($options['selected_value'] ?? null)) {
if ($options['selected_value'] == $element['id']) {
$group['elements'][$k]['selected'] = true;
}
} else {
if (in_array($element['id'], $options['selected_values'])) {
$group['elements'][$k]['selected'] = true;
}
}
}
}
// check for option disabled status
if (($options['disabled_value'] ?? null) || (is_array($options['disabled_values'] ?? null) && $options['disabled_values'])) {
foreach ($group['elements'] as $k => $element) {
if (($options['disabled_value'] ?? null)) {
if ($options['disabled_value'] == $element['id']) {
$group['elements'][$k]['disabled'] = true;
}
} else {
if (in_array($element['id'], $options['disabled_values'])) {
$group['elements'][$k]['disabled'] = true;
}
}
}
}
// push group element data source
$data_sources[] = [
'text' => $group['text'],
'children' => $group['elements'],
];
}
// data sources JSON encoded string
$data_sources_str = json_encode($data_sources);
// empty option tag
$empty_option = '';
if (!($options['attributes']['multiple'] ?? null)) {
$empty_option = '';
}
// clearing allowed
$clearable = (bool) ($options['allow_clear'] ?? 1);
$clearable_str = $clearable ? 'true' : 'false';
// tags allowed (for entering custom values)
$taggable = (bool) ($options['allow_tags'] ?? 1);
$taggable_str = $taggable ? 'true' : 'false';
// placeholder text
$placeholder = json_encode($options['placeholder'] ?? '');
// select2 width
$sel2width = $options['width'] ?? 'resolve';
// supported tag colors
$colors = json_encode((array) ($options['colors'] ?? []));
// build HTML string
$html = <<{$empty_option}
HTML;
// build script declaration
$js_decl = << parseInt(a.id) - parseInt(b.id));
// iterate all the existing tags
existingTags.forEach((tag) => {
let index = remainingColors.indexOf(tag.color);
if (index != -1) {
// remove the tag color from the remaining ones
remainingColors.splice(index, 1);
if (remainingColors.length == 0) {
// no more remaining colors, reset array
remainingColors = supportedColors.slice();
}
}
});
}
jQuery('select#{$options['id']}').select2({
width: '$sel2width',
allowClear: $clearable_str,
data: $data_sources_str,
placeholder: $placeholder,
tags: $taggable_str,
createTag: function (params) {
const term = (params.term || '').replace(/:/g, '').trim();
if (term === '') {
return null;
}
// temporarily assign the first available color
const color = remainingColors[0];
return {
id: term + ':' + color,
text: term,
color: color,
newTag: true,
};
},
templateResult: (element) => {
if (!element.id) {
return element.text;
}
let tag_class = '';
let tag_style = '';
if (element?.color) {
tag_class = element.color;
} else if (element?.hex) {
tag_style = 'background-color: ' + element.hex + ';';
} else {
tag_class = (element.id + '').toLowerCase().replace(/[^a-z0-9]/ig, '');
}
return jQuery('' + element.text + '');
},
templateSelection: (element) => {
if (!element.id) {
return element.text;
}
let tag_elem = jQuery('')
.addClass('vbo-sel2-selected-tag')
.text(element.text);
if (element.newTag) {
// we can understand here whether a new tag has been officially submitted
element.newTag = false;
// permanently detach the last color assigned
remainingColors.shift();
if (remainingColors.length == 0) {
// no more remaining colors, reset array
remainingColors = supportedColors.slice();
}
}
if (element?.color) {
tag_elem.addClass(element.color);
} else if (element?.hex) {
tag_elem.css('background-color', element.hex);
} else {
tag_elem.addClass((element.id + '').toLowerCase().replace(/[^a-z0-9]/ig, ''));
}
return tag_elem;
},
});
});
JAVASCRIPT;
if ((VBOPlatformDetection::isWordPress() && wp_doing_ajax()) || (!VBOPlatformDetection::isWordPress() && !strcasecmp((string) JFactory::getApplication()->input->server->get('HTTP_X_REQUESTED_WITH', ''), 'xmlhttprequest'))) {
// concatenate script to HTML string when doing an AJAX request
$html .= "\n" . '';
} else {
// add script declaration to document
JFactory::getDocument()->addScriptDeclaration($js_decl);
}
// return the HTML string to be displayed
return $html;
}
/**
* Renders a select2 component to display elements with thumbnails.
*
* @param array $options Associative list of dropdown options.
* @param array $elements Associative list of element records.
* @param array $groups Optional list of element groups to source.
*
* @return string The HTML string necessary to render the dropdown.
*
* @since 1.17.5 (J) - 1.7.5 (WP)
* @since 1.18.7 (J) - 1.8.7 (WP) added support to elements "listings" with sub-units.
*/
public function renderElementsDropDown(array $options = [], array $elements = [], array $groups = [])
{
if (!$elements && ($options['elements'] ?? '') == 'listings') {
// load listing records
$filter_listing_ids = (array) ($options['element_ids'] ?? []);
$elements = array_values(VikBooking::getAvailabilityInstance(true)->loadRooms($filter_listing_ids, 0, true));
if (($options['subunits'] ?? [])) {
// check if any of the listings involved is using a room-type (hotel) inventory
$hotelInventoryListingIds = array_column(array_filter($elements, function($roomRecord) {
return ($roomRecord['units'] ?? 0) > 1;
}), 'id');
// determine the value format for the sub-unit IDs
$subunitValueFormat = $options['subunits']['value_format'] ?? '%d-%d';
foreach ($hotelInventoryListingIds as $hotelInventoryListingId) {
foreach ($elements as $elIndex => $element) {
if ($element['id'] != $hotelInventoryListingId) {
// single-unit (or previous sub-unit) listing identified
continue;
}
// clone first element for sub-unit
$subunitEl = $element;
if (!($options['subunits']['entire_listing'] ?? true)) {
// the entire listing should no longer be included
unset($elements[$elIndex]);
$elIndex -= 1;
} else {
// modify the "entire listing" name ("All" by default)
$nameSuffix = (string) ($options['subunits']['listing_suffix'] ?? sprintf(' (%s)', strtolower(JText::translate('VBNEWCOUPONEIGHT'))));
$elements[$elIndex]['name'] .= $nameSuffix;
}
// handle sub-unit listing element to insert
$counter = 1;
for ($u = 1; $u <= $element['units']; $u++) {
// set sub-unit ID and name
$subunitEl['id'] = sprintf($subunitValueFormat, $hotelInventoryListingId, $u);
$subunitEl['name'] = $element['name'] . sprintf(' #%d', $u);
$subunitEl['_nested'] = true;
// insert the sub-unit element array next to the parent listing
array_splice($elements, $elIndex + $counter, 0, [$subunitEl]);
// increase counter
$counter++;
}
}
}
}
}
if (!$elements && !$groups) {
// abort
return '';
}
// load select2 assets
$this->loadSelect2();
if (!($options['id'] ?? null)) {
// the ID attribute is mandatory
$options['id'] = uniqid('ldd_');
}
if ($options['attributes']['multiple'] ?? null) {
// inject custom data attribute to prevent flashing via CSS during JS mutation
$options['attributes']['data-will-mutate'] = 1;
}
// build attributes list
$attributes = array_merge([
'id' => $options['id'],
], ($options['attributes'] ?? []));
// build attributes string
$attr_str = implode(' ', array_map(function($name, $value) {
return $name . '="' . htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8') . '"';
}, array_keys($attributes), array_values($attributes)));
// build data sources
$data_sources = [];
$base_img_path = implode(DIRECTORY_SEPARATOR, [VBO_SITE_PATH, 'resources', 'uploads']) . DIRECTORY_SEPARATOR;
$base_img_uri = VBO_SITE_URI . 'resources/uploads/';
foreach ($elements as $element) {
if (is_object($element)) {
$element = (array) $element;
}
if (empty($element['id'])) {
continue;
}
// element image
$element_img_uri = null;
if (($options['elements'] ?? '') == 'listings') {
// check for listing mini-thumbnail
if (!empty($element['img']) && is_file($base_img_path . 'mini_' . $element['img'])) {
$element_img_uri = $base_img_uri . 'mini_' . $element['img'];
}
}
if (!$element_img_uri && ($options['element_def_img_uri'] ?? '')) {
// use the provided default element image
$element_img_uri = $options['element_def_img_uri'];
} elseif (!$element_img_uri && ($element['img_uri'] ?? '')) {
// use the provided element image URI
$element_img_uri = $element['img_uri'];
}
// build element data source
$data_source = [
'id' => $element['id'],
'text' => $element['name'] ?? $element['id'],
'img' => $element_img_uri,
];
if (!$element_img_uri && !empty($element['html'])) {
// set element HTML (icon) content
$data_source['html'] = $element['html'];
}
if (!empty($element['_nested'])) {
// set element nested flag
$data_source['_nested'] = 1;
}
// check for option selected status
if (($options['selected_value'] ?? null) && $options['selected_value'] == $element['id']) {
$data_source['selected'] = true;
} elseif (is_array($options['selected_values'] ?? null) && in_array($element['id'], $options['selected_values'])) {
$data_source['selected'] = true;
}
// check for option disabled status
if (($options['disabled_value'] ?? null) && $options['disabled_value'] == $element['id']) {
$data_source['disabled'] = true;
} elseif (is_array($options['disabled_values'] ?? null) && in_array($element['id'], $options['disabled_values'])) {
$data_source['disabled'] = true;
}
// push element data source
$data_sources[] = $data_source;
}
// check for listing category groups
if (($options['elements'] ?? '') == 'listings' && ($options['load_categories'] ?? null)) {
// load listing categories
$categories = VikBooking::getAvailabilityInstance(true)->loadRoomCategories();
if (count($categories) > 1) {
// turn the category IDs into negative
$categories = array_map(function($cat) {
return [
'id' => ($cat['id'] - ($cat['id'] * 2)),
'text' => $cat['name'],
];
}, $categories);
// active or disabled listing categories
foreach ($categories as &$category) {
// check for option selected status
if (($options['selected_value'] ?? null) && $options['selected_value'] == $category['id']) {
$category['selected'] = true;
} elseif (is_array($options['selected_values'] ?? null) && in_array($category['id'], $options['selected_values'])) {
$category['selected'] = true;
}
// check for option disabled status
if (($options['disabled_value'] ?? null) && $options['disabled_value'] == $category['id']) {
$category['disabled'] = true;
} elseif (is_array($options['disabled_values'] ?? null) && in_array($category['id'], $options['disabled_values'])) {
$category['disabled'] = true;
}
}
unset($category);
// push listing category groups
$groups[] = [
'text' => $options['categories_lbl'] ?? 'Filter by category',
'elements' => $categories,
];
}
}
// append groups to source as data elements
foreach ($groups as $group) {
if (is_object($group)) {
// always cast to array
$group = (array) $group;
}
if (!is_array($group) || empty($group['text']) || empty($group['elements'])) {
continue;
}
// filter out invalid group elements
$group['elements'] = array_filter((array) $group['elements'], function($group_element) {
return is_array($group_element) && isset($group_element['id']) && isset($group_element['text']);
});
// check for option selected status
if (($options['selected_value'] ?? null) || (is_array($options['selected_values'] ?? null) && $options['selected_values'])) {
foreach ($group['elements'] as $k => $element) {
if (($options['selected_value'] ?? null)) {
if ($options['selected_value'] == $element['id']) {
$group['elements'][$k]['selected'] = true;
}
} else {
if (in_array($element['id'], $options['selected_values'])) {
$group['elements'][$k]['selected'] = true;
}
}
}
}
// check for option disabled status
if (($options['disabled_value'] ?? null) || (is_array($options['disabled_values'] ?? null) && $options['disabled_values'])) {
foreach ($group['elements'] as $k => $element) {
if (($options['disabled_value'] ?? null)) {
if ($options['disabled_value'] == $element['id']) {
$group['elements'][$k]['disabled'] = true;
}
} else {
if (in_array($element['id'], $options['disabled_values'])) {
$group['elements'][$k]['disabled'] = true;
}
}
}
}
// push group element data source
$data_sources[] = [
'text' => $group['text'],
'children' => $group['elements'],
];
}
// data sources JSON encoded string
$data_sources_str = json_encode($data_sources);
// empty option tag
$empty_option = '';
if (!($options['attributes']['multiple'] ?? null)) {
$empty_option = '';
}
// clearing allowed
$clearable = (bool) ($options['allow_clear'] ?? 1);
$clearable_str = $clearable ? 'true' : 'false';
// placeholder text
$placeholder = json_encode($options['placeholder'] ?? '');
// select2 width
$sel2width = $options['width'] ?? 'resolve';
// build HTML string
$html = <<{$empty_option}
HTML;
// template selection function
$selectionFn = '';
if ($options['style_selection'] ?? null) {
$defaultSelectionIcn = $options['default_selection_icon'] ?? '';
$selectionFn = << {
if (!element.id) {
return element.text;
}
let sel_elem = jQuery('')
.addClass('vbo-sel2-selected-tag')
.text(element.text);
if (element.img) {
let avatar_elem = jQuery('')
.addClass('vbo-sel2-selected-tag-avatar')
.attr('src', element.img);
sel_elem.prepend(avatar_elem);
} else if ('$defaultSelectionIcn') {
let icn_elem = jQuery('')
.addClass('$defaultSelectionIcn')
.addClass('vbo-sel2-selected-tag-avatar');
sel_elem.prepend(icn_elem);
}
return sel_elem;
}
JAVASCRIPT;
}
// choices name data attribute
$sel2ChoiceName = preg_replace('/[^a-z0-9\-\_]+/i', '', (string) ($options['attributes']['data-choice'] ?? $options['attributes']['name'] ?? ''));
// build script declaration
$js_decl = << {
let isNested = element?._nested;
if (element.img) {
return jQuery('' + element.text + '');
}
if (element.html) {
return jQuery('' + element.html + ' ' + element.text + '');
}
if (isNested) {
return jQuery('' + element.text + '');
}
return element.text;
},
$selectionFn
});
});
JAVASCRIPT;
if ((VBOPlatformDetection::isWordPress() && wp_doing_ajax()) || (!VBOPlatformDetection::isWordPress() && !strcasecmp((string) JFactory::getApplication()->input->server->get('HTTP_X_REQUESTED_WITH', ''), 'xmlhttprequest'))) {
// concatenate script to HTML string when doing an AJAX request
$html .= "\n" . '';
} else {
// add script declaration to document
JFactory::getDocument()->addScriptDeclaration($js_decl);
}
// return the HTML string to be displayed
return $html;
}
/**
* Renders a select2 component to display searchable elements with thumbnails.
*
* @param array $options Associative list of dropdown and search options.
*
* @return string The HTML string necessary to render the dropdown.
*
* @since 1.18.0 (J) - 1.8.0 (WP)
*/
public function renderSearchElementsDropDown(array $options = [])
{
if (!($options['endpoint'] ?? null) && ($options['elements'] ?? '') == 'bookings') {
// search bookings endpoint
$options['endpoint'] = VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=bookings.bookings_search');
} elseif (!($options['endpoint'] ?? null) && ($options['elements'] ?? '') == 'customers') {
// search customers endpoint
$options['endpoint'] = VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=bookings.customer_elements_search');
}
if (empty($options['endpoint'])) {
// abort
return '';
}
// set AJAX endpoint for searching
$endpoint = $options['endpoint'];
if ($options['load_assets'] ?? true) {
// load select2 assets
$this->loadSelect2();
}
if (!($options['id'] ?? null)) {
// the ID attribute is mandatory
$options['id'] = uniqid('ldd_');
}
// build attributes list
$attributes = array_merge([
'id' => $options['id'],
], ($options['attributes'] ?? []));
// build attributes string
$attr_str = implode(' ', array_map(function($name, $value) {
return $name . '="' . htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8') . '"';
}, array_keys($attributes), array_values($attributes)));
// build data sources
$data_sources = [];
// check for default option(s) selected status
$selected_options = [];
if (is_array($options['selected_values'] ?? null) && $options['selected_values']) {
$selected_options = $options['selected_values'];
} elseif (($options['selected_value'] ?? null)) {
$selected_options[] = $options['selected_value'];
}
foreach ($selected_options as $sel_option) {
if (is_array($sel_option) || is_object($sel_option)) {
// element object expected
$sel_option = (array) $sel_option;
if (isset($sel_option['id']) && isset($sel_option['text'])) {
// push valid data source
$sel_option['selected'] = true;
$data_sources[] = $sel_option;
}
} else {
// selected ID expected, push a data source object with limited information
$data_sources[] = [
'id' => $sel_option,
'text' => $sel_option,
'selected' => true,
];
}
}
// data sources JSON encoded string
$data_sources_str = json_encode($data_sources);
// empty option tag
$empty_option = '';
if (!($options['attributes']['multiple'] ?? null)) {
$empty_option = '';
}
// clearing allowed
$clearable = (bool) ($options['allow_clear'] ?? 1);
$clearable_str = $clearable ? 'true' : 'false';
// placeholder text
$placeholder = json_encode($options['placeholder'] ?? '');
// minimum input length for searching
$min_inp_len = 1;
// search language definitions
$lang_error_loading = json_encode($options['language']['error'] ?? JText::translate('VBO_ERR_LOAD_RESULTS'));
$lang_no_results = json_encode($options['language']['noresults'] ?? JText::translate('VBO_NO_RECORDS_FOUND'));
$lang_searching = json_encode($options['language']['searching'] ?? JText::translate('VBO_SEARCHING'));
// this language definition is NOT quoted, because it is not JSON-encoded
$lang_inptooshort = $min_inp_len > 1 && ($options['language']['inptooshort'] ?? '') ? $options['language']['inptooshort'] : '';
// selection/result with ID
$selection_with_id = ($options['selected_id'] ?? null) ? 'true' : 'false';
// selection extra class
$selection_class = $options['selection_class'] ?? '';
// selection click open widget
$selection_click_widget = $options['selection_click_widget'] ?? '';
// selection dispatch event
$selection_event = $options['selection_event'] ?? '';
// select2 width
$sel2width = $options['width'] ?? 'resolve';
// build HTML string
$html = <<{$empty_option}
HTML;
// template selection function
$selectionFn = '';
if ($options['style_selection'] ?? null) {
$defaultSelectionIcn = $options['default_selection_icon'] ?? '';
$selectionFn = << {
if (!element.id) {
return element.text;
}
let sel_elem = jQuery('')
.addClass(('vbo-sel2-selected-search-elem $selection_class').trim())
.text(element.text + ($selection_with_id ? ' #' + element.id : ''));
if (element.img) {
let avatar_elem = jQuery('')
.addClass('vbo-sel2-selected-search-elem-avatar')
.attr('src', element.img);
if (element.img_title) {
avatar_elem.attr('title', element.img_title);
}
sel_elem.prepend(avatar_elem);
} else if (element.icon_class) {
let icn_wrap = jQuery('')
.addClass('vbo-sel2-selected-search-elem-avatar');
let icn_elem = jQuery('')
.addClass(element.icon_class);
icn_wrap.append(icn_elem);
sel_elem.prepend(icn_wrap);
} else if ('$defaultSelectionIcn') {
let icn_elem = jQuery('')
.addClass('$defaultSelectionIcn')
.addClass('vbo-sel2-selected-search-elem-avatar');
sel_elem.prepend(icn_elem);
}
if ("$selection_click_widget" == 'booking_details') {
sel_elem.on('click', () => {
VBOCore.handleDisplayWidgetNotification({
widget_id: 'booking_details',
}, {
booking_id: element.id,
modal_options: {
suffix: 'vbo-booking-details-inner',
body_prepend: false,
enlargeable: false,
minimizeable: false,
},
});
});
}
return sel_elem;
}
JAVASCRIPT;
}
// build script declaration
$js_decl = << {
return $lang_error_loading;
},
noResults: () => {
return $lang_no_results;
},
searching: () => {
return $lang_searching;
},
inputTooShort: () => {
return "$lang_inptooshort";
},
},
ajax: {
delay: 350,
url: "$endpoint",
dataType: 'json',
},
templateResult: (element) => {
if (!element.id) {
return element.text;
}
let search_elem = jQuery('')
.addClass('vbo-sel2-search-elem');
let search_avatar = jQuery('')
.addClass('vbo-sel2-search-elem-avatar');
let elem_name = jQuery('')
.addClass('vbo-sel2-search-elem-name')
.text(element.text + ($selection_with_id ? ' #' + element.id : ''));
if (element.img) {
search_avatar.append('');
} else if (element.icon_class) {
search_avatar.append('');
}
search_avatar.append(elem_name);
search_elem.append(search_avatar);
return search_elem;
},
$selectionFn
});
if ("$selection_event") {
jQuery('select#{$options['id']}').on('select2:select', (e) => {
let element = e?.params?.data || e;
VBOCore.emitEvent("$selection_event", {
element: element,
});
});
}
});
JAVASCRIPT;
if ((VBOPlatformDetection::isWordPress() && wp_doing_ajax()) || (!VBOPlatformDetection::isWordPress() && !strcasecmp((string) JFactory::getApplication()->input->server->get('HTTP_X_REQUESTED_WITH', ''), 'xmlhttprequest'))) {
// concatenate script to HTML string when doing an AJAX request
$html .= "\n" . '';
} else {
// add script declaration to document
JFactory::getDocument()->addScriptDeclaration($js_decl);
}
// return the HTML string to be displayed
return $html;
}
/**
* Loads the assets for setting up the VBOCore JS in the site section.
*
* @param array $options Associative list of loading options.
*
* @return void
*
* @since 1.17.4 (J) - 1.7.4 (WP)
*/
public function loadCoreJS(array $options = [])
{
static $corejs_loaded = null;
if ($corejs_loaded) {
// loaded flag
return;
}
// cache loaded flag
$corejs_loaded = 1;
// add script
$this->addScript(VBO_ADMIN_URI . 'resources/vbocore.js', ['version' => VIKBOOKING_SOFTWARE_VERSION]);
if ($options) {
$core_options = json_encode((object) $options, JSON_PRETTY_PRINT);
// add script declaration to document
JFactory::getDocument()->addScriptDeclaration(
<<addScript(VBO_SITE_URI . 'resources/signature_pad.js', ['version' => VIKBOOKING_SOFTWARE_VERSION]);
}
/**
* Loads the assets solely needed to render the DRP calendar.
*
* @param array $options Associative list of loading options.
*
* @return void
*
* @since 1.17.3 (J) - 1.7.3 (WP)
*/
public function loadDatesRangePicker(array $options = [])
{
static $drp_loaded = null;
if ($drp_loaded) {
// loaded flag
return;
}
// cache loaded flag
$drp_loaded = 1;
// add DRP script
$this->addScript(VBO_SITE_URI . 'resources/datesrangepicker.js', ['version' => VIKBOOKING_SOFTWARE_VERSION]);
// load JS lang defs
JText::script('VBPICKUPROOM');
JText::script('VBRETURNROOM');
JText::script('VBO_MIN_STAY_NIGHTS');
JText::script('VBO_CLEAR_DATES');
JText::script('VBO_CLOSE');
}
/**
* Loads the necessary JS and CSS assets to render the jQuery UI Datepicker calendar.
* It is also possible to load the assets for the DatesRangePicker extension.
*
* @param array $options Associative list of loading options.
*
* @return void
*
* @since 1.1.0
* @since 1.15.0 (J) - 1.5.0 (WP) the lang definitions work for both front and back -ends.
* @since 1.17.3 (J) - 1.7.3 (WP) added support for the DatesRangePicker extension.
*/
public function loadDatePicker(array $options = [])
{
static $datepicker_loaded = null;
if ($datepicker_loaded) {
// loaded flag
return;
}
$document = JFactory::getDocument();
$document->addStyleSheet(VBO_SITE_URI . 'resources/jquery-ui.min.css');
JHtml::fetch('jquery.framework', true, true);
$this->addScript(VBO_SITE_URI . 'resources/jquery-ui.min.js');
if (!strcasecmp(($options['type'] ?? ''), 'dates_range')) {
// load DRP calendar assets
$this->loadDatesRangePicker($options);
}
$vbo_df = VikBooking::getDateFormat();
$juidf = $vbo_df == "%d/%m/%Y" ? 'dd/mm/yy' : ($vbo_df == "%m/%d/%Y" ? 'mm/dd/yy' : 'yy/mm/dd');
$is_rtl_lan = false;
$now_lang = JFactory::getLanguage();
if (method_exists($now_lang, 'isRtl')) {
$is_rtl_lan = $now_lang->isRtl();
}
// list of week day names for "short" and "min" versions
$day_names_short_list = [
JText::translate('SUN'),
JText::translate('MON'),
JText::translate('TUE'),
JText::translate('WED'),
JText::translate('THU'),
JText::translate('FRI'),
JText::translate('SAT'),
];
$day_names_min_list = [];
foreach ($day_names_short_list as $wdn) {
$day_names_min_list[] = $this->safeSubstr($wdn, 2);
}
// ensure this language does not produce conflicting week-days "min"
$day_names_min_list = array_unique($day_names_min_list);
if (count($day_names_min_list) != count($day_names_short_list)) {
// fallback onto the "short" week-days list to avoid conflicts with this language
$day_names_min_list = $day_names_short_list;
}
// build default regional values for datepicker
$vbo_dp_regional_vals = [
'closeText' => JText::translate('VBJQCALDONE'),
'prevText' => JText::translate('VBJQCALPREV'),
'nextText' => JText::translate('VBJQCALNEXT'),
'currentText' => JText::translate('VBJQCALTODAY'),
'monthNames' => [
JText::translate('JANUARY'),
JText::translate('FEBRUARY'),
JText::translate('MARCH'),
JText::translate('APRIL'),
JText::translate('MAY'),
JText::translate('JUNE'),
JText::translate('JULY'),
JText::translate('AUGUST'),
JText::translate('SEPTEMBER'),
JText::translate('OCTOBER'),
JText::translate('NOVEMBER'),
JText::translate('DECEMBER'),
],
'monthNamesShort' => [
JText::translate('JANUARY_SHORT'),
JText::translate('FEBRUARY_SHORT'),
JText::translate('MARCH_SHORT'),
JText::translate('APRIL_SHORT'),
JText::translate('MAY_SHORT'),
JText::translate('JUNE_SHORT'),
JText::translate('JULY_SHORT'),
JText::translate('AUGUST_SHORT'),
JText::translate('SEPTEMBER_SHORT'),
JText::translate('OCTOBER_SHORT'),
JText::translate('NOVEMBER_SHORT'),
JText::translate('DECEMBER_SHORT'),
],
'dayNames' => [
JText::translate('SUNDAY'),
JText::translate('MONDAY'),
JText::translate('TUESDAY'),
JText::translate('WEDNESDAY'),
JText::translate('THURSDAY'),
JText::translate('FRIDAY'),
JText::translate('SATURDAY'),
],
'dayNamesShort' => $day_names_short_list,
'dayNamesMin' => $day_names_min_list,
'weekHeader' => JText::translate('VBJQCALWKHEADER'),
'dateFormat' => $juidf,
'firstDay' => VikBooking::getFirstWeekDay(),
'isRTL' => $is_rtl_lan,
'showMonthAfterYear' => false,
'yearSuffix' => '',
];
$ldecl = '
jQuery(function($) {' . "\n" . '
$.datepicker.regional["vikbooking"] = ' . json_encode($vbo_dp_regional_vals) . ';
$.datepicker.setDefaults($.datepicker.regional["vikbooking"]);' . "\n" . '
});';
/**
* Trigger event to allow third party plugins to overwrite the JS declaration for the datepicker.
*
* @since 1.16.0 (J) - 1.6.0 (WP)
*/
VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeDeclareDatepickerRegionalVikBooking', [$is_rtl_lan, $now_lang->getTag(), &$ldecl]);
// add script declaration
$document->addScriptDeclaration($ldecl);
// cache loaded flag
$datepicker_loaded = 1;
}
/**
* Loads the CMS's native datepicker calendar.
*
* @since 1.10
*/
public function getCalendar($val, $name, $id = null, $df = null, array $attributes = array())
{
if ($df === null)
{
$df = VikBooking::getDateFormat();
}
return parent::calendar($val, $name, $id, $df, $attributes);
}
/**
* Returns a masked e-mail address. The e-mail are masked using
* a technique to encode the bytes in hexadecimal representation.
* The chunk of the masked e-mail will be also encoded to be HTML readable.
*
* @param string $email The e-mail to mask.
* @param boolean $reverse True to reverse the e-mail address.
* Only if the e-mail is not contained into an attribute.
*
* @return string The masked e-mail address.
*/
public function maskMail($email, $reverse = false)
{
if ($reverse)
{
// reverse the e-mail address
$email = strrev($email);
}
// converts the e-mail address from bin to hex
$email = bin2hex($email);
// append ; sequence after every chunk of the masked e-mail
$email = chunk_split($email, 2, ";");
// prepend sequence before the address and trim the ending sequence
$email = "" . substr($email, 0, -3);
return $email;
}
/**
* Returns a safemail tag to avoid the bots spoof a plain address.
*
* @param string $email The e-mail address to mask.
* @param boolean $mail_to True if the address should be wrapped
* within a "mailto" link.
*
* @return string The HTML tag containing the masked address.
*
* @uses maskMail()
*/
public function safeMailTag($email, $mail_to = false)
{
// include the CSS declaration to reverse the text contained in the tags
JFactory::getDocument()->addStyleDeclaration('safemail {direction: rtl;unicode-bidi: bidi-override;}');
// mask the reversed e-mail address
$masked = $this->maskMail($email, true);
// include the address into a custom tag
$tag = "$masked";
if ($mail_to)
{
// mask the address for mailto command (do not use reverse)
$mailto = $this->maskMail($email);
// wrap the safemail tag within a mailto link
$tag = "$tag";
}
return $tag;
}
/**
* Loads and echoes the script necessary to render the Fancybox
* plugin for jQuery to open images or iframes within a modal box.
* This resolves conflicts with some Bootstrap or Joomla (4) versions
* that do not support the old-native CSS class .modal with "behavior.modal".
* Mainly made to open pictures in a modal box, so the default "type" is set to "image".
* By passing a custom $opts string, the "type" property could be set to "iframe", but
* in this case it's better to use the other method of this class (Jmodal).
* The base jQuery library should be already loaded when using this method.
*
* @param string $selector The jQuery selector to trigger Fancybox.
* @param string $opts The options object for the Fancybox setup.
* @param boolean $reloadfunc If true, an additional function is included in the script
* to apply again Fancybox to newly added images to the DOM (via Ajax).
*
* @return void
*
* @uses addScript()
*/
public function prepareModalBox($selector = '.vbomodal', $opts = '', $reloadfunc = false)
{
if (empty($opts)) {
$opts = '{
"helpers": {
"overlay": {
"locked": false
}
},
"width": "70%",
"height": "75%",
"autoScale": true,
"transitionIn": "none",
"transitionOut": "none",
"padding": 0,
"type": "image"
}';
}
$document = JFactory::getDocument();
$document->addStyleSheet(VBO_SITE_URI.'resources/jquery.fancybox.css');
$this->addScript(VBO_SITE_URI.'resources/jquery.fancybox.js');
$reloadjs = '
function reloadFancybox() {
jQuery("'.$selector.'").fancybox('.$opts.');
}
';
$js = '
';
echo $js;
}
/**
* Method used to handle the reCAPTCHA events.
*
* @param string $event The reCAPTCHA event to trigger.
* Here's the list of the accepted events:
* - display Returns the HTML used to
* display the reCAPTCHA input.
* - check Validates the POST data to make sure
* the reCAPTCHA input was checked.
* @param array $options A configuration array.
*
* @return mixed The event response.
*
* @since 1.2.3
* @wponly the Joomla integration differs
*/
public function reCaptcha($event = 'display', array $options = array())
{
$response = null;
// an optional configuration array (just leave empty)
$options = array();
// trigger reCAPTCHA display event to fill $response var
do_action_ref_array('vik_recaptcha_' . $event, array(&$response, $options));
// display reCAPTCHA by echoing it (empty in case reCAPTCHA is not available)
return $response;
}
/**
* Checks if the com_user captcha is configured.
* In case the parameter is set to global, the default one
* will be retrieved.
*
* @param string $plugin The plugin name to check ('recaptcha' by default).
*
* @return boolean True if configured, otherwise false.
*
* @since 1.2.3
* @wponly the Joomla integration differs
*/
public function isCaptcha($plugin = 'recaptcha')
{
return apply_filters('vik_' . $plugin . '_on', false);
}
/**
* Checks if the global captcha is configured.
*
* @param string $plugin The plugin name to check ('recaptcha' by default).
*
* @return boolean True if configured, otherwise false.
*
* @since 1.2.3
*/
public function isGlobalCaptcha($plugin = 'recaptcha')
{
return $this->isCaptcha($plugin);
}
/**
* Method used to obtain a WordPress media form field.
*
* @return string The media in HTML.
*
* @since 1.3.0
*/
public function getMediaField($name, $value = null, array $data = array())
{
// check if WordPress is installed
if (VBOPlatformDetection::isWordPress())
{
add_action('admin_enqueue_scripts', function() {
wp_enqueue_media();
});
// import form field class
JLoader::import('adapter.form.field');
// create XML field manifest
$xml = "";
// instantiate field
$field = JFormField::getInstance(simplexml_load_string($xml));
// overwrite name and value within data
$data['name'] = $name;
$data['value'] = $value;
// inject display data within field instance
foreach ($data as $k => $v)
{
$field->bind($v, $k);
}
// render field
return $field->render();
}
// fallback to Joomla
// init media field
$field = new JFormFieldMedia(null, $value);
// setup an empty form as placeholder
$field->setForm(new JForm('vikbooking.media'));
// force field attributes
$data['name'] = $name;
$data['value'] = $value;
if (empty($data['previewWidth']))
{
// there is no preview width, set a defualt value
// to make the image visible within the popover
$data['previewWidth'] = 480;
}
// render the field
return $field->render('joomla.form.field.media', $data);
}
/**
* Displays a multi-state toggle switch element with unlimited buttons.
* Custom values, contents, labels, attributes and JS events can be attached
* to each button. VCM will use this same method.
*
* @param string $name the input name equal for all radio buttons.
* @param string $value the current input field value to be pre-selected.
* @param array $values list of radio buttons with each value.
* @param array $labels list of contents for each button trigger.
* @param array $attrs list of associative array attributes for each button.
* @param array $wrap list of associative array attributes for the wrapper.
*
* @return string the necessary HTML to render the multi-state toggle switch.
*
* @since 1.15.0 (J) - 1.5.0 (WP)
*/
public function multiStateToggleSwitchField($name, $value, $values = array(), $labels = array(), $attrs = array(), $wrap = array())
{
static $tooltip_js_declared = null;
// whether tooltip for titles is needed
$needs_tooltip = false;
// HTML container
$multi_state_switch = '';
if (!is_array($values) || !count($values)) {
// values must be set or we don't know what buttons to display
return $multi_state_switch;
}
// build default classes for the tri-state toggle switch (with 3 buttons)
$def_tristate_cls = array(
'vik-multiswitch-radiobtn-on',
'vik-multiswitch-radiobtn-def',
'vik-multiswitch-radiobtn-off',
);
// start wrapper
$multi_state_switch .= "\n" . '
' . "\n";
foreach ($values as $btn_k => $btn_val) {
// build default classes for button label
$btn_classes = array('vik-multiswitch-radiobtn');
if (isset($def_tristate_cls[$btn_k])) {
// push default class for a 3-state toggle switch
array_push($btn_classes, $def_tristate_cls[$btn_k]);
}
// check if additional custom classes have been defined for this button
if (isset($attrs[$btn_k]) && isset($attrs[$btn_k]['label_class']) && !empty($attrs[$btn_k]['label_class'])) {
if (is_array($attrs[$btn_k]['label_class'])) {
// list of additional classes for this button
$btn_classes = array_merge($btn_classes, $attrs[$btn_k]['label_class']);
} elseif (is_string($attrs[$btn_k]['label_class'])) {
// multiple classes should be space-separated
array_push($btn_classes, $attrs[$btn_k]['label_class']);
}
}
// check title as first thing, even though this is passed along with the labels
$label_title = '';
if (isset($labels[$btn_k]) && !is_scalar($labels[$btn_k]) && isset($labels[$btn_k]['title'])) {
$needs_tooltip = true;
$label_title = ' title="' . addslashes(htmlentities($labels[$btn_k]['title'])) . '"';
}
// start button label
$multi_state_switch .= "\t" . '' . "\n";
}
// end wrapper
$multi_state_switch .= '
' . "\n";
// check tooltip JS rendering
if (!$tooltip_js_declared && $needs_tooltip) {
// turn static flag on
$tooltip_js_declared = 1;
// add script declaration for JS rendering of tooltips
$doc = JFactory::getDocument();
$doc->addScriptDeclaration(
<<loadVisualEditorDefinitions();
// build the list of font families
$font_families = $this->getVisualEditorFonts();
$font_shortfam = $this->getVisualEditorFonts(true);
$js_font_names = json_encode($font_shortfam);
// build inline CSS styles
$css_font_decl = '';
foreach ($font_families as $k => $font_name) {
$font_val = $font_shortfam[$k];
$css_font_decl .= '.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="' . $font_val . '"]::before,';
$css_font_decl .= '.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="' . $font_val . '"]::before {' . "\n";
$css_font_decl .= 'content: "' . $font_name . '";' . "\n";
$css_font_decl .= 'font-family: "' . $font_name . '";' . "\n";
$css_font_decl .= '}' . "\n";
}
$css_font_decl .= '
.ql-picker.ql-specialtags .ql-picker-label {
padding-right: 18px;
}
.ql-picker.ql-specialtags .ql-picker-label:before {
content: "' . htmlspecialchars(JText::translate('VBO_CONDTEXT_TKN')) . '";
}
.ql-formats .ql-genai {
width: auto !important;
font-weight: bold;
}
';
/**
* Cache the pre-configuration CSS onto a file to allow AJAX requests
* to properly load the asset inline within the response. Before this
* change the styles used to be added as an inline style declaration.
*
* @since 1.16.7 (J) - 1.6.7 (WP)
* @since 1.17.3 (J) - 1.7.3 (WP) cached file is related to software version.
*/
$cached_preconfig_suffix = defined('VIKBOOKING_SOFTWARE_VERSION') ? '-' . VIKBOOKING_SOFTWARE_VERSION : '';
$cached_preconfig_css_path = implode(DIRECTORY_SEPARATOR, [VBO_ADMIN_PATH, 'resources', 'quill', 'vik-quill-preconfig-cache' . $cached_preconfig_suffix . '.css']);
$cached_preconfig_css_uri = VBO_ADMIN_URI . 'resources/quill/vik-quill-preconfig-cache' . $cached_preconfig_suffix . '.css';
$cached_preconfig_css_ok = is_file($cached_preconfig_css_path);
if (!$cached_preconfig_css_ok) {
// attempt to create the file
$cached_preconfig_css_ok = JFile::write($cached_preconfig_css_path, $css_font_decl);
}
if ($cached_preconfig_css_ok) {
// load cached script file
$doc->addStyleSheet($cached_preconfig_css_uri);
} else {
// revert to append CSS style declaration to document
$doc->addStyleDeclaration($css_font_decl);
}
// append theme CSS to document
$doc->addStyleSheet(VBO_ADMIN_URI . 'resources/quill/quill.snow.css');
// append JS assets to document
$this->addScript(VBO_ADMIN_URI . 'resources/quill/quill.js');
$this->addScript(VBO_ADMIN_URI . 'resources/quill/quill-image-resize.min.js');
$this->addScript(VBO_ADMIN_URI . 'resources/quill/vik-content-builder.js');
// icon for mail wrapper
$mail_wrapper_icn = '';
// icon for mail preview
$mail_preview_icn = '';
// icon for property logo (home icon)
$mail_homelogo_icn = '';
// text for generating content through AI
$mail_genai_icn = htmlspecialchars(JText::translate('VBO_AI_LABEL_DEF'), ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401);
// build script pre-configuration string
$quill_preconfig_script =
<<addScript($cached_preconfig_script_uri);
} else {
// revert to append JS script declaration to document
$doc->addScriptDeclaration($quill_preconfig_script);
}
/**
* Load Context Menu assets.
*
* @since 1.17.6 (J) - 1.7.6 (WP)
* @since 1.18.0 (J) - 1.8.0 (WP) loaded only if not during an AJAX request.
*/
if ((VBOPlatformDetection::isWordPress() && !wp_doing_ajax()) || (!VBOPlatformDetection::isWordPress() && strcasecmp((string) JFactory::getApplication()->input->server->get('HTTP_X_REQUESTED_WITH', ''), 'xmlhttprequest'))) {
$this->loadContextMenuAssets();
}
}
/**
* Renders a third-party visual editor (Quill).
*
* @param string $name the input name of the textarea field.
* @param string $value the current value of the textarea field/editor.
* @param array $attrs list of associative array attributes for the textarea.
* @param array $opts associative array of options for the editor.
* @param array $btns associative array of custom buttons for the editor (special tags).
*
* @return string the necessary HTML to render the visual editor.
*
* @since 1.15.0 (J) - 1.5.0 (WP)
* @since 1.17.3 (J) - 1.7.3 (WP) added support to Generative AI text functions.
*/
public function renderVisualEditor($name, $value, array $attrs = [], array $opts = [], array $btns = [])
{
if (empty($name)) {
return '';
}
// build the AJAX endpoints for uploading files, to preview the message and more
$upload_endpoint = VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=upload_media_file');
$ajax_preview_mess = VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=mail.preview_visual_editor');
$ajax_logo_url = VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=mail.get_default_logo');
// the HTML to build
$editor = "\n";
// static editor counter
static $editor_counter = 0;
// increase counter for this instance
$editor_counter++;
// build id attributes for text and visual editors
$editor_id = 'vik-contentbuilder-editor-' . $editor_counter;
if (!isset($attrs['id'])) {
$attrs['id'] = 'vik-contentbuilder-tarea-' . $editor_counter;
}
// labels for modes and editor
$text_html_lbl = JText::translate('VBO_MODE_TEXTHTML');
$visual_mode_lbl = JText::translate('VBO_MODE_VISUAL');
// allowed modes to display the visual editor
$allowed_modes = [
'text' => $text_html_lbl,
'visual' => $visual_mode_lbl,
'modal-visual' => $visual_mode_lbl,
];
// define the default buttons to display for mode switching
$modes = [
'text' => $text_html_lbl,
'modal-visual' => $visual_mode_lbl,
];
// overwrite modes to display
if (isset($opts['modes']) && is_array($opts['modes']) && $opts['modes']) {
// check if the given array is associative, hence inclusive of texts
if (array_keys($opts['modes']) != range(0, (count($opts['modes']) - 1))) {
// replace modes
$modes = $opts['modes'];
} else {
// only the allowed keys must have been passed
$new_modes = [];
foreach ($opts['modes'] as $mode_type) {
if (!isset($allowed_modes[$mode_type])) {
continue;
}
$new_modes[$mode_type] = $allowed_modes[$mode_type];
}
$modes = $new_modes ?: $modes;
}
}
// resolve conflicts for both visual modes
if (($modes['visual'] ?? '') && ($modes['modal-visual'] ?? '') && $modes['visual'] == $modes['modal-visual']) {
$modes['modal-visual'] .= ' ';
}
// ensure the text mode is set
if (!isset($modes['text'])) {
$modes['text'] = $allowed_modes['text'];
}
// overwrite default mode
if (isset($opts['def_mode']) && isset($modes[$opts['def_mode']]) && count($modes) > 1) {
$def_mode_val = $modes[$opts['def_mode']];
unset($modes[$opts['def_mode']]);
// sort modes accordingly
$modes = array_merge([$opts['def_mode'] => $def_mode_val], $modes);
// reset the array pointer
reset($modes);
}
// set default mode
$default_mode = key($modes);
// ensure text-area styles are merged or set when not the default mode
if ($default_mode != 'text') {
if (!($attrs['style'] ?? '')) {
// set style attribute to hide the text-area
$attrs['style'] = 'display: none;';
} else {
// append the style instruction to hide the text-area
$attrs['style'] .= ' display: none;';
}
}
// textarea attributes
$ta_attributes = [];
foreach ($attrs as $aname => $aval) {
if ($aname == 'name') {
// skip reserved attribute name
continue;
}
$ta_attributes[] = $aname . '="' . JHtml::fetch('esc_attr', $aval) . '"';
}
// visual editor JS options for list buttons
$js_editor_opts_list_btns = [
// ordered list (ol)
['list' => 'ordered'],
// un-ordered list (ul)
['list' => 'bullet'],
];
if ($opts['list_check'] ?? null) {
// push the un-ordered list (ul) with checked data-attribute, simulating a check-boxes list
$js_editor_opts_list_btns[] = ['list' => 'check'];
}
// build visual editor JS options
$js_editor_opts = [
'snippetsyntax' => true,
'modules' => [
'toolbar' => [
'container' => [
[
[
'font' => $this->getVisualEditorFonts(true)
],
],
[
[
'header' => [1, 2, 3, 4, 5, 6, false]
]
],
[
'bold',
'italic',
'underline',
'strike',
'blockquote',
],
[
['align' => []],
['indent' => '-1'],
['indent' => '+1'],
],
[
[
'color' => []
],
[
'background' => []
],
],
$js_editor_opts_list_btns,
[
'link',
'image',
'homelogo',
],
[
'mailwrapper',
'preview',
],
],
],
'imageResize' => [
'displaySize' => true,
],
],
'theme' => 'snow',
];
// check for Generative AI support through Vik Channel Manager and E4jConnect
if (class_exists('VikChannelManager') && defined('VikChannelManagerConfig::AI')) {
// add editor button for Gen-AI
$js_editor_opts['modules']['toolbar']['container'][] = ['genai'];
}
// build the list of special tags to be added to the editor
$special_tags_btns = [];
foreach ($btns as $tag_val) {
$special_tags_btns[] = $tag_val;
}
if ($special_tags_btns) {
// add custom buttons to the editor to manage special tags
$js_editor_opts['modules']['toolbar']['container'][] = [
['specialtags' => $special_tags_btns]
];
// append CSS inline styles
$editor .= '' . "\n";
}
if ($opts['unset_buttons'] ?? null) {
// unset toolbar container main-level buttons
$opts['unset_buttons'] = (array) $opts['unset_buttons'];
foreach ($js_editor_opts['modules']['toolbar']['container'] as $tc_index => $tc_buttons) {
if (!is_array($tc_buttons)) {
continue;
}
foreach ($opts['unset_buttons'] as $unset_btn) {
if (!is_string($unset_btn)) {
continue;
}
foreach ($tc_buttons as $tc_btn_index => $tc_btn) {
if (is_string($tc_btn) && $tc_btn === $unset_btn) {
// unset toolbar container button
unset($js_editor_opts['modules']['toolbar']['container'][$tc_index][$tc_btn_index]);
}
}
}
if (!$js_editor_opts['modules']['toolbar']['container'][$tc_index]) {
// unset the whole container
unset($js_editor_opts['modules']['toolbar']['container'][$tc_index]);
} else {
// restore numeric (non-associative) list
$js_editor_opts['modules']['toolbar']['container'][$tc_index] = array_values($js_editor_opts['modules']['toolbar']['container'][$tc_index]);
}
}
// restore numeric (non-associative) list
$js_editor_opts['modules']['toolbar']['container'] = array_values($js_editor_opts['modules']['toolbar']['container']);
}
// attept to pretty print a JSON encoded string for the editor options
$editor_opts_str = defined('JSON_PRETTY_PRINT') ? json_encode($js_editor_opts, JSON_PRETTY_PRINT) : json_encode($js_editor_opts);
// safe default value for editor (HTML tags should not be converted to entities)
$safe_value = preg_replace("/(<\/ ?textarea>)+/i", '', $value);
// the HTML to render the visual editor
$html_visual_editor = '';
// build the actual HTML content
$editor .= '
' . "\n";
if (count($modes) > 1) {
// display buttons to switch mode only if more than one mode available
$editor .= "\t" . '
' . "\n";
foreach ($modes as $key => $val) {
if ($key == 'text') {
// if text is not the default mode, the text-area will be always hid through the style attribute
$editor .= "\t\t" . '' . "\n";
} elseif ($key == 'visual') {
$editor .= "\t\t" . '