Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/vikappointments/admin/models/invoice.php
+++ b/vikappointments/admin/models/invoice.php
@@ -48,7 +48,7 @@
$q->from($dbo->qn('#__vikappointments_package_order'));
// get any approved codes
- $approved = JHtml::fetch('vaphtml.status.find', 'code', array('packages' => 1, 'approved' => 1));
+ $approved = JHtml::fetch('vaphtml.status.find', 'code', array('packages' => 1, 'approved' => 1));
}
else if ($data['group'] == 'employees')
{
@@ -70,8 +70,14 @@
{
$q->from($dbo->qn('#__vikappointments_reservation'));
+ // exclude reservations that belong to a parent order
+ $q->where(1)->andWhere([
+ $dbo->qn('id_parent') . ' = ' . $dbo->qn('id'),
+ $dbo->qn('id_parent') . ' = -1',
+ ], 'OR');
+
// get any approved codes
- $approved = JHtml::fetch('vaphtml.status.find', 'code', array('appointments' => 1, 'approved' => 1));
+ $approved = JHtml::fetch('vaphtml.status.find', 'code', array('appointments' => 1, 'approved' => 1));
}
if (!empty($data['cid']))
--- a/vikappointments/admin/models/reservation.php
+++ b/vikappointments/admin/models/reservation.php
@@ -1399,12 +1399,11 @@
}
}
- // fetch number of participants
- $people = !empty($data['people']) ? max(array(1, (int) $data['people'])) : 1;
// check if we are editing a reservation
$id = !empty($data['id']) ? (int) $data['id'] : 0;
+ $people = !empty($data['people']) ? max(array(1, (int) $data['people'])) : 1;
- // exmployee was specified, validate its availability
+ // employee was specified, validate its availability
if ($data['id_employee'] > 0)
{
// check if the employee is able to host the appointment
--- a/vikappointments/admin/models/subscription.php
+++ b/vikappointments/admin/models/subscription.php
@@ -21,6 +21,30 @@
class VikAppointmentsModelSubscription extends JModelVAP
{
/**
+ * Basic item loading implementation.
+ *
+ * @param mixed $pk An optional primary key value to load the row by, or an array of fields to match.
+ * If not set the instance property value is used.
+ * @param boolean $new True to return an empty object if missing.
+ *
+ * @return mixed The record object on success, null otherwise.
+ *
+ * @since 1.7.9
+ */
+ public function getItem($pk, $new = false)
+ {
+ // load item through parent
+ $item = parent::getItem($pk, $new);
+
+ if ($item)
+ {
+ $item->services = $item->services ? array_values(array_filter(explode(',', $item->services))) : [];
+ }
+
+ return $item;
+ }
+
+ /**
* Basic save implementation.
*
* @param mixed $data Either an array or an object of data to save.
@@ -123,6 +147,11 @@
switch ($subscription->type)
{
+ case 1:
+ // daily subscription
+ $add = 'days';
+ break;
+
case 2:
// weekly subscription
$add = 'weeks';
@@ -137,24 +166,42 @@
// yearly subscription
$add = 'years';
break;
-
+
default:
- // daily subscription
- $add = 'days';
+ $add = null;
}
- if ($subscription->amount == 1)
+ // create date instance
+ $date = JFactory::getDate($date);
+
+ if ($add)
{
- // get rid of plural in case amount is 1
- $add = rtrim($add, 's');
- }
+ if ($subscription->amount == 1)
+ {
+ // get rid of plural in case amount is 1
+ $add = rtrim($add, 's');
+ }
- // create date add string
- $add = '+' . $subscription->amount . ' ' . $add;
+ // create date add string
+ $add = '+' . $subscription->amount . ' ' . $add;
- // create date instance and extend it
- $date = JFactory::getDate($date);
- $date->modify($add);
+ // extend the date by the provided time
+ $date->modify($add);
+ }
+
+ /**
+ * Fires while extending the subscription of a customer or an employee.
+ * It is possible to rely on this hook to alter the new expiration date
+ * of the subscription.
+ *
+ * @param JDate $date The expiration date.
+ * @param object $subscription The details of the purchased subscription.
+ *
+ * @return void
+ *
+ * @since 1.7.9
+ */
+ VAPFactory::getEventDispatcher()->trigger('onBeforeExtendSubscription', [$date, $subscription]);
return $date->toSql();
}
@@ -234,4 +281,39 @@
$dbo->setQuery($q);
$dbo->execute();
}
+
+ /**
+ * Returns a list holding all the supported subscription types.
+ *
+ * @return array
+ *
+ * @since 1.7.9
+ */
+ public function getSupportedSubscriptionTypes()
+ {
+ $types = [];
+
+ for ($i = 1; $i <= 5; $i++)
+ {
+ $types[$i] = JText::translate('VAPSUBSCRTYPE' . $i);
+ }
+
+ /**
+ * Fires while loading the supported subscription types.
+ * It is possible to rely on this hook to support custom durations.
+ *
+ * @return array An associative array with id-label pairs.
+ *
+ * @since 1.7.9
+ */
+ $results = VAPFactory::getEventDispatcher()->trigger('onLoadSupportedSubscriptionTypes');
+
+ foreach ($results as $custom)
+ {
+ // use native array concat instead of array_merge to preserve associative keys
+ $types = $types + (array) $custom;
+ }
+
+ return $types;
+ }
}
--- a/vikappointments/admin/tables/subscription.php
+++ b/vikappointments/admin/tables/subscription.php
@@ -73,10 +73,6 @@
if (isset($src['type']))
{
- // type must be in the range [1,5]
- $src['type'] = max(array(1, (int) $src['type']));
- $src['type'] = min(array(5, (int) $src['type']));
-
if ($src['type'] == 5)
{
// lifetime selected, force amount to 1
--- a/vikappointments/admin/views/backups/view.html.php
+++ b/vikappointments/admin/views/backups/view.html.php
@@ -136,7 +136,10 @@
$lim0 = max(array(0, $lim0 - $lim));
}
- $rows = array_slice($rows, $lim0, $lim);
+ if ($lim)
+ {
+ $rows = array_slice($rows, $lim0, $lim);
+ }
jimport('joomla.html.pagination');
$pageNav = new JPagination($tot_count, $lim0, $lim);
--- a/vikappointments/admin/views/emprates/tmpl/default_services_modal.php
+++ b/vikappointments/admin/views/emprates/tmpl/default_services_modal.php
@@ -107,6 +107,18 @@
</div>
<?php echo $vik->closeControl(); ?>
+ <!-- MAX CAPACITY - Number -->
+
+ <?php
+ $help = $vik->createPopover(array(
+ 'title' => JText::translate('VAPMANAGESERVICE21'),
+ 'content' => JText::translate('VAPMANAGESERVICE21_DESC'),
+ ));
+
+ echo $vik->openControl(JText::translate('VAPMANAGESERVICE21') . $help, 'service-global-child', array('style' => 'display: none;')); ?>
+ <input type="number" id="service_max_capacity" size="10" min="1" max="999999" />
+ <?php echo $vik->closeControl(); ?>
+
<!-- DESCRIPTION -->
<?php
@@ -284,6 +296,13 @@
jQuery('#service_sleep').val(data.sleep);
+ // set max capacity
+ if (data.max_capacity === undefined) {
+ data.max_capacity = 1;
+ }
+
+ jQuery('#service_max_capacity').val(data.max_capacity);
+
// set description
Joomla.editors.instances.service_description.setValue(data.description ? data.description : '');
@@ -345,6 +364,9 @@
// get sleep
data.sleep = parseInt(jQuery('#service_sleep').val());
+ // get max capacity
+ data.max_capacity = parseInt(jQuery('#service_max_capacity').val());
+
// get description
data.description = Joomla.editors.instances.service_description.getValue();
@@ -368,6 +390,7 @@
jQuery('#service_rate').val(service.price);
jQuery('#service_duration').val(service.duration);
jQuery('#service_sleep').val(service.sleep);
+ jQuery('#service_max_capacity').val(service.max_capacity);
}
function serviceGlobalValueChanged(is) {
--- a/vikappointments/admin/views/emprates/view.html.php
+++ b/vikappointments/admin/views/emprates/view.html.php
@@ -53,7 +53,7 @@
$q = $dbo->getQuery(true)
->select($dbo->qn(array(
- 's.id', 's.name', 's.price', 's.duration', 's.sleep',
+ 's.id', 's.name', 's.price', 's.duration', 's.sleep', 's.max_capacity',
)))
->from($dbo->qn('#__vikappointments_service', 's'))
->order($dbo->qn('s.ordering') . ' ASC');
--- a/vikappointments/admin/views/manageservice/tmpl/default_assoc_employees_modal.php
+++ b/vikappointments/admin/views/manageservice/tmpl/default_assoc_employees_modal.php
@@ -105,7 +105,19 @@
<span class="btn"><?php echo JText::translate('VAPSHORTCUTMINUTE'); ?></span>
</div>
- <?php echo $vik->closeControl(); ?>
+ <?php echo $vik->closeControl(); ?>
+
+ <!-- MAX CAPACITY - Number -->
+
+ <?php
+ $help = $vik->createPopover(array(
+ 'title' => JText::translate('VAPMANAGESERVICE21'),
+ 'content' => JText::translate('VAPMANAGESERVICE21_DESC'),
+ ));
+
+ echo $vik->openControl(JText::translate('VAPMANAGESERVICE21') . $help, 'employee-global-child', array('style' => 'display: none;')); ?>
+ <input type="number" id="employee_max_capacity" size="10" min="1" max="999999" />
+ <?php echo $vik->closeControl(); ?>
<!-- DESCRIPTION -->
@@ -278,6 +290,14 @@
jQuery('#employee_sleep').val(data.sleep);
+ // set max capacity
+ if (data.max_capacity === undefined) {
+ // use default service max capacity
+ data.max_capacity = parseInt(jQuery('input[name="max_capacity"]').val());
+ }
+
+ jQuery('#employee_max_capacity').val(data.max_capacity);
+
// set description
Joomla.editors.instances.employee_description.setValue(data.description ? data.description : '');
@@ -347,6 +367,9 @@
// get sleep
data.sleep = parseInt(jQuery('#employee_sleep').val());
+ // get max capacity
+ data.max_capacity = parseInt(jQuery('#employee_max_capacity').val());
+
// get description
data.description = Joomla.editors.instances.employee_description.getValue();
}
--- a/vikappointments/admin/views/media/view.html.php
+++ b/vikappointments/admin/views/media/view.html.php
@@ -89,7 +89,7 @@
if ($tot_count)
{
- if ($lim0 % $lim)
+ if ($lim == 0 || $lim0 % $lim)
{
/**
* The current offset is not divisible by the selected limit. For this reason,
@@ -112,7 +112,10 @@
$lim0 = floor($tot_count / $lim) * $lim;
}
- $all_img = array_slice($all_img, $lim0, $lim);
+ if ($lim)
+ {
+ $all_img = array_slice($all_img, $lim0, $lim);
+ }
jimport('joomla.html.pagination');
$pageNav = new JPagination($tot_count, $lim0, $lim);
--- a/vikappointments/defines.php
+++ b/vikappointments/defines.php
@@ -15,7 +15,7 @@
defined('_JEXEC') or define('_JEXEC', 1);
// Software version
-define('VIKAPPOINTMENTS_SOFTWARE_VERSION', '1.2.19');
+define('VIKAPPOINTMENTS_SOFTWARE_VERSION', '1.2.20');
// Software debugging flag
define('VIKAPPOINTMENTS_DEBUG', false);
--- a/vikappointments/libraries/adapter/database/database.php
+++ b/vikappointments/libraries/adapter/database/database.php
@@ -254,6 +254,13 @@
{
// result should contain an array
$this->result = $this->db->get_results($sql);
+
+ /**
+ * Flush result after executing the query to free disk space.
+ *
+ * @since 10.1.73
+ */
+ $this->db->flush();
}
// otherwise we can launch a generic query
else
@@ -324,7 +331,15 @@
if (is_array($this->result))
{
- return $this->result;
+ /**
+ * Copy result on a local variable and flush the cached value.
+ *
+ * @since 10.1.73
+ */
+ $result = $this->result;
+ $this->result = null;
+
+ return $result;
}
return array();
--- a/vikappointments/site/helpers/lib.vikappointments.php
+++ b/vikappointments/site/helpers/lib.vikappointments.php
@@ -5866,15 +5866,13 @@
$lim0 = $start;
$session = JFactory::getSession();
- $ordering = $session->get('reviewsOrdering', '', 'vikappointments');
+
+ $app = JFactory::getApplication();
- if (empty($ordering))
- {
- $ordering = array(
- 'by' => 'timestamp',
- 'mode' => 'DESC',
- );
- }
+ $ordering = [
+ 'by' => $app->getUserStateFromRequest('vikappointments.reviews.order.column', 'revordby', '', 'string') ?: 'timestamp',
+ 'mode' => $app->getUserStateFromRequest('vikappointments.reviews.order.direction', 'revordmode', '', 'string') ?: 'desc',
+ ];
$q = $dbo->getQuery(true);
@@ -5904,7 +5902,14 @@
$q->where($dbo->qn('r.langtag') . ' = ' . $dbo->q(JFactory::getLanguage()->getTag()));
}
- $q->order($dbo->qn('r.' . $ordering['by']) . ' ' . $ordering['mode']);
+ /**
+ * Sanitize ordering mode.
+ *
+ * @since 1.7.9
+ */
+ $direction = strcasecmp((string) $ordering['mode'], 'asc') ? 'DESC' : 'ASC';
+
+ $q->order($dbo->qn('r.' . $ordering['by']) . ' ' . $direction);
$dbo->setQuery($q, $lim0, $lim);
$result->rows = $dbo->loadObjectList();
@@ -5954,22 +5959,25 @@
'rating' => 'DESC',
);
- $session = JFactory::getSession();
- $ordering = $session->get('reviewsOrdering', '', 'vikappointments');
+ $app = JFactory::getApplication();
- if (empty($ordering))
- {
- $ordering = array(
- 'by' => 'timestamp',
- 'mode' => 'DESC',
- );
- }
+ $ordering = [
+ 'by' => $app->getUserState('vikappointments.reviews.order.column', '') ?: 'timestamp',
+ 'mode' => $app->getUserState('vikappointments.reviews.order.direction', '') ?: 'DESC',
+ ];
if (empty($by))
{
$by = $ordering['by'];
$mode = $ordering['mode'];
}
+
+ /**
+ * Sanitize ordering mode.
+ *
+ * @since 1.7.9
+ */
+ $mode = strcasecmp((string) $mode, 'asc') ? 'DESC' : 'ASC';
if (!array_key_exists($by, $columns))
{
@@ -6007,8 +6015,9 @@
$ordering['by'] = $by;
$ordering['mode'] = $mode;
-
- $session->set('reviewsOrdering', $ordering, 'vikappointments');
+
+ $app->getUserState('vikappointments.reviews.order.column', $ordering['by']);
+ $app->getUserState('vikappointments.reviews.order.direction', $ordering['mode']);
return $links;
}
--- a/vikappointments/site/helpers/libraries/availability/implementor.php
+++ b/vikappointments/site/helpers/libraries/availability/implementor.php
@@ -855,7 +855,28 @@
// make sure the current people count plus the specified number of
// participants doesn't exceed the maximum capacity of the service
- return ($count + $people) <= $service->max_capacity;
+ // return ($count + $people) <= $service->max_capacity;
+
+ /**
+ * Ignore the people validation to bypass the limitation related to overlapping appointments
+ * with maximum capacity higher than one and time slots length different than the duration.
+ *
+ * Practical example (service max capacity = 2, duration = 60 min):
+ * 11:00 -> 1 seat available
+ * 11:30 -> 1 seat available
+ * 12:00 -> 1 seat available
+ *
+ * Considering that the service lasts 1 hour, checking the availability for the 11:30 - 12:30
+ * time slot results in a failure, as the total count is equal to 2 (11:00 + 12:00). Since the
+ * total count is equal to the maximum capacity, the appointment won't be accepted.
+ *
+ * However, the availability is already evaluated by looking at the timeline status. So, in case
+ * the timeline reports the time as available, we can bypass an extra validation check applied to
+ * the number of participants.
+ *
+ * @since 1.7.9
+ */
+ return true;
}
/**
--- a/vikappointments/site/helpers/libraries/html/countries.php
+++ b/vikappointments/site/helpers/libraries/html/countries.php
@@ -122,13 +122,12 @@
->where($dbo->qn('country_2_code') . ' = ' . $dbo->q($country_2_code));
$dbo->setQuery($q, 0, 1);
-
- $country = static::db2country($dbo->loadObject());
+ $country = $dbo->loadObject();
if ($country)
{
// cache country
- static::$countries[$country_2_code] = $country;
+ static::$countries[$country_2_code] = static::db2country($country);
}
else
{
--- a/vikappointments/site/models/emplogin.php
+++ b/vikappointments/site/models/emplogin.php
@@ -93,6 +93,8 @@
throw new Exception(JText::translate('JERROR_ALERTNOAUTHOR'), 403);
}
+ $dispatcher = VAPFactory::getEventDispatcher();
+
$dbo = JFactory::getDbo();
$options['start'] = !isset($options['start']) ? 0 : $options['start'];
@@ -127,6 +129,21 @@
// filter by reserved status
$q->where($dbo->qn('r.status') . ' IN (' . implode(',', array_map(array($dbo, 'q'), $reserved)) . ')');
}
+
+ /**
+ * Trigger hook to manipulate the query at runtime. Third party plugins
+ * can extend the query by applying further conditions or selecting
+ * additional data.
+ *
+ * @param mixed &$query Either a query builder or a query string.
+ * @param array &$options An array of options.
+ * @param VAPEmployeeAuth $auth The authenticated employee instance.
+ *
+ * @return void
+ *
+ * @since 1.7.9
+ */
+ $dispatcher->trigger('onBuildEmploginAppointmentsQuery', [&$q, &$options, $auth]);
$dbo->setQuery($q, $options['start'], $options['limit']);
$rows = $dbo->loadAssocList();
@@ -137,6 +154,20 @@
$this->getPagination($options);
}
+ /**
+ * Trigger hook to manipulate the query response at runtime. Third party
+ * plugins can alter the resulting list of orders.
+ *
+ * @param array &$rows An array of fetched orders.
+ * @param VAPEmployeeAuth $auth The authenticated employee instance.
+ * @param JModel $model The current model.
+ *
+ * @return void
+ *
+ * @since 1.7.9
+ */
+ $dispatcher->trigger('onBuildEmploginAppointmentsData', [&$rows, $auth, $this]);
+
return $rows;
}
--- a/vikappointments/vikappointments.php
+++ b/vikappointments/vikappointments.php
@@ -3,7 +3,7 @@
Plugin Name: VikAppointments
Plugin URI: https://vikwp.com/plugin/vikappointments
Description: A professional tool for managing any kind of appointments.
-Version: 1.2.19
+Version: 1.2.20
Author: E4J s.r.l.
Author URI: https://vikwp.com
License: GPL2