File "ical.php"
Full Path: /home/romayxjt/public_html/wp-content/plugins/vikbooking/admin/helpers/src/task/operator/ical.php
File size: 19.44 KB
MIME-type: text/x-php
Charset: utf-8
<?php
/**
* @package VikBooking
* @subpackage core
* @author E4J s.r.l.
* @copyright Copyright (C) 2025 E4J s.r.l. All Rights Reserved.
* @license http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL
* @link https://vikwp.com
*/
// No direct access
defined('ABSPATH') or die('No script kiddies please!');
/**
* Task operator iCal implementation.
*
* @since 1.18.0 (J) - 1.8.0 (WP)
*/
final class VBOTaskOperatorIcal
{
/** @var array */
protected $operator = [];
/** @var object|null */
protected $permissions;
/** @var string */
protected $tool = '';
/** @var string */
protected $toolUri = '';
/** @var string|null */
protected $calendarSubscriber = null;
/** @var array */
private $events = [];
/** @var VBOPlatformDispatcherInterface */
private $dispatcher;
/**
* Proxy for immediately accessing the object.
*
* @return self
*/
public static function getInstance()
{
return new static;
}
/**
* Class constructor.
*/
public function __construct()
{
$this->dispatcher = VBOFactory::getPlatform()->getDispatcher();
}
/**
* Magic method used to access protected properties.
* Private properties are still not accessible.
*
* @inheritDoc
*
* @since 1.18.1 (J) - 1.8.1 (WP)
*/
public function __get(string $name)
{
try {
// obtain class property details
$prop = (new ReflectionClass($this))->getProperty($name);
if ($prop->isPrivate()) {
// cannot access a private property
throw new DomainException;
}
} catch (Exception $error) {
// the property doesn't exist or is private
return null;
}
// grant access to protected properties instead
return $prop->getValue($this);
}
/**
* Sets the list of event objects.
*
* @param object[] $events List of event objects.
*
* @return self
*/
public function setEvents(array $events)
{
$this->events = $events;
return $this;
}
/**
* Sets the current operator record.
*
* @param array|object $operator The operator information record.
*
* @return self
*/
public function setOperator($operator)
{
if (is_array($operator) || is_object($operator)) {
$this->operator = (array) $operator;
}
return $this;
}
/**
* Sets the current operator permissions object.
*
* @param object $permissions The operator permissions object.
*
* @return self
*/
public function setPermissions($permissions)
{
if (is_object($permissions)) {
$this->permissions = $permissions;
}
return $this;
}
/**
* Sets the name of the current operator tool.
*
* @param string $tool The operator tool identifier.
*
* @return self
*/
public function setTool(string $tool)
{
$this->tool = $tool;
return $this;
}
/**
* Sets the URI for the current operator tool.
*
* @param string $uri The operator tool URI.
*
* @return self
*/
public function setToolUri(string $uri)
{
$this->toolUri = VBOFactory::getPlatform()->getUri()->route($uri);
return $this;
}
/**
* Internally sets the calendar that will subscribe to the ICS.
* Useful to generate different contents depending on the receiver.
*
* @param ?string $calendarId Such as google, apple and so on.
*
* @return self
*
* @since 1.18.1 (J) - 1.8.1 (WP)
*/
public function setCalendarSubscriber(?string $calendarId)
{
$this->calendarSubscriber = $calendarId ? strtolower($calendarId) : null;
return $this;
}
/**
* Builds the event UID.
*
* @param VBOTaskTaskregistry $task The task registry.
*
* @return string
*/
public function getEventUid(VBOTaskTaskregistry $task)
{
return md5($task->getID() ?: rand());
}
/**
* Builds up the iCal calendar file content.
*
* @return string The full iCal calendar file content.
*/
public function toString()
{
/**
* Starts the calendar declaration and build header and events.
*
* @link https://icalendar.org/iCalendar-RFC-5545/3-4-icalendar-object.html
*/
return implode('', [
$this->addLine('BEGIN', 'VCALENDAR'),
$this->buildCalendarHead(),
$this->buildCalendarContent(),
$this->addLine('END', 'VCALENDAR')
]);
}
/**
* Downloads the iCal calendar file content.
*
* @param mixed $app The CMS application.
* @param ?string $filename The file name to use for the download.
*
* @return void
*
* @since 1.18.1 (J) - 1.8.1 (WP)
*/
public function download($app = null, ?string $filename = null)
{
// use default application if not provided
$app = $app ?: JFactory::getApplication();
if (!$filename) {
// use default name format: {ID}-{FIRST_NAME}-{TODAY}
$filename = sprintf(
'%d-%s-%s',
$this->operator['id'],
(string) $this->operator['first_name'],
date('Y-m-d')
);
}
// remove .ics extenion from file name
$filename = preg_replace("/\.ics$/i", '', $filename);
// generate ICS output
$ics = $this->toString();
// declare headers
$app->setHeader('Content-Type', 'text/calendar; charset=utf-8');
$app->setHeader('Content-Disposition', 'attachment; filename=' . $filename . '.ics');
$app->setHeader('Content-Length', strlen($ics));
$app->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
$app->setHeader('Pragma', 'no-cache');
$app->setHeader('Expires', '0');
$app->sendHeaders();
// start the ICS download
echo $ics;
}
/**
* Builds and returns the iCal calendar head string section.
*
* @return string
*/
private function buildCalendarHead()
{
$ics = '';
// set up default head information
$head = [
'version' => '2.0',
'prodid' => '-//e4j//VikBooking ' . VIKBOOKING_SOFTWARE_VERSION . '//EN',
'calscale' => 'GREGORIAN',
'calname' => JText::translate('VBO_TASK_MANAGER') . ' - ' . trim($this->operator['first_name'] . ' ' . $this->operator['last_name']),
];
/**
* Trigger event to allow the plugins to include custom options within the
* head of the ICS file.
*
* @param array &$head The default head data.
* @param mixed $handler The current handler instance.
*
* @return string Some extra rules to include at the end of the head.
*
* @since 1.18.1 (J) - 1.8.1 (WP)
*/
$extra = $this->dispatcher->filter('onBuildHeadExportICS', [&$head, $this]);
/**
* This property specifies the identifier corresponding to the highest version number
* or the minimum and maximum range of the iCalendar specification that is required
* in order to interpret the iCalendar object.
*
* @link https://icalendar.org/iCalendar-RFC-5545/3-7-4-version.html
*/
$ics .= $this->addLine('VERSION', $head['version']);
/**
* This property specifies the identifier for the product that created the iCalendar object.
*
* @link https://icalendar.org/iCalendar-RFC-5545/3-7-3-product-identifier.html
*/
$ics .= $this->addLine('PRODID', $head['prodid']);
/**
* This property defines the calendar scale used for the calendar information
* specified in the iCalendar object.
*
* @link https://icalendar.org/iCalendar-RFC-5545/3-7-1-calendar-scale.html
*/
$ics .= $this->addLine('CALSCALE', $head['calscale']);
/**
* This non standard property defines the default name that will be used
* when creating a new subscription.
*
* @since 1.18.1 (J) - 1.8.1 (WP)
*/
$ics .= $this->addLine('X-WR-CALNAME', $head['calname']);
// append also the values that have been returned by the plugins
$ics .= implode('', array_filter($extra));
return $ics;
}
/**
* Builds and returns the iCal calendar content string.
*
* @return string
*/
private function buildCalendarContent()
{
$content = '';
foreach ($this->events as $event) {
$content .= $this->buildCalendarEvent((array) $event);
}
return $content;
}
/**
* Builds and returns the iCal content string for the given event data.
*
* @param array $event The event (task) information record.
*
* @return string
*/
private function buildCalendarEvent(array $event)
{
// wrap the event (task) record into a registry
$task = VBOTaskTaskregistry::getInstance($event);
// check if the task is currently un-assigned
$assigneeIds = $task->getAssigneeIds();
$unassigned_label = !$assigneeIds ? sprintf(' (%s)', JText::translate('VBO_UNASSIGNED')) : '';
// fetch room name and geo details
$roomInfo = VikBooking::getRoomInfo($task->getListingId(), $columns = ['name', 'params']);
if (!empty($roomInfo['params'])) {
$roomInfo['params'] = json_decode($roomInfo['params']);
}
$uri = null;
if ($this->toolUri) {
// use task direct link
$uri = new JUri($this->toolUri);
$uri->setVar('filters[calendar_type]', 'taskdetails');
$uri->setVar('filters[task_id]', $task->getID());
}
// add a new line at the end of each paragraph
$notes = preg_replace("/<\/p></", "</p>\n<", $task->getNotes());
// event description is built through various task values separated by a safe new-line
$description = implode('\n', array_filter([
// task status
$task->getStatusName() . $unassigned_label,
// listing name
$roomInfo['name'] ?? '',
// listing notes (plain text)
preg_replace("/\R/", "\\n", strip_tags($notes)),
]));
if ($this->calendarSubscriber === 'google') {
// Google Calendar doesn't support the URI rule, therefore the task URL
// should be included directly within the description.
$description .= '\n\n' . $uri;
}
$data = [
'uid' => $this->getEventUid($task),
'created' => $task->getCreationDate(true, 'Ymd\THis\Z'),
'modified' => $task->getModificationDate(true, 'Ymd\THis\Z'),
'start' => $task->getDueDate(true, 'Ymd\THis\Z'),
'end' => $task->getFinishDate(true, 'Ymd\THis\Z') ?: $task->getDurationDate(true, 'Ymd\THis\Z'),
'summary' => $task->getTitle(),
'description' => $description,
'location' => $roomInfo['params']->geo->address ?? null,
'url' => (string) $uri,
'status' => 'CONFIRMED',
];
// adjust ics status depending on task current status
if (in_array($task->getStatus(), ['notstarted', 'pending'])) {
$data['status'] = 'TENTATIVE';
} else if (in_array($task->getStatus(), ['cancelled', 'archived'])) {
$data['status'] = 'CANCELLED';
}
$ics = '';
/**
* Trigger event to allow the plugins to manipulate the event details before being included.
*
* @param array &$data The event data.
* @param mixed $task The task registry.
* @param mixed $handler The current handler instance.
*
* @return string Some extra rules to include at the end of the event body.
*
* @since 1.18.1 (J) - 1.8.1 (WP)
*/
$extra = $this->dispatcher->filter('onBeforeBuildEventICS', [&$data, $task, $this]);
/**
* Provide a grouping of component properties that describe an event.
*
* @link https://icalendar.org/iCalendar-RFC-5545/3-6-1-event-component.html
*/
$ics .= $this->addLine('BEGIN', 'VEVENT');
/**
* This property specifies the persistent, globally unique identifier for the
* iCalendar object. This can be used, for example, to identify duplicate calendar
* streams that a client may have been given access to.
*
* Generate a md5 string of the order number because "UID" values MUST NOT include any
* data that might identify a user, host, domain, or any other private sensitive information.
*
* @link https://icalendar.org/New-Properties-for-iCalendar-RFC-7986/5-3-uid-property.html
*/
$ics .= $this->addLine('UID', $data['uid']);
/**
* This property specifies when the calendar component begins.
*
* @link https://icalendar.org/iCalendar-RFC-5545/3-8-2-4-date-time-start.html
*
* @since 1.18.1 (J) - 1.8.1 (WP) Changed from VALUE=DATE.
*/
$ics .= $this->addLine(['DTSTART', 'VALUE=DATE-TIME'], $data['start']);
/**
* This property specifies the date and time that a calendar component ends.
*
* @link https://icalendar.org/iCalendar-RFC-5545/3-8-2-2-date-time-end.html
*
* @since 1.18.1 (J) - 1.8.1 (WP) Changed from VALUE=DATE.
*/
$ics .= $this->addLine(['DTEND', 'VALUE=DATE-TIME'], $data['end']);
/**
* In the case of an iCalendar object that specifies a "METHOD" property, this property
* specifies the date and time that the instance of the iCalendar object was created.
* In the case of an iCalendar object that doesn't specify a "METHOD" property, this
* property specifies the date and time that the information associated with the calendar
* component was last revised in the calendar store.
*
* @link https://icalendar.org/iCalendar-RFC-5545/3-8-7-2-date-time-stamp.html
*/
$ics .= $this->addLine('DTSTAMP', $data['created']);
/**
* In case an event is modified through a client, it updates the Last-Modified property to the
* current time. When the calendar is going to refresh an event, in case the Last-Modified is
* not specified or it is lower than the current one, the changes will be discarded.
* For this reason, it is needed to specify our internal modified date in order to refresh
* any existing events with the updated details.
*
* @link https://icalendar.org/iCalendar-RFC-5545/3-8-7-3-last-modified.html
*
* @since 1.18.1 (J) - 1.8.1 (WP)
*/
if ($data['modified']) {
$ics .= $this->addLine('LAST-MODIFIED', $data['modified']);
}
/**
* This property may be used to convey a location where a more dynamic
* rendition of the calendar information can be found.
*
* Google Calendar DOES NOT support this rule.
*
* @link https://icalendar.org/New-Properties-for-iCalendar-RFC-7986/5-5-url-property.html
*
* @since 1.18.1 (J) - 1.8.1 (WP)
*/
if ($this->calendarSubscriber !== 'google') {
$ics .= $this->addLine(['URL', 'VALUE=URI'], $data['url']);
}
/**
* This property defines a short summary or subject for the calendar component.
*
* @link https://icalendar.org/iCalendar-RFC-5545/3-8-1-12-summary.html
*/
$ics .= $this->addLine('SUMMARY', $this->safeContent($data['summary']));
/**
* This property provides a more complete description of the calendar component
* than that provided by the "SUMMARY" property.
*
* @link https://icalendar.org/iCalendar-RFC-5545/3-8-1-5-description.html
*/
if ($data['description']) {
$ics .= $this->addLine('DESCRIPTION', $this->safeContent($data['description']));
}
/**
* This property defines the intended venue for the activity defined by a calendar component.
*
* @link https://icalendar.org/iCalendar-RFC-5545/3-8-1-7-location.html
*/
if ($data['location']) {
$ics .= $this->addLine('LOCATION', $this->safeContent($data['location']));
}
/**
* This property defines whether or not an event is transparent to busy time searches.
*
* @link https://icalendar.org/iCalendar-RFC-5545/3-8-2-7-time-transparency.html
*
* @since 1.18.1 (J) - 1.8.1 (WP)
*/
$ics .= $this->addLine('TRANSP', 'OPAQUE');
/**
* This property defines the overall status or confirmation for the calendar component.
*
* @link https://icalendar.org/iCalendar-RFC-5545/3-8-1-11-status.html
*
* @since 1.18.1 (J) - 1.8.1 (WP)
*/
$ics .= $this->addLine('STATUS', $data['status']);
// append also the values that have been returned by the plugins
$ics .= implode('', array_filter($extra));
/**
* Closes the event properties.
*
* @see BEGIN:VEVENT
*/
$ics .= $this->addLine('END', 'VEVENT');
return $ics;
}
/**
* Adds a line within the ICS buffer by caring of the iCalendar standards.
*
* @param mixed $rule Either the rule command or an array of commands to be concatenated (;).
* @param mixed $content Either the rule content or an array of contents to be concatenated (,).
*
* @return string The compliant ICS declaration.
*
* @since 1.18.1 (J) - 1.8.1 (WP)
*/
public function addLine($rule, $content = null)
{
// concat rules in case of array
if (is_array($rule))
{
// rule with multiple parts, use semi-colon
$rule = implode(';', $rule);
}
// concat contents in case of array
if (is_array($content))
{
// multi-contents list, use comma
$content = implode(',', $content);
}
// create line
if (is_null($content))
{
// we had the full line within the rule
$line = $rule;
}
else
{
// merge rule and content
$line = $rule . ':' . $content;
}
// split string every 73 characters (reserve 2 chars to include new line and space)
$chunks = str_split($line, 73);
// merge lines togheter by using indentation technique,
// then add the line to the buffer
return implode("\n ", $chunks) . "\n";
}
/**
* Escapes the characters of the given content.
*
* @param string $content The content string to make safe.
*
* @return string
*
* @since 1.18.1 (J) - 1.8.1 (WP) Changed visibility from private.
*/
public function safeContent(string $content)
{
return preg_replace('/([\,;])/', '\\\$1', $content);
}
}