summaryrefslogtreecommitdiffstats
path: root/modules-available/locationinfo/inc
diff options
context:
space:
mode:
Diffstat (limited to 'modules-available/locationinfo/inc')
-rw-r--r--modules-available/locationinfo/inc/coursebackend.inc.php98
-rw-r--r--modules-available/locationinfo/inc/coursebackend/coursebackend_davinci.inc.php20
-rw-r--r--modules-available/locationinfo/inc/coursebackend/coursebackend_dummy.inc.php20
-rwxr-xr-xmodules-available/locationinfo/inc/coursebackend/coursebackend_exchange.inc.php55
-rw-r--r--modules-available/locationinfo/inc/coursebackend/coursebackend_hisinone.inc.php383
-rw-r--r--modules-available/locationinfo/inc/coursebackend/coursebackend_ical.inc.php61
-rw-r--r--modules-available/locationinfo/inc/icalcoursebackend.inc.php148
-rw-r--r--modules-available/locationinfo/inc/icalevent.inc.php254
-rw-r--r--modules-available/locationinfo/inc/icalparser.inc.php2052
-rw-r--r--modules-available/locationinfo/inc/infopanel.inc.php64
-rw-r--r--modules-available/locationinfo/inc/locationinfo.inc.php122
-rw-r--r--modules-available/locationinfo/inc/locationinfohooks.inc.php57
12 files changed, 2844 insertions, 490 deletions
diff --git a/modules-available/locationinfo/inc/coursebackend.inc.php b/modules-available/locationinfo/inc/coursebackend.inc.php
index bc5b059e..ea1bebac 100644
--- a/modules-available/locationinfo/inc/coursebackend.inc.php
+++ b/modules-available/locationinfo/inc/coursebackend.inc.php
@@ -19,7 +19,7 @@ abstract class CourseBackend
*/
protected $error = false;
/**
- * @var array list of errors that occured, fill using addError()
+ * @var array list of errors that occurred, fill using addError()
*/
private $errors;
/**
@@ -39,7 +39,7 @@ abstract class CourseBackend
$this->errors = [];
}
- protected final function addError($message, $fatal)
+ protected final function addError(string $message, bool $fatal)
{
$this->errors[] = ['time' => time(), 'message' => $message, 'fatal' => $fatal];
}
@@ -55,7 +55,7 @@ abstract class CourseBackend
self::$backendTypes = array();
foreach (glob(dirname(__FILE__) . '/coursebackend/coursebackend_*.inc.php', GLOB_NOSORT) as $file) {
require_once $file;
- preg_match('#coursebackend_([^/\.]+)\.inc\.php$#i', $file, $out);
+ preg_match('#coursebackend_([^/.]+)\.inc\.php$#i', $file, $out);
$className = 'CourseBackend_' . $out[1];
if (!class_exists($className)) {
trigger_error("Backend type source unit $file doesn't seem to define class $className", E_USER_ERROR);
@@ -71,13 +71,13 @@ abstract class CourseBackend
*
* @return array list of backends
*/
- public static function getList()
+ public static function getList(): array
{
self::loadDb();
return array_keys(self::$backendTypes);
}
- public static function exists($backendType)
+ public static function exists($backendType): bool
{
self::loadDb();
return isset(self::$backendTypes[$backendType]);
@@ -89,7 +89,7 @@ abstract class CourseBackend
* @param string $backendType name of module type
* @return \CourseBackend|false module instance
*/
- public static function getInstance($backendType)
+ public static function getInstance(string $backendType)
{
self::loadDb();
if (!isset(self::$backendTypes[$backendType])) {
@@ -106,18 +106,18 @@ abstract class CourseBackend
/**
* @return string return display name of backend
*/
- public abstract function getDisplayName();
+ public abstract function getDisplayName(): string;
/**
* @returns \BackendProperty[] list of properties that need to be set
*/
- public abstract function getCredentialDefinitions();
+ public abstract function getCredentialDefinitions(): array;
/**
* @return boolean true if the connection works, false otherwise
*/
- public abstract function checkConnection();
+ public abstract function checkConnection(): bool;
/**
* uses json to setCredentials, the json must follow the form given in
@@ -126,36 +126,45 @@ abstract class CourseBackend
* @param array $data assoc array with data required by backend
* @returns bool if the credentials were in the correct format
*/
- public abstract function setCredentialsInternal($data);
+ public abstract function setCredentialsInternal(array $data): bool;
/**
* @return int desired caching time of results, in seconds. 0 = no caching
*/
- public abstract function getCacheTime();
+ public abstract function getCacheTime(): int;
/**
* @return int age after which timetables are no longer refreshed should be
* greater then CacheTime
*/
- public abstract function getRefreshTime();
+ public abstract function getRefreshTime(): int;
/**
* Internal version of fetch, to be overridden by subclasses.
*
- * @param $roomIds array with remote IDs for wanted rooms
+ * @param $requestedRoomIds array with remote IDs for wanted rooms
* @return array a recursive array that uses the roomID as key
* and has the schedule array as value. A schedule array contains an array in this format:
* ["start"=>'JJJJ-MM-DD"T"HH:MM:SS',"end"=>'JJJJ-MM-DD"T"HH:MM:SS',"title"=>string]
*/
- protected abstract function fetchSchedulesInternal($roomId);
+ protected abstract function fetchSchedulesInternal(array $requestedRoomIds): array;
- private static function fixTime(&$start, &$end)
+ /**
+ * In case you want to sanitize or otherwise mangle a property for your backend,
+ * override this.
+ */
+ public function mangleProperty(string $prop, $value)
+ {
+ return $value;
+ }
+
+ private static function fixTime(string &$start, string &$end): bool
{
- if (!preg_match('/^\d+-\d+-\d+T\d+:\d+:\d+$/', $start) || !preg_match('/^\d+-\d+-\d+T\d+:\d+:\d+$/', $end))
+ if (!preg_match('/^(\d{2}|\d{4})-?\d{2}-?\d{2}-?T\d{1,2}:?\d{2}:?(\d{2})?$/', $start))
return false;
$start = strtotime($start);
$end = strtotime($end);
- if ($start >= $end)
+ if ($start === false || $end === false || $start >= $end)
return false;
$start = date('Y-m-d\TH:i:s', $start);
$end = date('Y-m-d\TH:i:s', $end);
@@ -166,14 +175,10 @@ abstract class CourseBackend
* Method for fetching the schedule of the given rooms on a server.
*
* @param array $requestedLocationIds array of room ID to fetch
- * @return array|bool array containing the timetables as value and roomid as key as result, or false on error
+ * @return array array containing the timetables as value and roomid as key as result, or false on error
*/
- public final function fetchSchedule($requestedLocationIds)
+ public final function fetchSchedule(array $requestedLocationIds): array
{
- if (!is_array($requestedLocationIds)) {
- $this->addError('No array of roomids was given to fetchSchedule', false);
- return false;
- }
if (empty($requestedLocationIds))
return array();
$requestedLocationIds = array_values($requestedLocationIds);
@@ -183,14 +188,13 @@ abstract class CourseBackend
array('locations' => $requestedLocationIds));
$returnValue = [];
$remoteIds = [];
- while ($row = $dbquery1->fetch(PDO::FETCH_ASSOC)) {
- //Check if in cache if lastUpdate is null then it is interpreted as 1970
- if ($row['lastcalendarupdate'] + $this->getCacheTime() > $NOW) {
- $returnValue[$row['locationid']] = json_decode($row['calendar']);
- } else {
+ foreach ($dbquery1 as $row) {
+ // Check if in cache - if lastUpdate is null then it is interpreted as 1970
+ if ($row['lastcalendarupdate'] + $this->getCacheTime() < $NOW) {
$remoteIds[$row['locationid']] = $row['serverlocationid'];
}
-
+ // Always add to return value - if updating fails, we better use the stale data than nothing
+ $returnValue[$row['locationid']] = json_decode($row['calendar'], true);
}
// No need for additional round trips to backend
if (empty($remoteIds)) {
@@ -211,7 +215,7 @@ abstract class CourseBackend
'lastuse' => $NOW - $this->getRefreshTime(),
'minage' => $NOW - $this->getCacheTime(),
));
- while ($row = $dbquery4->fetch(PDO::FETCH_ASSOC)) {
+ foreach ($dbquery4 as $row) {
$remoteIds[$row['locationid']] = $row['serverlocationid'];
}
}
@@ -222,15 +226,12 @@ abstract class CourseBackend
// if, nothing bad will happen...
Database::exec("UPDATE locationinfo_locationconfig SET lastcalendarupdate = :time
WHERE lastcalendarupdate < :time AND serverid = :serverid AND serverlocationid IN (:slocs)", [
- 'time' => $NOW - $this->getCacheTime() / 2,
+ 'time' => $NOW - ($this->getCacheTime() - 60), // Protect for one minute max.
'serverid' => $this->serverId,
'slocs' => array_values($remoteIds),
]);
}
$backendResponse = $this->fetchSchedulesInternal(array_unique($remoteIds));
- if ($backendResponse === false) {
- return false;
- }
// Fetching might have taken a while, get current time again
$NOW = time();
@@ -254,15 +255,15 @@ abstract class CourseBackend
'serverid' => $this->serverId,
'serverlocationid' => $serverRoomId,
'ttable' => $value,
- 'now' => $NOW
+ 'now' => $NOW, // Set real "lastupdate" here
));
}
-
- unset($calendar);
}
+ unset($calendar);
// Add rooms that were requested to the final return value
foreach ($remoteIds as $location => $serverRoomId) {
- if (isset($backendResponse[$serverRoomId]) && in_array($location, $requestedLocationIds)) {
+ if (isset($backendResponse[$serverRoomId]) && is_array($backendResponse[$serverRoomId])
+ && in_array($location, $requestedLocationIds)) {
// Only add if we can map it back to our location id AND it was not an unsolicited coalesced refresh
$returnValue[$location] = $backendResponse[$serverRoomId];
}
@@ -271,7 +272,7 @@ abstract class CourseBackend
return $returnValue;
}
- public final function setCredentials($serverId, $data)
+ public final function setCredentials(int $serverId, array $data): bool
{
foreach ($this->getCredentialDefinitions() as $prop) {
if (!isset($data[$prop->property])) {
@@ -291,18 +292,9 @@ abstract class CourseBackend
}
/**
- * @return false if there was no error string with error message if there was one
- */
- public final function getError()
- {
- trigger_error('getError() is legacy; use getErrors()');
- return $this->error;
- }
-
- /**
- * @return array list of errors that occured during processing.
+ * @return array list of errors that occurred during processing.
*/
- public final function getErrors()
+ public final function getErrors(): array
{
return $this->errors;
}
@@ -367,7 +359,7 @@ abstract class CourseBackend
* @param string $response xml document to convert
* @return bool|array array representation of the xml if possible, false otherwise
*/
- protected function xmlStringToArray($response, &$error)
+ protected function xmlStringToArray(string $response, &$error)
{
$cleanresponse = preg_replace('/(<\/?)(\w+):([^>]*>)/', '$1$2$3', $response);
try {
@@ -379,8 +371,7 @@ abstract class CourseBackend
}
return false;
}
- $array = json_decode(json_encode((array)$xml), true);
- return $array;
+ return json_decode(json_encode((array)$xml), true);
}
}
@@ -403,7 +394,6 @@ class BackendProperty {
* Initialize additional fields of this class that are only required
* for rendering the server configuration dialog.
*
- * @param string $backendId target backend id
* @param mixed $current current value of this property.
*/
public function initForRender($current = null) {
diff --git a/modules-available/locationinfo/inc/coursebackend/coursebackend_davinci.inc.php b/modules-available/locationinfo/inc/coursebackend/coursebackend_davinci.inc.php
index 07c8457d..786ab459 100644
--- a/modules-available/locationinfo/inc/coursebackend/coursebackend_davinci.inc.php
+++ b/modules-available/locationinfo/inc/coursebackend/coursebackend_davinci.inc.php
@@ -11,7 +11,7 @@ class CourseBackend_Davinci extends CourseBackend
*/
private $curlHandle = false;
- public function setCredentialsInternal($data)
+ public function setCredentialsInternal(array $data): bool
{
if (empty($data['baseUrl'])) {
$this->addError("No url is given", true);
@@ -24,7 +24,7 @@ class CourseBackend_Davinci extends CourseBackend
return true;
}
- public function checkConnection()
+ public function checkConnection(): bool
{
if (empty($this->location)) {
$this->addError("Credentials are not set", true);
@@ -40,7 +40,7 @@ class CourseBackend_Davinci extends CourseBackend
return true;
}
- public function getCredentialDefinitions()
+ public function getCredentialDefinitions(): array
{
return [
new BackendProperty('baseUrl', 'string'),
@@ -49,17 +49,17 @@ class CourseBackend_Davinci extends CourseBackend
];
}
- public function getDisplayName()
+ public function getDisplayName(): string
{
return 'Davinci';
}
- public function getCacheTime()
+ public function getCacheTime(): int
{
return 30 * 60;
}
- public function getRefreshTime()
+ public function getRefreshTime(): int
{
return 0;
}
@@ -68,9 +68,9 @@ class CourseBackend_Davinci extends CourseBackend
* @param string $roomId unique name of the room, as used by davinci
* @param \DateTime $startDate start date to fetch
* @param \DateTime $endDate end date of range to fetch
- * @return array|bool if successful the arrayrepresentation of the timetable
+ * @return false|string if successful the array representation of the timetable
*/
- private function fetchRoomRaw($roomId, $startDate, $endDate)
+ private function fetchRoomRaw(string $roomId, DateTime $startDate, DateTime $endDate)
{
$url = $this->location . "content=xml&type=room&name=" . urlencode($roomId)
. "&startdate=" . $startDate->format('d.m.Y') . "&enddate=" . $endDate->format('d.m.Y');
@@ -97,7 +97,7 @@ class CourseBackend_Davinci extends CourseBackend
}
- public function fetchSchedulesInternal($requestedRoomIds)
+ public function fetchSchedulesInternal(array $requestedRoomIds): array
{
$startDate = new DateTime('last Monday 0:00');
$endDate = new DateTime('+14 days 0:00');
@@ -134,7 +134,7 @@ class CourseBackend_Davinci extends CourseBackend
$start = substr($start, 0, 2) . ':' . substr($start, 2, 2);
$end = $lesson['Finish'];
$end = substr($end, 0, 2) . ':' . substr($end, 2, 2);
- $subject = isset($lesson['Subject']) ? $lesson['Subject'] : '???';
+ $subject = $lesson['Subject'] ?? '???';
$timetable[] = array(
'title' => $subject,
'start' => $date . "T" . $start . ':00',
diff --git a/modules-available/locationinfo/inc/coursebackend/coursebackend_dummy.inc.php b/modules-available/locationinfo/inc/coursebackend/coursebackend_dummy.inc.php
index 2cb2be18..4588bf7c 100644
--- a/modules-available/locationinfo/inc/coursebackend/coursebackend_dummy.inc.php
+++ b/modules-available/locationinfo/inc/coursebackend/coursebackend_dummy.inc.php
@@ -15,9 +15,9 @@ class CourseBackend_Dummy extends CourseBackend
* @param int $serverId ID of the server
* @returns bool if the credentials were in the correct format
*/
- public function setCredentialsInternal($json)
+ public function setCredentialsInternal(array $data): bool
{
- $x = $json;
+ $x = $data;
$this->pw = $x['password'];
if ($this->pw === "mfg") {
@@ -30,7 +30,7 @@ class CourseBackend_Dummy extends CourseBackend
/**
* @return boolean true if the connection works, false otherwise
*/
- public function checkConnection()
+ public function checkConnection(): bool
{
if ($this->pw == "mfg") {
return true;
@@ -42,7 +42,7 @@ class CourseBackend_Dummy extends CourseBackend
/**
* @returns array with parameter name as key and and an array with type, help text and mask as value
*/
- public function getCredentialDefinitions()
+ public function getCredentialDefinitions(): array
{
$options = ["opt1", "opt2", "opt3", "opt4", "opt5", "opt6", "opt7", "opt8"];
return [
@@ -58,7 +58,7 @@ class CourseBackend_Dummy extends CourseBackend
/**
* @return string return display name of backend
*/
- public function getDisplayName()
+ public function getDisplayName(): string
{
return 'Dummy with array';
}
@@ -66,7 +66,7 @@ class CourseBackend_Dummy extends CourseBackend
/**
* @return int desired caching time of results, in seconds. 0 = no caching
*/
- public function getCacheTime()
+ public function getCacheTime(): int
{
return 0;
}
@@ -75,7 +75,7 @@ class CourseBackend_Dummy extends CourseBackend
* @return int age after which timetables are no longer refreshed should be
* greater then CacheTime
*/
- public function getRefreshTime()
+ public function getRefreshTime(): int
{
return 0;
}
@@ -83,15 +83,15 @@ class CourseBackend_Dummy extends CourseBackend
/**
* Internal version of fetch, to be overridden by subclasses.
*
- * @param $roomIds array with local ID as key and serverId as value
+ * @param $requestedRoomIds array with local ID as key and serverId as value
* @return array a recursive array that uses the roomID as key
* and has the schedule array as value. A schedule array contains an array in this format:
* ["start"=>'YYYY-MM-DD<T>HH:MM:SS',"end"=>'YYYY-MM-DD<T>HH:MM:SS',"title"=>string]
*/
- public function fetchSchedulesInternal($roomId)
+ public function fetchSchedulesInternal(array $requestedRoomIds): array
{
$a = array();
- foreach ($roomId as $id) {
+ foreach ($requestedRoomIds as $id) {
if ($id == 1) {
$now = time();
return array($id => array(
diff --git a/modules-available/locationinfo/inc/coursebackend/coursebackend_exchange.inc.php b/modules-available/locationinfo/inc/coursebackend/coursebackend_exchange.inc.php
index 60561586..df33dadd 100755
--- a/modules-available/locationinfo/inc/coursebackend/coursebackend_exchange.inc.php
+++ b/modules-available/locationinfo/inc/coursebackend/coursebackend_exchange.inc.php
@@ -12,6 +12,7 @@ spl_autoload_register(function ($class) {
require_once $file;
});
+use jamesiarmes\PhpEws\ArrayType\NonEmptyArrayOfBaseFolderIdsType;
use jamesiarmes\PhpEws\Client;
use jamesiarmes\PhpEws\Enumeration\DefaultShapeNamesType;
use jamesiarmes\PhpEws\Enumeration\DistinguishedFolderIdNameType;
@@ -38,7 +39,7 @@ class CourseBackend_Exchange extends CourseBackend
/**
* @return string return display name of backend
*/
- public function getDisplayName()
+ public function getDisplayName(): string
{
return "Microsoft Exchange";
}
@@ -46,7 +47,7 @@ class CourseBackend_Exchange extends CourseBackend
/**
* @returns \BackendProperty[] list of properties that need to be set
*/
- public function getCredentialDefinitions()
+ public function getCredentialDefinitions(): array
{
$options = [
Client::VERSION_2007,
@@ -72,7 +73,7 @@ class CourseBackend_Exchange extends CourseBackend
/**
* @return boolean true if the connection works, false otherwise
*/
- public function checkConnection()
+ public function checkConnection(): bool
{
$client = $this->getClient();
$request = new ResolveNamesType();
@@ -104,7 +105,7 @@ class CourseBackend_Exchange extends CourseBackend
* @param array $data assoc array with data required by backend
* @returns bool if the credentials were in the correct format
*/
- public function setCredentialsInternal($data)
+ public function setCredentialsInternal(array $data): bool
{
foreach (['username', 'password'] as $field) {
if (empty($data[$field])) {
@@ -133,7 +134,7 @@ class CourseBackend_Exchange extends CourseBackend
/**
* @return int desired caching time of results, in seconds. 0 = no caching
*/
- public function getCacheTime()
+ public function getCacheTime(): int
{
return 15 * 60;
}
@@ -142,7 +143,7 @@ class CourseBackend_Exchange extends CourseBackend
* @return int age after which timetables are no longer refreshed. should be
* greater than CacheTime.
*/
- public function getRefreshTime()
+ public function getRefreshTime(): int
{
return 30 * 60;
}
@@ -150,12 +151,12 @@ class CourseBackend_Exchange extends CourseBackend
/**
* Internal version of fetch, to be overridden by subclasses.
*
- * @param $roomIds array with local ID as key and serverId as value
+ * @param $requestedRoomIds array with local ID as key and serverId as value
* @return array a recursive array that uses the roomID as key
- * and has the schedule array as value. A shedule array contains an array in this format:
+ * and has the schedule array as value. A schedule array contains an array in this format:
* ["start"=>'JJJJ-MM-DD HH:MM:SS',"end"=>'JJJJ-MM-DD HH:MM:SS',"title"=>string]
*/
- protected function fetchSchedulesInternal($requestedRoomIds)
+ protected function fetchSchedulesInternal(array $requestedRoomIds): array
{
$startDate = new DateTime('last Monday 0:00');
$endDate = new DateTime('+14 days 0:00');
@@ -172,8 +173,13 @@ class CourseBackend_Exchange extends CourseBackend
// Iterate over the events that were found, printing some data for each.
foreach ($items as $item) {
- $start = new DateTime($item->Start);
- $end = new DateTime($item->End);
+ try {
+ $start = new DateTime($item->Start);
+ $end = new DateTime($item->End);
+ } catch (Exception $e) {
+ $this->addError("Invalid date range: '{$item->Start}' -> '{$item->End}'", false);
+ continue;
+ }
$schedules[$roomId][] = array(
'title' => $item->Subject,
@@ -186,13 +192,9 @@ class CourseBackend_Exchange extends CourseBackend
}
/**
- * @param \jamesiarmes\PhpEws\Client $client
- * @param \DateTime $startDate
- * @param \DateTime $endDate
- * @param string $roomAddress
* @return \jamesiarmes\PhpEws\Type\CalendarItemType[]
*/
- public function findEventsForRoom($client, $startDate, $endDate, $roomAddress)
+ public function findEventsForRoom(Client $client, DateTime $startDate, DateTime $endDate, string $roomAddress): array
{
$request = new FindItemType();
$request->Traversal = ItemQueryTraversalType::SHALLOW;
@@ -206,12 +208,20 @@ class CourseBackend_Exchange extends CourseBackend
$folderId->Id = DistinguishedFolderIdNameType::CALENDAR;
$folderId->Mailbox = new EmailAddressType();
$folderId->Mailbox->EmailAddress = $roomAddress;
+ $request->ParentFolderIds = new NonEmptyArrayOfBaseFolderIdsType();
$request->ParentFolderIds->DistinguishedFolderId[] = $folderId;
- $response = $client->FindItem($request);
- $response_messages = $response->ResponseMessages->FindItemResponseMessage;
-
+ try {
+ $response = $client->FindItem($request);
+ } catch (Exception $e) {
+ $this->addError('Exception calling FindItem: ' . $e->getMessage(), true);
+ return [];
+ }
+ if (!is_object($response->ResponseMessages)) {
+ $this->addError('FindItem returned response without ResponseMessages', true);
+ return [];
+ }
$items = [];
- foreach ($response_messages as $response_message) {
+ foreach ($response->ResponseMessages->FindItemResponseMessage as $response_message) {
// Make sure the request succeeded.
if ($response_message->ResponseClass !== ResponseClassType::SUCCESS) {
$code = $response_message->ResponseCode;
@@ -224,10 +234,7 @@ class CourseBackend_Exchange extends CourseBackend
return $items;
}
- /**
- * @return \jamesiarmes\PhpEws\Client
- */
- public function getClient()
+ public function getClient(): Client
{
$client = new Client($this->serverAddress, $this->username, $this->password, $this->clientVersion);
$client->setTimezone($this->timezone);
diff --git a/modules-available/locationinfo/inc/coursebackend/coursebackend_hisinone.inc.php b/modules-available/locationinfo/inc/coursebackend/coursebackend_hisinone.inc.php
index 4664a011..55d5ed4b 100644
--- a/modules-available/locationinfo/inc/coursebackend/coursebackend_hisinone.inc.php
+++ b/modules-available/locationinfo/inc/coursebackend/coursebackend_hisinone.inc.php
@@ -1,391 +1,84 @@
<?php
-class CourseBackend_HisInOne extends CourseBackend
+class CourseBackend_HisInOne extends ICalCourseBackend
{
- private $username = '';
- private $password = '';
- private $open = true;
- private $location;
- private $verifyHostname = true;
- private $verifyCert = true;
- /**
- * @var bool|resource
- */
- private $curlHandle = false;
-
- public function setCredentialsInternal($data)
+ public function setCredentialsInternal(array $data): bool
{
- if (!$data['open']) {
- // If not using OpenCourseService, require credentials
- foreach (['username', 'password'] as $field) {
- if (empty($data[$field])) {
- $this->addError('setCredentials: Missing field ' . $field, true);
- return false;
- }
- }
- }
if (empty($data['baseUrl'])) {
$this->addError("No url is given", true);
return false;
}
- $this->username = $data['username'];
- if (!empty($data['role'])) {
- $this->username .= "\t" . $data['role'];
- }
- $this->password = $data['password'];
- $this->open = $data['open'] !== 'CourseService';
- $url = preg_replace('#(/+qisserver(/+services\d+(/+OpenCourseService)?)?)?\W*$#i', '', $data['baseUrl']);
- if ($this->open) {
- $this->location = $url . "/qisserver/services2/OpenCourseService";
- } else {
- $this->location = $url . "/qisserver/services2/CourseService";
- }
- $this->verifyHostname = $data['verifyHostname'];
- $this->verifyCert = $data['verifyCert'];
+ $this->init($this->mangleProperty('baseUrl', $data['baseUrl']),
+ $data['verifyCert'], $data['verifyHostname']);
return true;
}
- public function getCredentialDefinitions()
+ public function getCredentialDefinitions(): array
{
return [
new BackendProperty('baseUrl', 'string'),
- new BackendProperty('username', 'string'),
- new BackendProperty('role', 'string'),
- new BackendProperty('password', 'password'),
- new BackendProperty('open', ['OpenCourseService', 'CourseService'], 'OpenCourseService'),
new BackendProperty('verifyCert', 'bool', true),
new BackendProperty('verifyHostname', 'bool', true)
];
}
- public function checkConnection()
+ public function mangleProperty(string $prop, $value)
{
- if (empty($this->location)) {
- $this->addError("Credentials are not set", true);
- return false;
- }
- return $this->findUnit(123456789, date('Y-m-d'), true) !== false;
- }
-
- /**
- * @param int $roomId his in one room id to get
- * @param bool $connectionCheckOnly true will only check if no soapError is returned, return value will be empty
- * @return array|bool if successful an array with the event ids that take place in the room
- */
- public function findUnit($roomId, $day, $connectionCheckOnly = false)
- {
- $doc = new DOMDocument('1.0', 'utf-8');
- $doc->formatOutput = true;
- $envelope = $doc->createElementNS('http://schemas.xmlsoap.org/soap/envelope/', 'SOAP-ENV:Envelope');
- $doc->appendChild($envelope);
- if ($this->open) {
- $envelope->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:ns1', 'http://www.his.de/ws/OpenCourseService');
- } else {
- $envelope->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:ns1', 'http://www.his.de/ws/CourseService');
- }
- $header = $this->getHeader($doc);
- $envelope->appendChild($header);
- //Body of the request
- $body = $doc->createElement('SOAP-ENV:Body');
- $envelope->appendChild($body);
- $findUnit = $doc->createElement('ns1:findUnit');
- $body->appendChild($findUnit);
- $findUnit->appendChild($doc->createElement('ns1:individualDatesExecutionDate', $day));
- $findUnit->appendChild($doc->createElement('ns1:roomId', $roomId));
-
- $soap_request = $doc->saveXML();
- $response1 = $this->postToServer($soap_request, "findUnit");
- if ($response1 === false) {
- $this->addError('Could not fetch room ' . $roomId, true);
- return false;
- }
- $response2 = $this->xmlStringToArray($response1, $err);
- if (!is_array($response2)) {
- $this->addError("Parsing room $roomId: $err", false);
- return false;
- }
- if (!isset($response2['soapenvBody'])) {
- $this->addError('Backend reply is missing element soapenvBody', true);
- return false;
- }
- if (isset($response2['soapenvBody']['soapenvFault'])) {
- $this->addError('SOAP-Fault (' . $response2['soapenvBody']['soapenvFault']['faultcode'] . ") " . $response2['soapenvBody']['soapenvFault']['faultstring'], true);
- return false;
- }
- // We only need to check if the connection is working (URL ok, credentials ok, ..) so bail out early
- if ($connectionCheckOnly) {
- return array();
- }
- if ($this->open) {
- $path = '/soapenvBody/hisfindUnitResponse/hisunits';
- $subpath = '/hisunit/hisid';
- } else {
- $path = '/soapenvBody/hisfindUnitResponse/hisunitIds';
- $subpath = '/hisid';
- }
- $idSubDoc = $this->getArrayPath($response2, $path);
- if ($idSubDoc === false) {
- $this->addError('Cannot find ' . $path, false);
- //@file_put_contents('/tmp/findUnit-1.' . $roomId . '.' . microtime(true), print_r($response2, true));
- return false;
- }
- if (empty($idSubDoc))
- return $idSubDoc;
- $idList = $this->getArrayPath($idSubDoc, $subpath);
- if ($idList === false) {
- $this->addError('Cannot find ' . $subpath . ' after ' . $path, false);
- @file_put_contents('/tmp/bwlp-findUnit-2.' . $roomId . '.' . microtime(true), print_r($idSubDoc, true));
+ if ($prop === 'baseUrl') {
+ // Update form SOAP to iCal url
+ if (preg_match(',^(http.*?)/qisserver,', $value, $out)) {
+ $value = $out[1] . '/qisserver/pages/cm/exa/timetable/roomScheduleCalendarExport.faces?roomId=%ID%';
+ } elseif (preg_match(',(.*[/=])\d*$', $value, $out)) {
+ $value = $out[1] . '%ID%';
+ } elseif (substr_count($value, '/') <= 3) {
+ if (substr($value, -1) !== '/') {
+ $value .= '/';
+ }
+ $value .= 'qisserver/pages/cm/exa/timetable/roomScheduleCalendarExport.faces?roomId=%ID%';
+ }
}
- return $idList;
+ return $value;
}
- /**
- * @param $doc DOMDocument
- * @return DOMElement
- */
- private function getHeader($doc)
+ protected function toTitle(ICalEvent $event): string
{
- $header = $doc->createElement('SOAP-ENV:Header');
- $security = $doc->createElementNS('http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd', 'ns2:Security');
- $mustunderstand = $doc->createAttribute('SOAP-ENV:mustUnderstand');
- $mustunderstand->value = 1;
- $security->appendChild($mustunderstand);
- $header->appendChild($security);
- $token = $doc->createElement('ns2:UsernameToken');
- $security->appendChild($token);
- $user = $doc->createElement('ns2:Username', $this->username);
- $token->appendChild($user);
- $pass = $doc->createElement('ns2:Password', $this->password);
- $type = $doc->createAttribute('Type');
- $type->value = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText';
- $pass->appendChild($type);
- $token->appendChild($pass);
- return $header;
+ $title = parent::toTitle($event);
+ // His in one seems to prefix *some* (but *not* all) of the lectures by their ID/("Nummer")
+ // No clue what that format is supposed to be, this regex is some guesswork after observing this for a while
+ return preg_replace('#^[0-9][0-9A-ZÄÖÜ]{3,9}-[A-Za-z0-9/_ÄÖÜäöüß.-]{4,30}\s+#u', '', $title);
}
- /**
- * @param $request string with xml SOAP request
- * @param $action string with the name of the SOAP action
- * @return bool|string if successful the answer xml from the SOAP server
- */
- private function postToServer($request, $action)
+ public function checkConnection(): bool
{
- $header = array(
- 'Content-type: text/xml;charset="utf-8"',
- 'SOAPAction: "' . $action . '"',
- );
-
- if ($this->curlHandle === false) {
- $this->curlHandle = curl_init();
- }
-
- $options = array(
- CURLOPT_RETURNTRANSFER => true,
- CURLOPT_FOLLOWLOCATION => true,
- CURLOPT_SSL_VERIFYHOST => $this->verifyHostname ? 2 : 0,
- CURLOPT_SSL_VERIFYPEER => $this->verifyCert ? 1 : 0,
- CURLOPT_URL => $this->location,
- CURLOPT_POSTFIELDS => $request,
- CURLOPT_HTTPHEADER => $header,
- CURLOPT_TIMEOUT => 15,
- CURLOPT_CONNECTTIMEOUT => 3,
- );
-
- curl_setopt_array($this->curlHandle, $options);
-
- $output = curl_exec($this->curlHandle);
-
- if ($output === false) {
- $this->addError('Curl error: ' . curl_error($this->curlHandle), false);
- }
- return $output;
+ if (!$this->isOK())
+ return false;
+ // Unfortunately HisInOne returns an internal server error if you pass an invalid roomId.
+ // So we just try a bunch and see if anything works. Even if this fails, using
+ // the backend should work, given the URL is actually correct.
+ foreach ([60, 100, 5, 10, 50, 110, 200, 210, 250, 300, 333, 500, 1000, 2000] as $roomId) {
+ if ($this->downloadIcal($roomId) !== null)
+ return true;
+ }
+ return false;
}
- public function getCacheTime()
+ public function getCacheTime(): int
{
return 30 * 60;
}
-
- public function getRefreshTime()
+ public function getRefreshTime(): int
{
return 60 * 60;
}
- public function getDisplayName()
+ public function getDisplayName(): string
{
return "HisInOne";
}
- public function fetchSchedulesInternal($requestedRoomIds)
- {
- if (empty($requestedRoomIds)) {
- return array();
- }
- $currentWeek = $this->getCurrentWeekDates();
- $tTables = [];
- //get all eventIDs in a given room
- $eventIds = [];
- foreach ($requestedRoomIds as $roomId) {
- $ok = false;
- foreach ($currentWeek as $day) {
- $roomEventIds = $this->findUnit($roomId, $day, false);
- if ($roomEventIds === false)
- continue;
- $ok = true;
- $eventIds = array_merge($eventIds, $roomEventIds);
- }
- if ($ok) {
- $tTables[$roomId] = [];
- }
- }
- $eventIds = array_unique($eventIds);
- if (empty($eventIds)) {
- return $tTables;
- }
- $eventDetails = [];
- //get all information on each event
- foreach ($eventIds as $eventId) {
- $event = $this->readUnit(intval($eventId));
- if ($event === false)
- continue;
- $eventDetails = array_merge($eventDetails, $event);
- }
- $name = false;
- $now = time();
- foreach ($eventDetails as $event) {
- foreach (array('/hisdefaulttext',
- '/hisshorttext',
- '/hisshortcomment') as $path) {
- $name = $this->getArrayPath($event, $path);
- if (!empty($name) && !empty($name[0]))
- break;
- $name = false;
- }
- if ($name === false) {
- $name = ['???'];
- }
- $planElements = $this->getArrayPath($event, '/hisplanelements/hisplanelement');
- if ($planElements === false) {
- $this->addError('Cannot find ./hisplanelements/hisplanelement', false);
- //error_log('Cannot find ./hisplanelements/hisplanelement');
- //error_log(print_r($event, true));
- continue;
- }
- foreach ($planElements as $planElement) {
- if (empty($planElement['hisplannedDates']))
- continue;
- // Do not use -- is set improperly for some courses :-(
- /*
- $checkDate = $this->getArrayPath($planElement, '/hisplannedDates/hisplannedDate/hisenddate');
- if (!empty($checkDate) && strtotime($checkDate[0]) + 86400 < $now)
- continue; // Course ended
- $checkDate = $this->getArrayPath($planElement, '/hisplannedDates/hisplannedDate/hisstartdate');
- if (!empty($checkDate) && strtotime($checkDate[0]) - 86400 > $now)
- continue; // Course didn't start yet
- */
- $cancelled = $this->getArrayPath($planElement, '/hiscancelled');
- $cancelled = $cancelled !== false && is_array($cancelled) && ($cancelled[0] > 0 || strtolower($cancelled[0]) === 'true');
- $unitPlannedDates = $this->getArrayPath($planElement,
- '/hisplannedDates/hisplannedDate/hisindividualDates/hisindividualDate');
- if ($unitPlannedDates === false) {
- $this->addError('Cannot find ./hisplannedDates/hisplannedDate/hisindividualDates/hisindividualDate', false);
- //error_log('Cannot find ./hisplannedDates/hisplannedDate/hisindividualDates/hisindividualDate');
- //error_log(print_r($planElement, true));
- continue;
- }
- $localName = $this->getArrayPath($planElement, '/hisdefaulttext');
- if ($localName === false || empty($localName[0])) {
- $localName = $name;
- }
- foreach ($unitPlannedDates as $plannedDate) {
- $eventRoomId = $this->getArrayPath($plannedDate, '/hisroomId')[0];
- $eventDate = $this->getArrayPath($plannedDate, '/hisexecutiondate')[0];
- if (in_array($eventRoomId, $requestedRoomIds) && in_array($eventDate, $currentWeek)) {
- $startTime = $this->getArrayPath($plannedDate, '/hisstarttime')[0];
- $endTime = $this->getArrayPath($plannedDate, '/hisendtime')[0];
- $tTables[$eventRoomId][] = array(
- 'title' => $localName[0],
- 'start' => $eventDate . "T" . $startTime,
- 'end' => $eventDate . "T" . $endTime,
- 'cancelled' => $cancelled,
- );
- }
- }
- }
- }
- return $tTables;
- }
-
-
- /**
- * @param $unit int ID of the subject in HisInOne database
- * @return bool|array false if there was an error otherwise an array with the information about the subject
- */
- public function readUnit($unit)
- {
- $doc = new DOMDocument('1.0', 'utf-8');
- $doc->formatOutput = true;
- $envelope = $doc->createElementNS('http://schemas.xmlsoap.org/soap/envelope/', 'SOAP-ENV:Envelope');
- $doc->appendChild($envelope);
- if ($this->open) {
- $envelope->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:ns1', 'http://www.his.de/ws/OpenCourseService');
- } else {
- $envelope->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:ns1', 'http://www.his.de/ws/CourseService');
- }
- $header = $this->getHeader($doc);
- $envelope->appendChild($header);
- //body of the request
- $body = $doc->createElement('SOAP-ENV:Body');
- $envelope->appendChild($body);
- $readUnit = $doc->createElement('ns1:readUnit');
- $body->appendChild($readUnit);
- $readUnit->appendChild($doc->createElement('ns1:unitId', $unit));
-
- $soap_request = $doc->saveXML();
- $response1 = $this->postToServer($soap_request, "readUnit");
- if ($response1 === false) {
- return false;
- }
- $response2 = $this->xmlStringToArray($response1, $err);
- if ($response2 === false) {
- $this->addError("Cannot parse unit $unit as XML: $err", false);
- return false;
- }
- if (!isset($response2['soapenvBody'])) {
- $this->addError('Backend reply is missing element soapenvBody', true);
- return false;
- }
- if (isset($response2['soapenvBody']['soapenvFault'])) {
- $this->addError('SOAP-Fault (' . $response2['soapenvBody']['soapenvFault']['faultcode'] . ") " . $response2['soapenvBody']['soapenvFault']['faultstring'], true);
- return false;
- }
- return $this->getArrayPath($response2, '/soapenvBody/hisreadUnitResponse/hisunit');
- }
-
- /**
- * @return array with days of the current week in datetime format
- */
- private function getCurrentWeekDates()
- {
- $returnValue = array();
- $date = date('Y-m-d', strtotime('last Monday'));
- for ($i = 0; $i < 14; $i++) {
- $returnValue[] = $date;
- $date = date('Y-m-d', strtotime($date.' +1 day'));
- }
- return $returnValue;
- }
-
- public function __destruct()
- {
- if ($this->curlHandle !== false) {
- curl_close($this->curlHandle);
- }
- }
-
}
diff --git a/modules-available/locationinfo/inc/coursebackend/coursebackend_ical.inc.php b/modules-available/locationinfo/inc/coursebackend/coursebackend_ical.inc.php
new file mode 100644
index 00000000..f1791c4e
--- /dev/null
+++ b/modules-available/locationinfo/inc/coursebackend/coursebackend_ical.inc.php
@@ -0,0 +1,61 @@
+<?php
+
+class CourseBackend_ICal extends ICalCourseBackend
+{
+
+ /** @var string room ID for testing connection */
+ private $testId;
+
+ public function setCredentialsInternal(array $data): bool
+ {
+ if (empty($data['baseUrl'])) {
+ $this->addError("No url is given", true);
+ return false;
+ }
+
+ $this->init($data['baseUrl'], $data['verifyCert'], $data['verifyHostname'], $data['authMethod'],
+ $data['user'], $data['pass']);
+ $this->testId = $data['testId'];
+
+ return true;
+ }
+
+ public function getCredentialDefinitions(): array
+ {
+ return [
+ new BackendProperty('baseUrl', 'string'),
+ new BackendProperty('verifyCert', 'bool', true),
+ new BackendProperty('verifyHostname', 'bool', true),
+ new BackendProperty('testId', 'string'),
+ new BackendProperty('authMethod', ['NONE', 'BASIC', 'DIGEST', 'GSSNEGOTIATE', 'NTLM'], 'NONE'),
+ new BackendProperty('user', 'string'),
+ new BackendProperty('pass', 'string'),
+ ];
+ }
+
+ public function checkConnection(): bool
+ {
+ if (!$this->isOK())
+ return false;
+ if (empty($this->testId))
+ return true;
+ return ($this->downloadIcal($this->testId) !== null);
+ }
+
+ public function getCacheTime(): int
+ {
+ return 30 * 60;
+ }
+
+ public function getRefreshTime(): int
+ {
+ return 60 * 60;
+ }
+
+
+ public function getDisplayName(): string
+ {
+ return "iCal";
+ }
+
+}
diff --git a/modules-available/locationinfo/inc/icalcoursebackend.inc.php b/modules-available/locationinfo/inc/icalcoursebackend.inc.php
new file mode 100644
index 00000000..838d18b7
--- /dev/null
+++ b/modules-available/locationinfo/inc/icalcoursebackend.inc.php
@@ -0,0 +1,148 @@
+<?php
+
+abstract class ICalCourseBackend extends CourseBackend
+{
+
+ /** @var string */
+ private $location;
+ /** @var string */
+ private $authMethod;
+ /** @var string */
+ private $user;
+ /** @var string */
+ private $pass;
+ /** @var bool */
+ private $verifyHostname;
+ /** @var bool */
+ private $verifyCert;
+ /** @var bool|resource */
+ private $curlHandle = false;
+
+ protected function init(
+ string $location, bool $verifyCert, bool $verifyHostname,
+ string $authMethod = 'NONE', string $user = '', string $pass = '')
+ {
+ $this->verifyCert = $verifyCert;
+ $this->verifyHostname = $verifyHostname;
+ if (strpos($location, '%ID%') === false) {
+ $location .= '%ID%';
+ }
+ $this->location = $location;
+ $this->authMethod = $authMethod;
+ $this->user = $user;
+ $this->pass = $pass;
+ }
+
+ /**
+ * @param string $roomId room id
+ * @param callable $errorFunc
+ * @return ICalEvent[]|null all events for this room in the range -7 days to +7 days, or NULL on error
+ */
+ protected function downloadIcal(string $roomId): ?array
+ {
+ if (!$this->isOK())
+ return null;
+
+ try {
+ $ical = new ICalParser(['filterDaysBefore' => 7, 'filterDaysAfter' => 7]);
+ } catch (Exception $e) {
+ $this->addError('Error instantiating ICalParser: ' . $e->getMessage(), true);
+ return null;
+ }
+
+ if ($this->curlHandle === false) {
+ $this->curlHandle = curl_init();
+ }
+
+ $options = [
+ CURLOPT_WRITEFUNCTION => function ($ch, $data) use ($ical) {
+ $ical->feedData($data);
+ return strlen($data);
+ },
+ CURLOPT_FOLLOWLOCATION => true,
+ CURLOPT_SSL_VERIFYHOST => $this->verifyHostname ? 2 : 0,
+ CURLOPT_SSL_VERIFYPEER => $this->verifyCert ? 1 : 0,
+ CURLOPT_URL => str_replace('%ID%', $roomId, $this->location),
+ CURLOPT_TIMEOUT => 60,
+ CURLOPT_CONNECTTIMEOUT => 4,
+ ];
+ if ($this->authMethod !== 'NONE' && defined('CURLAUTH_' . $this->authMethod)) {
+ $options[CURLOPT_HTTPAUTH] = constant('CURLAUTH_' . $this->authMethod);
+ $options[CURLOPT_USERPWD] = $this->user;
+ if (!empty($this->pass)) {
+ $options[CURLOPT_USERPWD] .= ':' . $this->pass;
+ }
+ }
+
+ curl_setopt_array($this->curlHandle, $options);
+
+ $curlRet = curl_exec($this->curlHandle);
+ if (!$curlRet) {
+ $this->addError('Curl error: ' . curl_error($this->curlHandle) . ' for ' . $roomId, false);
+ }
+ $ical->finish();
+ if (!$ical->isValid()) {
+ if ($curlRet) {
+ $this->addError("Did not find a VCALENDAR in returned data for $roomId", false);
+ }
+ return null;
+ }
+ return $ical->events();
+ }
+
+ public function fetchSchedulesInternal(array $requestedRoomIds): array
+ {
+ if (empty($requestedRoomIds) || !$this->isOK()) {
+ return array();
+ }
+ $tTables = [];
+ foreach ($requestedRoomIds as $roomId) {
+ $data = $this->downloadIcal($roomId);
+ if ($data === null) {
+ $this->addError("Downloading ical for $roomId failed", false);
+ continue;
+ }
+ foreach ($data as $event) {
+ $tTables[$roomId][] = array(
+ 'title' => $this->toTitle($event),
+ 'start' => $event->dtstart,
+ 'end' => $event->dtend,
+ 'cancelled' => false, // ??? How
+ );
+ }
+ }
+ return $tTables;
+ }
+
+ /**
+ * Get a usable title from either SUMMARY or DESCRIPTION
+ */
+ protected function toTitle(ICalEvent $event): string
+ {
+ $title = $event->summary;
+ if (empty($title)) {
+ $title = $event->description;
+ }
+ if (empty($title)) {
+ $title = 'Unknown';
+ }
+ return (string)preg_replace([',(\s*<br\s*/?>\s*|\r|\n|\\\r|\\\n)+,', '/\\\\([,;:])/'], ["\n", '$1'], $title);
+ }
+
+ protected function isOK(): bool
+ {
+ if (empty($this->location)) {
+ $this->addError("Credentials are not set", true);
+ return false;
+ }
+ return true;
+ }
+
+ public function __destruct()
+ {
+ if ($this->curlHandle !== false) {
+ curl_close($this->curlHandle);
+ }
+ }
+
+} \ No newline at end of file
diff --git a/modules-available/locationinfo/inc/icalevent.inc.php b/modules-available/locationinfo/inc/icalevent.inc.php
new file mode 100644
index 00000000..c5aea349
--- /dev/null
+++ b/modules-available/locationinfo/inc/icalevent.inc.php
@@ -0,0 +1,254 @@
+<?php
+
+class ICalEvent
+{
+ // phpcs:disable Generic.Arrays.DisallowLongArraySyntax
+
+ const HTML_TEMPLATE = '<p>%s: %s</p>';
+
+ /**
+ * https://www.kanzaki.com/docs/ical/summary.html
+ *
+ * @var string
+ */
+ public $summary;
+
+ /**
+ * https://www.kanzaki.com/docs/ical/dtstart.html
+ *
+ * @var string
+ */
+ public $dtstart;
+
+ /**
+ * https://www.kanzaki.com/docs/ical/dtend.html
+ *
+ * @var string
+ */
+ public $dtend;
+
+ /**
+ * https://www.kanzaki.com/docs/ical/duration.html
+ *
+ * @var string
+ */
+ public $duration;
+
+ /**
+ * https://www.kanzaki.com/docs/ical/dtstamp.html
+ *
+ * @var string
+ */
+ public $dtstamp;
+
+ /**
+ * When the event starts, represented as a timezone-adjusted string
+ *
+ * @var string
+ */
+ public $dtstart_tz;
+
+ /**
+ * When the event ends, represented as a timezone-adjusted string
+ *
+ * @var string
+ */
+ public $dtend_tz;
+
+ /**
+ * https://www.kanzaki.com/docs/ical/uid.html
+ *
+ * @var string
+ */
+ public $uid;
+
+ /**
+ * https://www.kanzaki.com/docs/ical/created.html
+ *
+ * @var string
+ */
+ public $created;
+
+ /**
+ * https://www.kanzaki.com/docs/ical/lastModified.html
+ *
+ * @var string
+ */
+ public $last_modified;
+
+ /**
+ * https://www.kanzaki.com/docs/ical/description.html
+ *
+ * @var string
+ */
+ public $description;
+
+ /**
+ * https://www.kanzaki.com/docs/ical/location.html
+ *
+ * @var string
+ */
+ public $location;
+
+ /**
+ * https://www.kanzaki.com/docs/ical/sequence.html
+ *
+ * @var string
+ */
+ public $sequence;
+
+ /**
+ * https://www.kanzaki.com/docs/ical/status.html
+ *
+ * @var string
+ */
+ public $status;
+
+ /**
+ * https://www.kanzaki.com/docs/ical/transp.html
+ *
+ * @var string
+ */
+ public $transp;
+
+ /**
+ * https://www.kanzaki.com/docs/ical/organizer.html
+ *
+ * @var string
+ */
+ public $organizer;
+
+ /**
+ * https://www.kanzaki.com/docs/ical/attendee.html
+ *
+ * @var string
+ */
+ public $attendee;
+
+ /**
+ * Manage additional properties
+ *
+ * @var array<string, mixed>
+ */
+ private $additionalProperties = array();
+
+ /**
+ * Creates the Event object
+ *
+ * @param array $data
+ * @return void
+ */
+ public function __construct(array $data = array())
+ {
+ foreach ($data as $key => $value) {
+ $variable = self::snakeCase($key);
+ if (property_exists($this, $variable)) {
+ $this->{$variable} = $this->prepareData($value);
+ } else {
+ $this->additionalProperties[$variable] = $this->prepareData($value);
+ }
+ }
+ }
+
+ /**
+ * Magic getter method
+ *
+ * @param string $additionalPropertyName
+ * @return mixed
+ */
+ public function __get(string $additionalPropertyName)
+ {
+ if (array_key_exists($additionalPropertyName, $this->additionalProperties)) {
+ return $this->additionalProperties[$additionalPropertyName];
+ }
+
+ return null;
+ }
+
+ /**
+ * Magic isset method
+ */
+ public function __isset(string $name): bool
+ {
+ return is_null($this->$name) === false;
+ }
+
+ /**
+ * Prepares the data for output
+ *
+ * @param mixed $value
+ * @return mixed
+ */
+ protected function prepareData($value)
+ {
+ if (is_string($value)) {
+ return stripslashes(trim(str_replace('\n', "\n", $value)));
+ }
+
+ if (is_array($value)) {
+ return array_map(function ($value) {
+ return $this->prepareData($value);
+ }, $value);
+ }
+
+ return $value;
+ }
+
+ /**
+ * Returns Event data excluding anything blank
+ * within an HTML template
+ *
+ * @param string $html HTML template to use
+ * @return string
+ */
+ public function printData($html = self::HTML_TEMPLATE)
+ {
+ $data = array(
+ 'SUMMARY' => $this->summary,
+ 'DTSTART' => $this->dtstart,
+ 'DTEND' => $this->dtend,
+ 'DTSTART_TZ' => $this->dtstart_tz,
+ 'DTEND_TZ' => $this->dtend_tz,
+ 'DURATION' => $this->duration,
+ 'DTSTAMP' => $this->dtstamp,
+ 'UID' => $this->uid,
+ 'CREATED' => $this->created,
+ 'LAST-MODIFIED' => $this->last_modified,
+ 'DESCRIPTION' => $this->description,
+ 'LOCATION' => $this->location,
+ 'SEQUENCE' => $this->sequence,
+ 'STATUS' => $this->status,
+ 'TRANSP' => $this->transp,
+ 'ORGANISER' => $this->organizer,
+ 'ATTENDEE(S)' => $this->attendee,
+ );
+
+ // Remove any blank values
+ $data = array_filter($data);
+
+ $output = '';
+
+ foreach ($data as $key => $value) {
+ $output .= sprintf($html, $key, $value);
+ }
+
+ return $output;
+ }
+
+ /**
+ * Converts the given input to snake_case
+ *
+ * @param string $input
+ * @param string $glue
+ * @param string $separator
+ * @return string
+ */
+ protected static function snakeCase($input, $glue = '_', $separator = '-')
+ {
+ $input = preg_split('/(?<=[a-z])(?=[A-Z])/x', $input);
+ $input = implode($glue, $input);
+ $input = str_replace($separator, $glue, $input);
+
+ return strtolower($input);
+ }
+}
diff --git a/modules-available/locationinfo/inc/icalparser.inc.php b/modules-available/locationinfo/inc/icalparser.inc.php
new file mode 100644
index 00000000..eacb67b1
--- /dev/null
+++ b/modules-available/locationinfo/inc/icalparser.inc.php
@@ -0,0 +1,2052 @@
+<?php
+
+/*
+ * Modified for slx-admin to support streaming, some functions removed that are not needed,
+ * Carbon removed, Honor window size when calculating recurring events, ...
+ */
+
+/**
+ * This PHP class will read an ICS (`.ics`, `.ical`, `.ifb`) file, parse it and return an
+ * array of its contents.
+ *
+ * PHP 5 (≥ 5.3.9)
+ *
+ * @author Jonathan Goode <https://github.com/u01jmg3>
+ * @license https://opensource.org/licenses/mit-license.php MIT License
+ * @version 2.1.20
+ */
+class ICalParser
+{
+ // phpcs:disable Generic.Arrays.DisallowLongArraySyntax
+
+ const DATE_TIME_FORMAT = 'Ymd\THis';
+ const ICAL_DATE_TIME_TEMPLATE = 'TZID=%s:';
+ const ISO_8601_WEEK_START = 'MO';
+ const RECURRENCE_EVENT = 'Generated recurrence event';
+ const TIME_ZONE_UTC = 'UTC';
+ const UNIX_FORMAT = 'U';
+
+ /**
+ * Tracks the number of alarms in the current iCal feed
+ *
+ * @var integer
+ */
+ public $alarmCount = 0;
+
+ /**
+ * Tracks the number of events in the current iCal feed
+ *
+ * @var integer
+ */
+ public $eventCount = 0;
+
+ /**
+ * Tracks the free/busy count in the current iCal feed
+ *
+ * @var integer
+ */
+ public $freeBusyCount = 0;
+
+ /**
+ * Tracks the number of todos in the current iCal feed
+ *
+ * @var integer
+ */
+ public $todoCount = 0;
+
+ /**
+ * The value in years to use for indefinite, recurring events
+ *
+ * @var integer
+ */
+ public $defaultSpan = 2;
+
+ /**
+ * Enables customisation of the default time zone
+ *
+ * @var string
+ */
+ public $defaultTimeZone;
+
+ /**
+ * The two letter representation of the first day of the week
+ *
+ * @var string
+ */
+ public $defaultWeekStart = self::ISO_8601_WEEK_START;
+
+ /**
+ * Toggles whether to skip the parsing of recurrence rules
+ *
+ * @var boolean
+ */
+ public $skipRecurrence = false;
+
+ /**
+ * With this being non-null the parser will ignore all events more than roughly this many days after now.
+ *
+ * @var integer
+ */
+ public $filterDaysBefore;
+
+ /**
+ * With this being non-null the parser will ignore all events more than roughly this many days before now.
+ *
+ * @var integer
+ */
+ public $filterDaysAfter;
+
+ /**
+ * @var string Which object type we're currently handling while parsing.
+ */
+ private $parseStateComponent = '';
+
+ /**
+ * @var string Current line being read (in case of continuation).
+ */
+ private $currentLineBuffer = '';
+
+ /**
+ * @var string Chunk of data currently being handled - might stop mid-line.
+ */
+ private $feedBuffer = '';
+
+ /**
+ * @var bool whether we ever saw a BEGIN:VCALENDAR in the data
+ */
+ private $hasSeenStart = false;
+
+ /**
+ * The parsed calendar
+ *
+ * @var array
+ */
+ public $cal = array();
+
+ /**
+ * Tracks the VFREEBUSY component
+ *
+ * @var integer
+ */
+ protected $freeBusyIndex = 0;
+
+ /**
+ * Variable to track the previous keyword
+ *
+ * @var string
+ */
+ protected $lastKeyword;
+
+ /**
+ * Cache valid IANA time zone IDs to avoid unnecessary lookups
+ *
+ * @var array
+ */
+ protected $validIanaTimeZones = array();
+
+ /**
+ * Event recurrence instances that have been altered
+ *
+ * @var array
+ */
+ protected $alteredRecurrenceInstances = array();
+
+ /**
+ * An associative array containing weekday conversion data
+ *
+ * The order of the days in the array follow the ISO-8601 specification of a week.
+ *
+ * @var array
+ */
+ protected $weekdays = array(
+ 'MO' => 'monday',
+ 'TU' => 'tuesday',
+ 'WE' => 'wednesday',
+ 'TH' => 'thursday',
+ 'FR' => 'friday',
+ 'SA' => 'saturday',
+ 'SU' => 'sunday',
+ );
+
+ /**
+ * An associative array containing frequency conversion terms
+ *
+ * @var array
+ */
+ protected $frequencyConversion = array(
+ 'DAILY' => 'day',
+ 'WEEKLY' => 'week',
+ 'MONTHLY' => 'month',
+ 'YEARLY' => 'year',
+ );
+
+ /**
+ * Define which variables can be configured
+ *
+ * @var array
+ */
+ private static $configurableOptions = array(
+ 'defaultSpan',
+ 'defaultTimeZone',
+ 'defaultWeekStart',
+ 'filterDaysAfter',
+ 'filterDaysBefore',
+ 'skipRecurrence',
+ );
+
+ /**
+ * CLDR time zones mapped to IANA time zones.
+ *
+ * @var array
+ */
+ private static $cldrTimeZonesMap = array(
+ '(UTC-12:00) International Date Line West' => 'Etc/GMT+12',
+ '(UTC-11:00) Coordinated Universal Time-11' => 'Etc/GMT+11',
+ '(UTC-10:00) Hawaii' => 'Pacific/Honolulu',
+ '(UTC-09:00) Alaska' => 'America/Anchorage',
+ '(UTC-08:00) Pacific Time (US & Canada)' => 'America/Los_Angeles',
+ '(UTC-07:00) Arizona' => 'America/Phoenix',
+ '(UTC-07:00) Chihuahua, La Paz, Mazatlan' => 'America/Chihuahua',
+ '(UTC-07:00) Mountain Time (US & Canada)' => 'America/Denver',
+ '(UTC-06:00) Central America' => 'America/Guatemala',
+ '(UTC-06:00) Central Time (US & Canada)' => 'America/Chicago',
+ '(UTC-06:00) Guadalajara, Mexico City, Monterrey' => 'America/Mexico_City',
+ '(UTC-06:00) Saskatchewan' => 'America/Regina',
+ '(UTC-05:00) Bogota, Lima, Quito, Rio Branco' => 'America/Bogota',
+ '(UTC-05:00) Chetumal' => 'America/Cancun',
+ '(UTC-05:00) Eastern Time (US & Canada)' => 'America/New_York',
+ '(UTC-05:00) Indiana (East)' => 'America/Indianapolis',
+ '(UTC-04:00) Asuncion' => 'America/Asuncion',
+ '(UTC-04:00) Atlantic Time (Canada)' => 'America/Halifax',
+ '(UTC-04:00) Caracas' => 'America/Caracas',
+ '(UTC-04:00) Cuiaba' => 'America/Cuiaba',
+ '(UTC-04:00) Georgetown, La Paz, Manaus, San Juan' => 'America/La_Paz',
+ '(UTC-04:00) Santiago' => 'America/Santiago',
+ '(UTC-03:30) Newfoundland' => 'America/St_Johns',
+ '(UTC-03:00) Brasilia' => 'America/Sao_Paulo',
+ '(UTC-03:00) Cayenne, Fortaleza' => 'America/Cayenne',
+ '(UTC-03:00) City of Buenos Aires' => 'America/Buenos_Aires',
+ '(UTC-03:00) Greenland' => 'America/Godthab',
+ '(UTC-03:00) Montevideo' => 'America/Montevideo',
+ '(UTC-03:00) Salvador' => 'America/Bahia',
+ '(UTC-02:00) Coordinated Universal Time-02' => 'Etc/GMT+2',
+ '(UTC-01:00) Azores' => 'Atlantic/Azores',
+ '(UTC-01:00) Cabo Verde Is.' => 'Atlantic/Cape_Verde',
+ '(UTC) Coordinated Universal Time' => 'Etc/GMT',
+ '(UTC+00:00) Casablanca' => 'Africa/Casablanca',
+ '(UTC+00:00) Dublin, Edinburgh, Lisbon, London' => 'Europe/London',
+ '(UTC+00:00) Monrovia, Reykjavik' => 'Atlantic/Reykjavik',
+ '(UTC+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna' => 'Europe/Berlin',
+ '(UTC+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague' => 'Europe/Budapest',
+ '(UTC+01:00) Brussels, Copenhagen, Madrid, Paris' => 'Europe/Paris',
+ '(UTC+01:00) Sarajevo, Skopje, Warsaw, Zagreb' => 'Europe/Warsaw',
+ '(UTC+01:00) West Central Africa' => 'Africa/Lagos',
+ '(UTC+02:00) Amman' => 'Asia/Amman',
+ '(UTC+02:00) Athens, Bucharest' => 'Europe/Bucharest',
+ '(UTC+02:00) Beirut' => 'Asia/Beirut',
+ '(UTC+02:00) Cairo' => 'Africa/Cairo',
+ '(UTC+02:00) Chisinau' => 'Europe/Chisinau',
+ '(UTC+02:00) Damascus' => 'Asia/Damascus',
+ '(UTC+02:00) Harare, Pretoria' => 'Africa/Johannesburg',
+ '(UTC+02:00) Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius' => 'Europe/Kiev',
+ '(UTC+02:00) Jerusalem' => 'Asia/Jerusalem',
+ '(UTC+02:00) Kaliningrad' => 'Europe/Kaliningrad',
+ '(UTC+02:00) Tripoli' => 'Africa/Tripoli',
+ '(UTC+02:00) Windhoek' => 'Africa/Windhoek',
+ '(UTC+03:00) Baghdad' => 'Asia/Baghdad',
+ '(UTC+03:00) Istanbul' => 'Europe/Istanbul',
+ '(UTC+03:00) Kuwait, Riyadh' => 'Asia/Riyadh',
+ '(UTC+03:00) Minsk' => 'Europe/Minsk',
+ '(UTC+03:00) Moscow, St. Petersburg, Volgograd' => 'Europe/Moscow',
+ '(UTC+03:00) Nairobi' => 'Africa/Nairobi',
+ '(UTC+03:30) Tehran' => 'Asia/Tehran',
+ '(UTC+04:00) Abu Dhabi, Muscat' => 'Asia/Dubai',
+ '(UTC+04:00) Baku' => 'Asia/Baku',
+ '(UTC+04:00) Izhevsk, Samara' => 'Europe/Samara',
+ '(UTC+04:00) Port Louis' => 'Indian/Mauritius',
+ '(UTC+04:00) Tbilisi' => 'Asia/Tbilisi',
+ '(UTC+04:00) Yerevan' => 'Asia/Yerevan',
+ '(UTC+04:30) Kabul' => 'Asia/Kabul',
+ '(UTC+05:00) Ashgabat, Tashkent' => 'Asia/Tashkent',
+ '(UTC+05:00) Ekaterinburg' => 'Asia/Yekaterinburg',
+ '(UTC+05:00) Islamabad, Karachi' => 'Asia/Karachi',
+ '(UTC+05:30) Chennai, Kolkata, Mumbai, New Delhi' => 'Asia/Calcutta',
+ '(UTC+05:30) Sri Jayawardenepura' => 'Asia/Colombo',
+ '(UTC+05:45) Kathmandu' => 'Asia/Katmandu',
+ '(UTC+06:00) Astana' => 'Asia/Almaty',
+ '(UTC+06:00) Dhaka' => 'Asia/Dhaka',
+ '(UTC+06:30) Yangon (Rangoon)' => 'Asia/Rangoon',
+ '(UTC+07:00) Bangkok, Hanoi, Jakarta' => 'Asia/Bangkok',
+ '(UTC+07:00) Krasnoyarsk' => 'Asia/Krasnoyarsk',
+ '(UTC+07:00) Novosibirsk' => 'Asia/Novosibirsk',
+ '(UTC+08:00) Beijing, Chongqing, Hong Kong, Urumqi' => 'Asia/Shanghai',
+ '(UTC+08:00) Irkutsk' => 'Asia/Irkutsk',
+ '(UTC+08:00) Kuala Lumpur, Singapore' => 'Asia/Singapore',
+ '(UTC+08:00) Perth' => 'Australia/Perth',
+ '(UTC+08:00) Taipei' => 'Asia/Taipei',
+ '(UTC+08:00) Ulaanbaatar' => 'Asia/Ulaanbaatar',
+ '(UTC+09:00) Osaka, Sapporo, Tokyo' => 'Asia/Tokyo',
+ '(UTC+09:00) Pyongyang' => 'Asia/Pyongyang',
+ '(UTC+09:00) Seoul' => 'Asia/Seoul',
+ '(UTC+09:00) Yakutsk' => 'Asia/Yakutsk',
+ '(UTC+09:30) Adelaide' => 'Australia/Adelaide',
+ '(UTC+09:30) Darwin' => 'Australia/Darwin',
+ '(UTC+10:00) Brisbane' => 'Australia/Brisbane',
+ '(UTC+10:00) Canberra, Melbourne, Sydney' => 'Australia/Sydney',
+ '(UTC+10:00) Guam, Port Moresby' => 'Pacific/Port_Moresby',
+ '(UTC+10:00) Hobart' => 'Australia/Hobart',
+ '(UTC+10:00) Vladivostok' => 'Asia/Vladivostok',
+ '(UTC+11:00) Chokurdakh' => 'Asia/Srednekolymsk',
+ '(UTC+11:00) Magadan' => 'Asia/Magadan',
+ '(UTC+11:00) Solomon Is., New Caledonia' => 'Pacific/Guadalcanal',
+ '(UTC+12:00) Anadyr, Petropavlovsk-Kamchatsky' => 'Asia/Kamchatka',
+ '(UTC+12:00) Auckland, Wellington' => 'Pacific/Auckland',
+ '(UTC+12:00) Coordinated Universal Time+12' => 'Etc/GMT-12',
+ '(UTC+12:00) Fiji' => 'Pacific/Fiji',
+ "(UTC+13:00) Nuku'alofa" => 'Pacific/Tongatapu',
+ '(UTC+13:00) Samoa' => 'Pacific/Apia',
+ '(UTC+14:00) Kiritimati Island' => 'Pacific/Kiritimati',
+ );
+
+ /**
+ * Maps Windows (non-CLDR) time zone ID to IANA ID. This is pragmatic but not 100% precise as one Windows zone ID
+ * maps to multiple IANA IDs (one for each territory). For all practical purposes this should be good enough, though.
+ *
+ * Source: http://unicode.org/repos/cldr/trunk/common/supplemental/windowsZones.xml
+ *
+ * @var array
+ */
+ private static $windowsTimeZonesMap = array(
+ 'AUS Central Standard Time' => 'Australia/Darwin',
+ 'AUS Eastern Standard Time' => 'Australia/Sydney',
+ 'Afghanistan Standard Time' => 'Asia/Kabul',
+ 'Alaskan Standard Time' => 'America/Anchorage',
+ 'Aleutian Standard Time' => 'America/Adak',
+ 'Altai Standard Time' => 'Asia/Barnaul',
+ 'Arab Standard Time' => 'Asia/Riyadh',
+ 'Arabian Standard Time' => 'Asia/Dubai',
+ 'Arabic Standard Time' => 'Asia/Baghdad',
+ 'Argentina Standard Time' => 'America/Buenos_Aires',
+ 'Astrakhan Standard Time' => 'Europe/Astrakhan',
+ 'Atlantic Standard Time' => 'America/Halifax',
+ 'Aus Central W. Standard Time' => 'Australia/Eucla',
+ 'Azerbaijan Standard Time' => 'Asia/Baku',
+ 'Azores Standard Time' => 'Atlantic/Azores',
+ 'Bahia Standard Time' => 'America/Bahia',
+ 'Bangladesh Standard Time' => 'Asia/Dhaka',
+ 'Belarus Standard Time' => 'Europe/Minsk',
+ 'Bougainville Standard Time' => 'Pacific/Bougainville',
+ 'Canada Central Standard Time' => 'America/Regina',
+ 'Cape Verde Standard Time' => 'Atlantic/Cape_Verde',
+ 'Caucasus Standard Time' => 'Asia/Yerevan',
+ 'Cen. Australia Standard Time' => 'Australia/Adelaide',
+ 'Central America Standard Time' => 'America/Guatemala',
+ 'Central Asia Standard Time' => 'Asia/Almaty',
+ 'Central Brazilian Standard Time' => 'America/Cuiaba',
+ 'Central Europe Standard Time' => 'Europe/Budapest',
+ 'Central European Standard Time' => 'Europe/Warsaw',
+ 'Central Pacific Standard Time' => 'Pacific/Guadalcanal',
+ 'Central Standard Time (Mexico)' => 'America/Mexico_City',
+ 'Central Standard Time' => 'America/Chicago',
+ 'Chatham Islands Standard Time' => 'Pacific/Chatham',
+ 'China Standard Time' => 'Asia/Shanghai',
+ 'Cuba Standard Time' => 'America/Havana',
+ 'Dateline Standard Time' => 'Etc/GMT+12',
+ 'E. Africa Standard Time' => 'Africa/Nairobi',
+ 'E. Australia Standard Time' => 'Australia/Brisbane',
+ 'E. Europe Standard Time' => 'Europe/Chisinau',
+ 'E. South America Standard Time' => 'America/Sao_Paulo',
+ 'Easter Island Standard Time' => 'Pacific/Easter',
+ 'Eastern Standard Time (Mexico)' => 'America/Cancun',
+ 'Eastern Standard Time' => 'America/New_York',
+ 'Egypt Standard Time' => 'Africa/Cairo',
+ 'Ekaterinburg Standard Time' => 'Asia/Yekaterinburg',
+ 'FLE Standard Time' => 'Europe/Kiev',
+ 'Fiji Standard Time' => 'Pacific/Fiji',
+ 'GMT Standard Time' => 'Europe/London',
+ 'GTB Standard Time' => 'Europe/Bucharest',
+ 'Georgian Standard Time' => 'Asia/Tbilisi',
+ 'Greenland Standard Time' => 'America/Godthab',
+ 'Greenwich Standard Time' => 'Atlantic/Reykjavik',
+ 'Haiti Standard Time' => 'America/Port-au-Prince',
+ 'Hawaiian Standard Time' => 'Pacific/Honolulu',
+ 'India Standard Time' => 'Asia/Calcutta',
+ 'Iran Standard Time' => 'Asia/Tehran',
+ 'Israel Standard Time' => 'Asia/Jerusalem',
+ 'Jordan Standard Time' => 'Asia/Amman',
+ 'Kaliningrad Standard Time' => 'Europe/Kaliningrad',
+ 'Korea Standard Time' => 'Asia/Seoul',
+ 'Libya Standard Time' => 'Africa/Tripoli',
+ 'Line Islands Standard Time' => 'Pacific/Kiritimati',
+ 'Lord Howe Standard Time' => 'Australia/Lord_Howe',
+ 'Magadan Standard Time' => 'Asia/Magadan',
+ 'Magallanes Standard Time' => 'America/Punta_Arenas',
+ 'Marquesas Standard Time' => 'Pacific/Marquesas',
+ 'Mauritius Standard Time' => 'Indian/Mauritius',
+ 'Middle East Standard Time' => 'Asia/Beirut',
+ 'Montevideo Standard Time' => 'America/Montevideo',
+ 'Morocco Standard Time' => 'Africa/Casablanca',
+ 'Mountain Standard Time (Mexico)' => 'America/Chihuahua',
+ 'Mountain Standard Time' => 'America/Denver',
+ 'Myanmar Standard Time' => 'Asia/Rangoon',
+ 'N. Central Asia Standard Time' => 'Asia/Novosibirsk',
+ 'Namibia Standard Time' => 'Africa/Windhoek',
+ 'Nepal Standard Time' => 'Asia/Katmandu',
+ 'New Zealand Standard Time' => 'Pacific/Auckland',
+ 'Newfoundland Standard Time' => 'America/St_Johns',
+ 'Norfolk Standard Time' => 'Pacific/Norfolk',
+ 'North Asia East Standard Time' => 'Asia/Irkutsk',
+ 'North Asia Standard Time' => 'Asia/Krasnoyarsk',
+ 'North Korea Standard Time' => 'Asia/Pyongyang',
+ 'Omsk Standard Time' => 'Asia/Omsk',
+ 'Pacific SA Standard Time' => 'America/Santiago',
+ 'Pacific Standard Time (Mexico)' => 'America/Tijuana',
+ 'Pacific Standard Time' => 'America/Los_Angeles',
+ 'Pakistan Standard Time' => 'Asia/Karachi',
+ 'Paraguay Standard Time' => 'America/Asuncion',
+ 'Romance Standard Time' => 'Europe/Paris',
+ 'Russia Time Zone 10' => 'Asia/Srednekolymsk',
+ 'Russia Time Zone 11' => 'Asia/Kamchatka',
+ 'Russia Time Zone 3' => 'Europe/Samara',
+ 'Russian Standard Time' => 'Europe/Moscow',
+ 'SA Eastern Standard Time' => 'America/Cayenne',
+ 'SA Pacific Standard Time' => 'America/Bogota',
+ 'SA Western Standard Time' => 'America/La_Paz',
+ 'SE Asia Standard Time' => 'Asia/Bangkok',
+ 'Saint Pierre Standard Time' => 'America/Miquelon',
+ 'Sakhalin Standard Time' => 'Asia/Sakhalin',
+ 'Samoa Standard Time' => 'Pacific/Apia',
+ 'Sao Tome Standard Time' => 'Africa/Sao_Tome',
+ 'Saratov Standard Time' => 'Europe/Saratov',
+ 'Singapore Standard Time' => 'Asia/Singapore',
+ 'South Africa Standard Time' => 'Africa/Johannesburg',
+ 'Sri Lanka Standard Time' => 'Asia/Colombo',
+ 'Sudan Standard Time' => 'Africa/Tripoli',
+ 'Syria Standard Time' => 'Asia/Damascus',
+ 'Taipei Standard Time' => 'Asia/Taipei',
+ 'Tasmania Standard Time' => 'Australia/Hobart',
+ 'Tocantins Standard Time' => 'America/Araguaina',
+ 'Tokyo Standard Time' => 'Asia/Tokyo',
+ 'Tomsk Standard Time' => 'Asia/Tomsk',
+ 'Tonga Standard Time' => 'Pacific/Tongatapu',
+ 'Transbaikal Standard Time' => 'Asia/Chita',
+ 'Turkey Standard Time' => 'Europe/Istanbul',
+ 'Turks And Caicos Standard Time' => 'America/Grand_Turk',
+ 'US Eastern Standard Time' => 'America/Indianapolis',
+ 'US Mountain Standard Time' => 'America/Phoenix',
+ 'UTC' => 'Etc/GMT',
+ 'UTC+12' => 'Etc/GMT-12',
+ 'UTC+13' => 'Etc/GMT-13',
+ 'UTC-02' => 'Etc/GMT+2',
+ 'UTC-08' => 'Etc/GMT+8',
+ 'UTC-09' => 'Etc/GMT+9',
+ 'UTC-11' => 'Etc/GMT+11',
+ 'Ulaanbaatar Standard Time' => 'Asia/Ulaanbaatar',
+ 'Venezuela Standard Time' => 'America/Caracas',
+ 'Vladivostok Standard Time' => 'Asia/Vladivostok',
+ 'W. Australia Standard Time' => 'Australia/Perth',
+ 'W. Central Africa Standard Time' => 'Africa/Lagos',
+ 'W. Europe Standard Time' => 'Europe/Berlin',
+ 'W. Mongolia Standard Time' => 'Asia/Hovd',
+ 'West Asia Standard Time' => 'Asia/Tashkent',
+ 'West Bank Standard Time' => 'Asia/Hebron',
+ 'West Pacific Standard Time' => 'Pacific/Port_Moresby',
+ 'Yakutsk Standard Time' => 'Asia/Yakutsk',
+ );
+
+ /**
+ * If `$filterDaysBefore` or `$filterDaysAfter` are set then the events are filtered according to the window defined
+ * by this field and `$windowMaxTimestamp`.
+ *
+ * @var integer
+ */
+ private $windowMinTimestamp;
+
+ /**
+ * If `$filterDaysBefore` or `$filterDaysAfter` are set then the events are filtered according to the window defined
+ * by this field and `$windowMinTimestamp`.
+ *
+ * @var integer
+ */
+ private $windowMaxTimestamp;
+
+ /**
+ * `true` if either `$filterDaysBefore` or `$filterDaysAfter` are set.
+ *
+ * @var boolean
+ */
+ private $shouldFilterByWindow;
+
+ /**
+ * Creates the ICal object
+ *
+ * @param array $options
+ * @return void
+ * @throws Exception
+ */
+ public function __construct(array $options = array())
+ {
+ foreach ($options as $option => $value) {
+ if (in_array($option, self::$configurableOptions)) {
+ $this->{$option} = $value;
+ }
+ }
+
+ // Fallback to use the system default time zone
+ if (!isset($this->defaultTimeZone) || !$this->isValidTimeZoneId($this->defaultTimeZone)) {
+ $this->defaultTimeZone = date_default_timezone_get();
+ }
+
+ $this->windowMinTimestamp = is_null($this->filterDaysBefore) ? PHP_INT_MIN : (new DateTime('now'))->sub(new DateInterval('P' . $this->filterDaysBefore . 'D'))->getTimestamp();
+ $this->windowMaxTimestamp = is_null($this->filterDaysAfter) ? PHP_INT_MAX : (new DateTime('now'))->add(new DateInterval('P' . $this->filterDaysAfter . 'D'))->getTimestamp();
+
+ $this->shouldFilterByWindow = !is_null($this->filterDaysBefore) || !is_null($this->filterDaysAfter);
+ }
+
+ /**
+ * Feed more data to the parser. This can be a chunk of arbitrary length, it
+ * is not required to end on a line break.
+ *
+ * @param string $data
+ */
+ public function feedData(string $data)
+ {
+ $this->feedBuffer .= $data;
+ $start = 0;
+ $bufferLen = strlen($this->feedBuffer);
+ while (($newLine = strcspn($this->feedBuffer, "\r\n", $start) + $start) !== $bufferLen) {
+ $length = $newLine - $start;
+ if ($length > 1) {
+ if ($this->feedBuffer[$start] === ' ' || $this->feedBuffer[$start] === '\t') {
+ // Continuation of previous line
+ $this->currentLineBuffer .= substr($this->feedBuffer, $start + 1, $length - 1);
+ } else {
+ // New line, flush previous one
+ $this->handleLine($this->currentLineBuffer);
+ $this->currentLineBuffer = substr($this->feedBuffer, $start, $length);
+ }
+ }
+ $start = $newLine + 1;
+ }
+ $this->feedBuffer = substr($this->feedBuffer, $start);
+ }
+
+ /**
+ * Finish feeding more data to the parser, process the data.
+ */
+ public function finish()
+ {
+ // Flush
+ $this->feedData("\n*\n");
+ $this->currentLineBuffer = '';
+ $this->feedBuffer = '';
+ $this->processEvents();
+
+ if (!$this->skipRecurrence) {
+ $this->processRecurrences();
+
+ // Apply changes to altered recurrence instances
+ if (!empty($this->alteredRecurrenceInstances)) {
+ $events = $this->cal['VEVENT'];
+
+ foreach ($this->alteredRecurrenceInstances as $alteredRecurrenceInstance) {
+ if (isset($alteredRecurrenceInstance['altered-event'])) {
+ $alteredEvent = $alteredRecurrenceInstance['altered-event'];
+ $key = key($alteredEvent);
+ $events[$key] = $alteredEvent[$key];
+ }
+ }
+
+ $this->cal['VEVENT'] = $events;
+ }
+ }
+
+ if ($this->shouldFilterByWindow) {
+ $this->reduceEventsToMinMaxRange();
+ }
+
+ $this->processDateConversions();
+ }
+
+ /**
+ * True if this resembles a calendar, i.e. we've seen the
+ * BEGIN:VCALENDAR line at some point.
+ *
+ * @return bool
+ */
+ public function isValid(): bool
+ {
+ return $this->hasSeenStart;
+ }
+
+ /**
+ * Process next completed line from file
+ *
+ * @param string $line
+ */
+ protected function handleLine(string $line)
+ {
+ $line = rtrim($line); // Trim trailing whitespace
+ $line = $this->removeUnprintableChars($line);
+
+ if (empty($line)) {
+ return;
+ }
+
+ $add = $this->keyValueFromString($line);
+
+ if ($add === null) {
+ return;
+ }
+
+ $keyword = $add[0]; // string
+ $values = $add[1]; // May be an array containing multiple values
+
+ if (!is_array($values)) {
+ if (!empty($values)) {
+ $values = array($values); // Make an array as not already
+ $blankArray = array(); // Empty placeholder array
+ $values[] = $blankArray;
+ } else {
+ $values = array(); // Use blank array to ignore this line
+ }
+ } elseif (empty($values[0])) {
+ $values = array(); // Use blank array to ignore this line
+ }
+
+ // Reverse so that our array of properties is processed first
+ $values = array_reverse($values);
+
+ foreach ($values as $value) {
+ switch ($line) {
+ // https://www.kanzaki.com/docs/ical/vtodo.html
+ case 'BEGIN:VTODO':
+ if (!is_array($value)) {
+ $this->todoCount++;
+ }
+
+ $this->parseStateComponent = 'VTODO';
+
+ break;
+
+ // https://www.kanzaki.com/docs/ical/vevent.html
+ case 'BEGIN:VEVENT':
+ if (!is_array($value)) {
+ $this->eventCount++;
+ }
+
+ $this->parseStateComponent = 'VEVENT';
+
+ break;
+
+ // https://www.kanzaki.com/docs/ical/vfreebusy.html
+ case 'BEGIN:VFREEBUSY':
+ if (!is_array($value)) {
+ $this->freeBusyIndex++;
+ }
+
+ $this->parseStateComponent = 'VFREEBUSY';
+
+ break;
+
+ case 'BEGIN:VALARM':
+ if (!is_array($value)) {
+ $this->alarmCount++;
+ }
+
+ $this->parseStateComponent = 'VALARM';
+
+ break;
+
+ case 'END:VALARM':
+ $this->parseStateComponent = 'VEVENT';
+
+ break;
+
+ case 'BEGIN:DAYLIGHT':
+ case 'BEGIN:STANDARD':
+ case 'BEGIN:VTIMEZONE':
+ $this->parseStateComponent = $value;
+
+ break;
+
+ case 'END:DAYLIGHT':
+ case 'END:STANDARD':
+ case 'END:VFREEBUSY':
+ case 'END:VTIMEZONE':
+ case 'END:VTODO':
+ $this->parseStateComponent = 'VCALENDAR';
+
+ break;
+
+ case 'BEGIN:VCALENDAR':
+ $this->hasSeenStart = true;
+ $this->parseStateComponent = $value;
+
+ break;
+
+ case 'END:VCALENDAR':
+ $this->parseStateComponent = '';
+
+ break;
+
+ case 'END:VEVENT':
+ if ($this->shouldFilterByWindow) {
+ $this->removeLastEventIfOutsideWindowAndNonRecurring();
+ }
+
+ $this->parseStateComponent = 'VCALENDAR';
+
+ break;
+
+ default:
+ if (!empty($this->parseStateComponent)) {
+ $this->addCalendarComponentWithKeyAndValue($this->parseStateComponent, $keyword, $value);
+ }
+ break;
+ }
+ }
+ }
+
+ /**
+ * Removes the last event (i.e. most recently parsed) if its start date is outside the window spanned by
+ * `$windowMinTimestamp` / `$windowMaxTimestamp`.
+ *
+ * @return void
+ */
+ protected function removeLastEventIfOutsideWindowAndNonRecurring()
+ {
+ $events = $this->cal['VEVENT'];
+
+ if (!empty($events)) {
+ $lastIndex = count($events) - 1;
+ $lastEvent = $events[$lastIndex];
+
+ if (empty($lastEvent['RRULE']) && $this->doesEventStartOutsideWindow($lastEvent)) {
+ $this->eventCount--;
+
+ unset($events[$lastIndex]);
+ }
+
+ $this->cal['VEVENT'] = $events;
+ }
+ }
+
+ /**
+ * Reduces the number of events to the defined minimum and maximum range
+ *
+ * @return void
+ */
+ protected function reduceEventsToMinMaxRange()
+ {
+ $events = (isset($this->cal['VEVENT'])) ? $this->cal['VEVENT'] : array();
+
+ if (!empty($events)) {
+ foreach ($events as $key => $anEvent) {
+ if ($anEvent === null) {
+ unset($events[$key]);
+ } elseif ($this->doesEventStartOutsideWindow($anEvent)) {
+ $this->eventCount--;
+ unset($events[$key]);
+ }
+ }
+
+ $this->cal['VEVENT'] = $events;
+ }
+ }
+
+ /**
+ * Determines whether the event start date is outside `$windowMinTimestamp` / `$windowMaxTimestamp`.
+ * Returns `true` for invalid dates.
+ *
+ * @param array $event
+ * @return boolean
+ */
+ protected function doesEventStartOutsideWindow(array $event): bool
+ {
+ return !isset($event['DTSTART']) || !$this->isValidDate($event['DTSTART'])
+ || $this->isOutOfRange($event['DTSTART'], $this->windowMinTimestamp, $this->windowMaxTimestamp);
+ }
+
+ /**
+ * Determines whether a valid iCalendar date is within a given range
+ *
+ * @param string $calendarDate
+ * @param integer $minTimestamp
+ * @param integer $maxTimestamp
+ * @return boolean
+ */
+ protected function isOutOfRange(string $calendarDate, int $minTimestamp, int $maxTimestamp): bool
+ {
+ $timestamp = strtotime(explode('T', $calendarDate)[0]);
+
+ return $timestamp < $minTimestamp || $timestamp > $maxTimestamp;
+ }
+
+ /**
+ * Add one key and value pair to the `$this->cal` array
+ *
+ * @param string $component
+ * @param string $keyword
+ * @param string|string[] $value
+ * @return void
+ */
+ protected function addCalendarComponentWithKeyAndValue(string $component, string $keyword, $value)
+ {
+ switch ($component) {
+ case 'VALARM':
+ $key1 = 'VEVENT';
+ $key2 = ($this->eventCount - 1);
+ $key3 = $component;
+
+ if (!isset($this->cal[$key1][$key2][$key3]["{$keyword}_array"])) {
+ $this->cal[$key1][$key2][$key3]["{$keyword}_array"] = array();
+ }
+
+ if (is_array($value)) {
+ // Add array of properties to the end
+ $this->cal[$key1][$key2][$key3]["{$keyword}_array"][] = $value;
+ } else {
+ if (!isset($this->cal[$key1][$key2][$key3][$keyword])) {
+ $this->cal[$key1][$key2][$key3][$keyword] = $value;
+ }
+
+ if ($this->cal[$key1][$key2][$key3][$keyword] !== $value) {
+ $this->cal[$key1][$key2][$key3][$keyword] .= ',' . $value;
+ }
+ }
+ break;
+
+ case 'VEVENT':
+ $key1 = $component;
+ $key2 = ($this->eventCount - 1);
+
+ if (!isset($this->cal[$key1][$key2]["{$keyword}_array"])) {
+ $this->cal[$key1][$key2]["{$keyword}_array"] = array();
+ }
+
+ if (is_array($value)) {
+ // Add array of properties to the end
+ $this->cal[$key1][$key2]["{$keyword}_array"][] = $value;
+ } else {
+ if (!isset($this->cal[$key1][$key2][$keyword])) {
+ $this->cal[$key1][$key2][$keyword] = $value;
+ }
+
+ if ($keyword === 'EXDATE') {
+ if (trim($value) === $value) {
+ $array = array_filter(explode(',', $value));
+ $this->cal[$key1][$key2]["{$keyword}_array"][] = $array;
+ } else {
+ $value = explode(',', implode(',', $this->cal[$key1][$key2]["{$keyword}_array"][1]) . trim($value));
+ $this->cal[$key1][$key2]["{$keyword}_array"][1] = $value;
+ }
+ } else {
+ $this->cal[$key1][$key2]["{$keyword}_array"][] = $value;
+
+ if ($keyword === 'DURATION') {
+ try {
+ $duration = new DateInterval($value);
+ $this->cal[$key1][$key2]["{$keyword}_array"][] = $duration;
+ } catch (Exception $e) {
+ error_log('Ignoring invalid duration ' . $value);
+ }
+ }
+ }
+
+ if ($this->cal[$key1][$key2][$keyword] !== $value) {
+ $this->cal[$key1][$key2][$keyword] .= ',' . $value;
+ }
+ }
+ break;
+
+ case 'VFREEBUSY':
+ $key1 = $component;
+ $key2 = ($this->freeBusyIndex - 1);
+ $key3 = $keyword;
+
+ if ($keyword === 'FREEBUSY') {
+ if (is_array($value)) {
+ $this->cal[$key1][$key2][$key3][][] = $value;
+ } else {
+ $this->freeBusyCount++;
+
+ end($this->cal[$key1][$key2][$key3]);
+ $key = key($this->cal[$key1][$key2][$key3]);
+
+ $value = explode('/', $value);
+ $this->cal[$key1][$key2][$key3][$key][] = $value;
+ }
+ } else {
+ $this->cal[$key1][$key2][$key3][] = $value;
+ }
+ break;
+
+ case 'VTODO':
+ $this->cal[$component][$this->todoCount - 1][$keyword] = $value;
+
+ break;
+
+ default:
+ $this->cal[$component][$keyword] = $value;
+
+ break;
+ }
+
+ // Remove?
+ $this->lastKeyword = $keyword;
+ }
+
+ /**
+ * Gets the key value pair from an iCal string
+ *
+ * @param string $text
+ * @return ?array
+ */
+ protected function keyValueFromString(string $text): ?array
+ {
+ $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
+
+ $colon = strpos($text, ':');
+ $quote = strpos($text, '"');
+ if ($colon === false) {
+ $matches = array();
+ } elseif ($quote === false || $colon < $quote) {
+ list($before, $after) = explode(':', $text, 2);
+ $matches = array($text, $before, $after);
+ } else {
+ list($before, $text) = explode('"', $text, 2);
+ $text = '"' . $text;
+ $matches = str_getcsv($text, ':');
+ $combinedValue = '';
+
+ foreach (array_keys($matches) as $key) {
+ if ($key === 0) {
+ if (!empty($before)) {
+ $matches[$key] = $before . '"' . $matches[$key] . '"';
+ }
+ } else {
+ if ($key > 1) {
+ $combinedValue .= ':';
+ }
+
+ $combinedValue .= $matches[$key];
+ }
+ }
+
+ $matches = array_slice($matches, 0, 2);
+ $matches[1] = $combinedValue;
+ array_unshift($matches, $before . $text);
+ }
+
+ if (count($matches) === 0) {
+ return null;
+ }
+
+ if (preg_match('/^([A-Z-]+)(;[\w\W]*)?$/', $matches[1])) {
+ $matches = array_splice($matches, 1, 2); // Remove first match and re-align ordering
+
+ // Process properties
+ if (preg_match('/([A-Z-]+);([\w\W]*)/', $matches[0], $properties)) {
+ // Remove first match
+ array_shift($properties);
+ // Fix to ignore everything in keyword after a ; (e.g. Language, TZID, etc.)
+ $matches[0] = $properties[0];
+ array_shift($properties); // Repeat removing first match
+
+ $formatted = array();
+ foreach ($properties as $property) {
+ // Match semicolon separator outside of quoted substrings
+ preg_match_all('~[^' . PHP_EOL . '";]+(?:"[^"\\\]*(?:\\\.[^"\\\]*)*"[^' . PHP_EOL . '";]*)*~', $property, $attributes);
+ // Remove multi-dimensional array and use the first key
+ $attributes = (count($attributes) === 0) ? array($property) : reset($attributes);
+
+ if (is_array($attributes)) {
+ foreach ($attributes as $attribute) {
+ // Match equals sign separator outside of quoted substrings
+ preg_match_all(
+ '~[^' . PHP_EOL . '"=]+(?:"[^"\\\]*(?:\\\.[^"\\\]*)*"[^' . PHP_EOL . '"=]*)*~',
+ $attribute,
+ $values
+ );
+ // Remove multi-dimensional array and use the first key
+ $value = (count($values) === 0) ? null : reset($values);
+
+ if (is_array($value) && isset($value[1])) {
+ // Remove double quotes from beginning and end only
+ $formatted[$value[0]] = trim($value[1], '"');
+ }
+ }
+ }
+ }
+
+ // Assign the keyword property information
+ $properties[0] = $formatted;
+
+ // Add match to beginning of array
+ array_unshift($properties, $matches[1]);
+ $matches[1] = $properties;
+ }
+
+ return $matches;
+ }
+ return null; // Ignore this match
+ }
+
+ /**
+ * Returns a `DateTime` object from an iCal date time format
+ *
+ * @param string $icalDate
+ * @return DateTime
+ */
+ public function iCalDateToDateTime(string $icalDate): DateTime
+ {
+ /**
+ * iCal times may be in 3 formats, (https://www.kanzaki.com/docs/ical/dateTime.html)
+ *
+ * UTC: Has a trailing 'Z'
+ * Floating: No time zone reference specified, no trailing 'Z', use local time
+ * TZID: Set time zone as specified
+ *
+ * Use DateTime class objects to get around limitations with `mktime` and `gmmktime`.
+ * Must have a local time zone set to process floating times.
+ */
+ $pattern = '/^(?:TZID=)?([^:]*|".*")'; // [1]: Time zone
+ $pattern .= ':?'; // Time zone delimiter
+ $pattern .= '([0-9]{8})'; // [2]: YYYYMMDD
+ $pattern .= 'T?'; // Time delimiter
+ $pattern .= '(?(?<=T)([0-9]{6}))'; // [3]: HHMMSS (filled if delimiter present)
+ $pattern .= '(Z?)/'; // [4]: UTC flag
+
+ preg_match($pattern, $icalDate, $date);
+
+ if (empty($date)) {
+ error_log('Invalid iCal date format: ' . $icalDate);
+ return new Datetime('1970-08-08'); // Return something far in the past
+ }
+
+ // A Unix timestamp usually cannot represent a date prior to 1 Jan 1970.
+ // PHP, on the other hand, uses negative numbers for that. Thus we don't
+ // need to special case them.
+
+ if ($date[4] === 'Z') {
+ $dateTimeZone = new DateTimeZone(self::TIME_ZONE_UTC);
+ } elseif (!empty($date[1])) {
+ $dateTimeZone = $this->timeZoneStringToDateTimeZone($date[1]);
+ } else {
+ $dateTimeZone = new DateTimeZone($this->defaultTimeZone);
+ }
+
+ // The exclamation mark at the start of the format string indicates that if a
+ // time portion is not included, the time in the returned DateTime should be
+ // set to 00:00:00. Without it, the time would be set to the current system time.
+ $dateFormat = '!Ymd';
+ $dateBasic = $date[2];
+ if (!empty($date[3])) {
+ $dateBasic .= "T{$date[3]}";
+ $dateFormat .= '\THis';
+ }
+
+ return DateTime::createFromFormat($dateFormat, $dateBasic, $dateTimeZone);
+ }
+
+ /**
+ * Returns a Unix timestamp from an iCal date time format
+ *
+ * @param string $icalDate
+ * @return integer
+ */
+ public function iCalDateToUnixTimestamp(string $icalDate): int
+ {
+ return $this->iCalDateToDateTime($icalDate)->getTimestamp();
+ }
+
+ /**
+ * Returns a date adapted to the calendar time zone depending on the event `TZID`
+ *
+ * @param array $event
+ * @param string $key
+ * @param string $format
+ * @return string|boolean
+ */
+ public function iCalDateWithTimeZone(array $event, string $key, string $format = self::DATE_TIME_FORMAT)
+ {
+ if (!isset($event["{$key}_array"]) || !isset($event[$key])) {
+ return false;
+ }
+
+ $dateArray = $event["{$key}_array"];
+
+ if ($key === 'DURATION') {
+ $dateTime = $this->parseDuration($event['DTSTART'], $dateArray[2], null);
+ } else {
+ // When constructing from a Unix Timestamp, no time zone needs passing.
+ $dateTime = new DateTime("@{$dateArray[2]}");
+ }
+
+ // Set the time zone we wish to use when running `$dateTime->format`.
+ $dateTime->setTimezone(new DateTimeZone($this->calendarTimeZone()));
+
+ if (is_null($format)) {
+ return $dateTime;
+ }
+
+ return $dateTime->format($format);
+ }
+
+ /**
+ * Performs admin tasks on all events as read from the iCal file.
+ * Adds a Unix timestamp to all `{DTSTART|DTEND|RECURRENCE-ID}_array` arrays
+ * Tracks modified recurrence instances
+ *
+ * @return void
+ */
+ protected function processEvents()
+ {
+ if (empty($this->cal['VEVENT']))
+ return;
+ $events =& $this->cal['VEVENT'];
+
+ foreach ($events as $key => $anEvent) {
+ foreach (array('DTSTART', 'DTEND', 'RECURRENCE-ID') as $type) {
+ if (isset($anEvent[$type])) {
+ $date = $anEvent["{$type}_array"][1];
+
+ if (isset($anEvent["{$type}_array"][0]['TZID'])) {
+ $timeZone = $this->escapeParamText($anEvent["{$type}_array"][0]['TZID']);
+ $date = sprintf(self::ICAL_DATE_TIME_TEMPLATE, $timeZone) . $date;
+ }
+
+ $anEvent["{$type}_array"][2] = $this->iCalDateToUnixTimestamp($date);
+ $anEvent["{$type}_array"][3] = $date;
+ }
+ }
+
+ if (isset($anEvent['RECURRENCE-ID'])) {
+ $uid = $anEvent['UID'];
+
+ if (!isset($this->alteredRecurrenceInstances[$uid])) {
+ $this->alteredRecurrenceInstances[$uid] = array();
+ }
+
+ $recurrenceDateUtc = $this->iCalDateToUnixTimestamp($anEvent['RECURRENCE-ID_array'][3]);
+ $this->alteredRecurrenceInstances[$uid][$key] = $recurrenceDateUtc;
+ }
+
+ $events[$key] = $anEvent;
+ }
+
+ $eventKeysToRemove = array();
+
+ foreach ($events as $key => $event) {
+ $checks = !isset($event['RECURRENCE-ID'])
+ && isset($event['UID']) && isset($this->alteredRecurrenceInstances[$event['UID']]);
+
+ if ($checks) {
+ $eventDtstartUnix = $this->iCalDateToUnixTimestamp($event['DTSTART_array'][3]);
+
+ // phpcs:ignore CustomPHPCS.ControlStructures.AssignmentInCondition
+ if (($alteredEventKey = array_search($eventDtstartUnix, $this->alteredRecurrenceInstances[$event['UID']])) !== false) {
+ $eventKeysToRemove[] = $alteredEventKey;
+
+ $alteredEvent = array_replace_recursive($event, $events[$alteredEventKey]);
+ $this->alteredRecurrenceInstances[$event['UID']]['altered-event'] = array($key => $alteredEvent);
+ }
+ }
+ }
+
+ foreach ($eventKeysToRemove as $eventKeyToRemove) {
+ $events[$eventKeyToRemove] = null;
+ }
+ }
+
+ /**
+ * Processes recurrence rules
+ *
+ * @return void
+ */
+ protected function processRecurrences()
+ {
+ // If there are no events, then we have nothing to process.
+ if (empty($this->cal['VEVENT']))
+ return;
+ $events =& $this->cal['VEVENT'];
+
+ $allEventRecurrences = array();
+ $eventKeysToRemove = array();
+
+ foreach ($events as $key => $anEvent) {
+ if (!isset($anEvent['RRULE']) || $anEvent['RRULE'] === '') {
+ continue;
+ }
+
+ // Tag as generated by a recurrence rule
+ $anEvent['RRULE_array'][2] = self::RECURRENCE_EVENT;
+
+ // Create new initial starting point.
+ $initialEventDate = $this->icalDateToDateTime($anEvent['DTSTART_array'][3]);
+
+ // Separate the RRULE stanzas, and explode the values that are lists.
+ $rrules = array();
+ foreach (explode(';', $anEvent['RRULE']) as $s) {
+ list($k, $v) = explode('=', $s);
+ if (in_array($k, array('BYSETPOS', 'BYDAY', 'BYMONTHDAY', 'BYMONTH'))) {
+ $rrules[$k] = explode(',', $v);
+ } else {
+ $rrules[$k] = $v;
+ }
+ }
+
+ // Get frequency
+ $frequency = $rrules['FREQ'];
+
+ // Reject RRULE if BYDAY stanza is invalid:
+ // > The BYDAY rule part MUST NOT be specified with a numeric value
+ // > when the FREQ rule part is not set to MONTHLY or YEARLY.
+ if (isset($rrules['BYDAY']) && !in_array($frequency, array('MONTHLY', 'YEARLY'))) {
+ $allByDayStanzasValid = array_reduce($rrules['BYDAY'], function ($carry, $weekday) {
+ return $carry && substr($weekday, -2) === $weekday;
+ }, true);
+
+ if (!$allByDayStanzasValid) {
+ error_log("ICal::ProcessRecurrences: A \"{$frequency}\" RRULE should not contain BYDAY values with numeric prefixes");
+
+ continue;
+ }
+ }
+
+ // Get Interval
+ $interval = (empty($rrules['INTERVAL'])) ? 1 : $rrules['INTERVAL'];
+
+ // Throw an error if this isn't an integer.
+ if (!is_int($this->defaultSpan)) {
+ trigger_error('ICal::defaultSpan: User defined value is not an integer', E_USER_NOTICE);
+ }
+
+ // Compute EXDATEs
+ $exdates = $this->parseExdates($anEvent);
+
+ // Determine if the initial date is also an EXDATE
+ $initialDateIsExdate = array_reduce($exdates, function ($carry, $exdate) use ($initialEventDate) {
+ return $carry || $exdate->getTimestamp() === $initialEventDate->getTimestamp();
+ }, false);
+
+ if ($initialDateIsExdate) {
+ $eventKeysToRemove[] = $key;
+ }
+
+ /**
+ * Determine at what point we should stop calculating recurrences
+ * by looking at the UNTIL or COUNT rrule stanza, or, if neither
+ * if set, using a fallback.
+ *
+ * If the initial date is also an EXDATE, it shouldn't be included
+ * in the count.
+ *
+ * Syntax:
+ * UNTIL={enddate}
+ * COUNT=<positive integer>
+ *
+ * Where:
+ * enddate = <icalDate> || <icalDateTime>
+ */
+ $count = 1;
+ $countLimit = (isset($rrules['COUNT'])) ? intval($rrules['COUNT']) : 0;
+ $until = date_create()->modify("{$this->defaultSpan} years")->setTime(23, 59, 59)->getTimestamp();
+
+ if (isset($rrules['UNTIL'])) {
+ $until = min($until, $this->iCalDateToUnixTimestamp($rrules['UNTIL']));
+ }
+ $until = min($until, $this->windowMaxTimestamp);
+
+ $eventRecurrences = array();
+
+ $frequencyRecurringDateTime = clone $initialEventDate;
+ while ($frequencyRecurringDateTime->getTimestamp() <= $until) {
+ $candidateDateTimes = array();
+
+ // phpcs:ignore Squiz.ControlStructures.SwitchDeclaration.MissingDefault
+ switch ($frequency) {
+ case 'DAILY':
+ $candidateDateTimes[] = clone $frequencyRecurringDateTime;
+
+ break;
+
+ case 'WEEKLY':
+ $initialDayOfWeek = $frequencyRecurringDateTime->format('N');
+ $matchingDays = array($initialDayOfWeek);
+
+ if (!empty($rrules['BYDAY'])) {
+ // setISODate() below uses the ISO-8601 specification of weeks: start on
+ // a Monday, end on a Sunday. However, RRULEs (or the caller of the
+ // parser) may state an alternate WeeKSTart.
+ $wkstTransition = 7;
+
+ if (empty($rrules['WKST'])) {
+ if ($this->defaultWeekStart !== self::ISO_8601_WEEK_START) {
+ $wkstTransition = array_search($this->defaultWeekStart, array_keys($this->weekdays));
+ }
+ } elseif ($rrules['WKST'] !== self::ISO_8601_WEEK_START) {
+ $wkstTransition = array_search($rrules['WKST'], array_keys($this->weekdays));
+ }
+
+ $matchingDays = array_map(
+ function ($weekday) use ($initialDayOfWeek, $wkstTransition, $interval) {
+ $day = array_search($weekday, array_keys($this->weekdays));
+
+ if ($day < $initialDayOfWeek) {
+ $day += 7;
+ }
+
+ if ($day >= $wkstTransition) {
+ $day += 7 * ($interval - 1);
+ }
+
+ // Ignoring alternate week starts, $day at this point will have a
+ // value between 0 and 6. But setISODate() expects a value of 1 to 7.
+ // Even with alternate week starts, we still need to +1 to set the
+ // correct weekday.
+ $day++;
+
+ return $day;
+ },
+ $rrules['BYDAY']
+ );
+ }
+
+ sort($matchingDays);
+
+ foreach ($matchingDays as $day) {
+ $clonedDateTime = clone $frequencyRecurringDateTime;
+ $candidateDateTimes[] = $clonedDateTime->setISODate(
+ $frequencyRecurringDateTime->format('o'),
+ $frequencyRecurringDateTime->format('W'),
+ $day
+ );
+ }
+ break;
+
+ case 'MONTHLY':
+ $matchingDays = array();
+
+ if (!empty($rrules['BYMONTHDAY'])) {
+ $matchingDays = $rrules['BYMONTHDAY'];
+ } elseif (!empty($rrules['BYDAY'])) {
+ $matchingDays = $this->getDaysOfMonthMatchingByDayRRule($rrules['BYDAY'], $frequencyRecurringDateTime);
+ }
+
+ if (!empty($rrules['BYSETPOS'])) {
+ $matchingDays = $this->filterValuesUsingBySetPosRRule($rrules['BYSETPOS'], $matchingDays);
+ }
+
+ foreach ($matchingDays as $day) {
+ // Skip invalid dates (e.g. 30th February)
+ if ($day > $frequencyRecurringDateTime->format('t')) {
+ continue;
+ }
+
+ $clonedDateTime = clone $frequencyRecurringDateTime;
+ $candidateDateTimes[] = $clonedDateTime->setDate(
+ $frequencyRecurringDateTime->format('Y'),
+ $frequencyRecurringDateTime->format('m'),
+ $day
+ );
+ }
+ break;
+
+ case 'YEARLY':
+ if (!empty($rrules['BYMONTH'])) {
+ foreach ($rrules['BYMONTH'] as $byMonth) {
+ $clonedDateTime = clone $frequencyRecurringDateTime;
+ $bymonthRecurringDatetime = $clonedDateTime->setDate(
+ $frequencyRecurringDateTime->format('Y'),
+ $byMonth,
+ $frequencyRecurringDateTime->format('d')
+ );
+
+ if (!empty($rrules['BYDAY'])) {
+ // Get all days of the month that match the BYDAY rule.
+ $matchingDays = $this->getDaysOfMonthMatchingByDayRRule($rrules['BYDAY'], $bymonthRecurringDatetime);
+
+ // And add each of them to the list of recurrences
+ foreach ($matchingDays as $day) {
+ $clonedDateTime = clone $bymonthRecurringDatetime;
+ $candidateDateTimes[] = $clonedDateTime->setDate(
+ $frequencyRecurringDateTime->format('Y'),
+ $bymonthRecurringDatetime->format('m'),
+ $day
+ );
+ }
+ } else {
+ $candidateDateTimes[] = clone $bymonthRecurringDatetime;
+ }
+ }
+ } else {
+ $candidateDateTimes[] = clone $frequencyRecurringDateTime;
+ }
+ break;
+ }
+
+ foreach ($candidateDateTimes as $candidate) {
+ $timestamp = $candidate->getTimestamp();
+ if ($timestamp <= $initialEventDate->getTimestamp()) {
+ continue;
+ }
+
+ if ($timestamp > $until) {
+ break;
+ }
+
+ // Exclusions
+ $isExcluded = ($this->shouldFilterByWindow && $timestamp + 160000 < $this->windowMinTimestamp);
+
+ if (!$isExcluded) {
+ $isExcluded = array_filter($exdates, function ($exdate) use ($timestamp) {
+ return $exdate->getTimestamp() == $timestamp;
+ });
+ }
+
+ if (!$isExcluded && isset($this->alteredRecurrenceInstances[$anEvent['UID']])) {
+ if (in_array($timestamp, $this->alteredRecurrenceInstances[$anEvent['UID']])) {
+ $isExcluded = true;
+ }
+ }
+
+ if (!$isExcluded) {
+ $eventRecurrences[] = $candidate;
+ $this->eventCount++;
+ }
+
+ // Count all evaluated candidates including excluded ones
+ if (isset($rrules['COUNT'])) {
+ $count++;
+
+ // If RRULE[COUNT] is reached then break
+ if ($count >= $countLimit) {
+ break 2;
+ }
+ }
+ }
+
+ // Move forwards $interval $frequency.
+ $monthPreMove = $frequencyRecurringDateTime->format('m');
+ $frequencyRecurringDateTime->modify("{$interval} {$this->frequencyConversion[$frequency]}");
+
+ // As noted in Example #2 on https://www.php.net/manual/en/datetime.modify.php,
+ // there are some occasions where adding months doesn't give the month you might
+ // expect. For instance: January 31st + 1 month == March 3rd (March 2nd on a leap
+ // year.) The following code crudely rectifies this.
+ if ($frequency === 'MONTHLY') {
+ $monthDiff = $frequencyRecurringDateTime->format('m') - $monthPreMove;
+
+ if (($monthDiff > 0 && $monthDiff > $interval) || ($monthDiff < 0 && $monthDiff > $interval - 12)) {
+ $frequencyRecurringDateTime->modify('-1 month');
+ }
+ }
+ }
+
+ // Determine event length
+ $eventLength = 0;
+ if (isset($anEvent['DURATION'])) {
+ $clonedDateTime = clone $initialEventDate;
+ $endDate = $clonedDateTime->add($anEvent['DURATION_array'][2]);
+ $eventLength = $endDate->getTimestamp() - $anEvent['DTSTART_array'][2];
+ } elseif (isset($anEvent['DTEND_array'])) {
+ $eventLength = $anEvent['DTEND_array'][2] - $anEvent['DTSTART_array'][2];
+ }
+
+ // Whether or not the initial date was UTC
+ $initialDateWasUTC = substr($anEvent['DTSTART'], -1) === 'Z';
+
+ // Build the param array
+ $dateParamArray = array();
+ if (
+ !$initialDateWasUTC
+ && isset($anEvent['DTSTART_array'][0]['TZID'])
+ && $this->isValidTimeZoneId($anEvent['DTSTART_array'][0]['TZID'])
+ ) {
+ $dateParamArray['TZID'] = $anEvent['DTSTART_array'][0]['TZID'];
+ }
+
+ // Populate the `DT{START|END}[_array]`s
+ $eventRecurrences = array_map(
+ function ($recurringDatetime) use ($anEvent, $eventLength, $initialDateWasUTC, $dateParamArray) {
+ $tzidPrefix = (isset($dateParamArray['TZID'])) ? 'TZID=' . $this->escapeParamText($dateParamArray['TZID']) . ':' : '';
+
+ foreach (array('DTSTART', 'DTEND') as $dtkey) {
+ $anEvent[$dtkey] = $recurringDatetime->format(self::DATE_TIME_FORMAT) . (($initialDateWasUTC) ? 'Z' : '');
+
+ $anEvent["{$dtkey}_array"] = array(
+ $dateParamArray, // [0] Array of params (incl. TZID)
+ $anEvent[$dtkey], // [1] ICalDateTime string w/o TZID
+ $recurringDatetime->getTimestamp(), // [2] Unix Timestamp
+ "{$tzidPrefix}{$anEvent[$dtkey]}", // [3] Full ICalDateTime string
+ );
+
+ if ($dtkey !== 'DTEND') {
+ $recurringDatetime->modify("{$eventLength} seconds");
+ }
+ }
+
+ return $anEvent;
+ },
+ $eventRecurrences
+ );
+
+ $allEventRecurrences = array_merge($allEventRecurrences, $eventRecurrences);
+ }
+
+ // Nullify the initial events that are also EXDATEs
+ foreach ($eventKeysToRemove as $eventKeyToRemove) {
+ $events[$eventKeyToRemove] = null;
+ }
+
+ $events = array_merge($events, $allEventRecurrences);
+ }
+
+ /**
+ * Find all days of a month that match the BYDAY stanza of an RRULE.
+ *
+ * With no {ordwk}, then return the day number of every {weekday}
+ * within the month.
+ *
+ * With a +ve {ordwk}, then return the {ordwk} {weekday} within the
+ * month.
+ *
+ * With a -ve {ordwk}, then return the {ordwk}-to-last {weekday}
+ * within the month.
+ *
+ * RRule Syntax:
+ * BYDAY={bywdaylist}
+ *
+ * Where:
+ * bywdaylist = {weekdaynum}[,{weekdaynum}...]
+ * weekdaynum = [[+]{ordwk} || -{ordwk}]{weekday}
+ * ordwk = 1 to 53
+ * weekday = SU || MO || TU || WE || TH || FR || SA
+ *
+ * @param array $byDays
+ * @param DateTime $initialDateTime
+ * @return array
+ */
+ protected function getDaysOfMonthMatchingByDayRRule(array $byDays, DateTime $initialDateTime): array
+ {
+ $matchingDays = array();
+
+ foreach ($byDays as $weekday) {
+ $bydayDateTime = clone $initialDateTime;
+
+ $ordwk = intval(substr($weekday, 0, -2));
+
+ // Quantise the date to the first instance of the requested day in a month
+ // (Or last if we have a -ve {ordwk})
+ $bydayDateTime->modify(
+ (($ordwk < 0) ? 'Last' : 'First')
+ . ' '
+ . $this->weekdays[substr($weekday, -2)] // e.g. "Monday"
+ . ' of ' . $initialDateTime->format('F') // e.g. "June"
+ );
+
+ if ($ordwk < 0) { // -ve {ordwk}
+ $bydayDateTime->modify((++$ordwk) . ' week');
+ $matchingDays[] = $bydayDateTime->format('j');
+ } elseif ($ordwk > 0) { // +ve {ordwk}
+ $bydayDateTime->modify((--$ordwk) . ' week');
+ $matchingDays[] = $bydayDateTime->format('j');
+ } else { // No {ordwk}
+ while ($bydayDateTime->format('n') === $initialDateTime->format('n')) {
+ $matchingDays[] = $bydayDateTime->format('j');
+ $bydayDateTime->modify('+1 week');
+ }
+ }
+ }
+
+ // Sort into ascending order.
+ sort($matchingDays);
+
+ return $matchingDays;
+ }
+
+ /**
+ * Filters a provided values-list by applying a BYSETPOS RRule.
+ *
+ * Where a +ve {daynum} is provided, the {ordday} position'd value as
+ * measured from the start of the list of values should be retained.
+ *
+ * Where a -ve {daynum} is provided, the {ordday} position'd value as
+ * measured from the end of the list of values should be retained.
+ *
+ * RRule Syntax:
+ * BYSETPOS={bysplist}
+ *
+ * Where:
+ * bysplist = {setposday}[,{setposday}...]
+ * setposday = {daynum}
+ * daynum = [+ || -] {ordday}
+ * ordday = 1 to 366
+ *
+ * @param array $bySetPos
+ * @param array $valuesList
+ * @return array
+ */
+ protected function filterValuesUsingBySetPosRRule(array $bySetPos, array $valuesList): array
+ {
+ $filteredMatches = array();
+
+ foreach ($bySetPos as $setPosition) {
+ if ($setPosition < 0) {
+ $setPosition = count($valuesList) + ++$setPosition;
+ }
+
+ // Positioning starts at 1, array indexes start at 0
+ if (isset($valuesList[$setPosition - 1])) {
+ $filteredMatches[] = $valuesList[$setPosition - 1];
+ }
+ }
+
+ return $filteredMatches;
+ }
+
+ /**
+ * Processes date conversions using the time zone
+ *
+ * Add keys `DTSTART_tz` and `DTEND_tz` to each Event
+ * These keys contain dates adapted to the calendar
+ * time zone depending on the event `TZID`.
+ *
+ * @return void
+ */
+ protected function processDateConversions()
+ {
+ if (empty($this->cal['VEVENT']))
+ return;
+
+ $events =& $this->cal['VEVENT'];
+ foreach ($events as $key => $anEvent) {
+ if (is_null($anEvent) || !$this->isValidDate($anEvent['DTSTART'])) {
+ unset($events[$key]);
+ $this->eventCount--;
+
+ continue;
+ }
+
+ $events[$key]['DTSTART_tz'] = $this->iCalDateWithTimeZone($anEvent, 'DTSTART');
+
+ if ($this->iCalDateWithTimeZone($anEvent, 'DTEND')) {
+ $events[$key]['DTEND_tz'] = $this->iCalDateWithTimeZone($anEvent, 'DTEND');
+ } elseif ($this->iCalDateWithTimeZone($anEvent, 'DURATION')) {
+ $events[$key]['DTEND_tz'] = $this->iCalDateWithTimeZone($anEvent, 'DURATION');
+ } else {
+ $events[$key]['DTEND_tz'] = $events[$key]['DTSTART_tz'];
+ }
+ }
+ }
+
+ /**
+ * Returns an array of Events.
+ * Every event is a class with the event
+ * details being properties within it.
+ *
+ * @return ICalEvent[]
+ */
+ public function events(): array
+ {
+ if (empty($this->cal) || empty($this->cal['VEVENT']))
+ return [];
+
+ $events = array();
+ foreach ($this->cal['VEVENT'] as $event) {
+ $events[] = new ICalEvent($event);
+ }
+
+ return $events;
+ }
+
+ /**
+ * Returns the calendar name
+ *
+ * @return string
+ */
+ public function calendarName(): string
+ {
+ return $this->cal['VCALENDAR']['X-WR-CALNAME'] ?? '';
+ }
+
+ /**
+ * Returns the calendar description
+ *
+ * @return string
+ */
+ public function calendarDescription(): string
+ {
+ return $this->cal['VCALENDAR']['X-WR-CALDESC'] ?? '';
+ }
+
+ /**
+ * Returns the calendar time zone
+ *
+ * @param boolean $ignoreUtc
+ * @return string
+ */
+ public function calendarTimeZone(bool $ignoreUtc = false): ?string
+ {
+ if (isset($this->cal['VCALENDAR']['X-WR-TIMEZONE'])) {
+ $timeZone = $this->cal['VCALENDAR']['X-WR-TIMEZONE'];
+ } elseif (isset($this->cal['VTIMEZONE']['TZID'])) {
+ $timeZone = $this->cal['VTIMEZONE']['TZID'];
+ } else {
+ $timeZone = $this->defaultTimeZone;
+ }
+
+ // Validate the time zone, falling back to the time zone set in the PHP environment.
+ $timeZone = $this->timeZoneStringToDateTimeZone($timeZone)->getName();
+
+ if ($ignoreUtc && strtoupper($timeZone) === self::TIME_ZONE_UTC) {
+ return null;
+ }
+
+ return $timeZone;
+ }
+
+ /**
+ * Returns an array of arrays with all free/busy events.
+ * Every event is an associative array and each property
+ * is an element it.
+ *
+ * @return array
+ */
+ public function freeBusyEvents(): array
+ {
+ $array = $this->cal;
+
+ return $array['VFREEBUSY'] ?? array();
+ }
+
+ /**
+ * Returns a sorted array of the events in a given range,
+ * or an empty array if no events exist in the range.
+ *
+ * Events will be returned if the start or end date is contained within the
+ * range (inclusive), or if the event starts before and end after the range.
+ *
+ * If a start date is not specified or of a valid format, then the start
+ * of the range will default to the current time and date of the server.
+ *
+ * If an end date is not specified or of a valid format, then the end of
+ * the range will default to the current time and date of the server,
+ * plus 20 years.
+ *
+ * Note that this function makes use of Unix timestamps. This might be a
+ * problem for events on, during, or after 29 Jan 2038.
+ * See https://en.wikipedia.org/wiki/Unix_time#Representing_the_number
+ *
+ * @param string|null $rangeStart
+ * @param string|null $rangeEnd
+ * @return array
+ * @throws Exception
+ */
+ public function eventsFromRange(string $rangeStart = null, string $rangeEnd = null): array
+ {
+ // Sort events before processing range
+ $events = $this->sortEventsWithOrder($this->events());
+
+ if (empty($events)) {
+ return array();
+ }
+
+ $extendedEvents = array();
+
+ if (!is_null($rangeStart)) {
+ try {
+ $rangeStart = new DateTime($rangeStart, new DateTimeZone($this->defaultTimeZone));
+ } catch (Exception $exception) {
+ error_log("ICal::eventsFromRange: Invalid date passed ({$rangeStart})");
+ $rangeStart = false;
+ }
+ } else {
+ $rangeStart = new DateTime('now', new DateTimeZone($this->defaultTimeZone));
+ }
+
+ if (!is_null($rangeEnd)) {
+ try {
+ $rangeEnd = new DateTime($rangeEnd, new DateTimeZone($this->defaultTimeZone));
+ } catch (Exception $exception) {
+ error_log("ICal::eventsFromRange: Invalid date passed ({$rangeEnd})");
+ $rangeEnd = false;
+ }
+ } else {
+ $rangeEnd = new DateTime('now', new DateTimeZone($this->defaultTimeZone));
+ $rangeEnd->modify('+20 years');
+ }
+
+ // If start and end are identical and are dates with no times...
+ if ($rangeEnd->format('His') == 0 && $rangeStart->getTimestamp() === $rangeEnd->getTimestamp()) {
+ $rangeEnd->modify('+1 day');
+ }
+
+ $rangeStart = $rangeStart->getTimestamp();
+ $rangeEnd = $rangeEnd->getTimestamp();
+
+ foreach ($events as $anEvent) {
+ $eventStart = $anEvent->dtstart_array[2];
+ $eventEnd = (isset($anEvent->dtend_array[2])) ? $anEvent->dtend_array[2] : null;
+
+ if (
+ ($eventStart >= $rangeStart && $eventStart < $rangeEnd) // Event start date contained in the range
+ || ($eventEnd !== null
+ && (
+ ($eventEnd > $rangeStart && $eventEnd <= $rangeEnd) // Event end date contained in the range
+ || ($eventStart < $rangeStart && $eventEnd > $rangeEnd) // Event starts before and finishes after range
+ )
+ )
+ ) {
+ $extendedEvents[] = $anEvent;
+ }
+ }
+
+ if (empty($extendedEvents)) {
+ return array();
+ }
+
+ return $extendedEvents;
+ }
+
+ /**
+ * Sorts events based on a given sort order
+ *
+ * @param array $events
+ * @param integer $sortOrder Either SORT_ASC, SORT_DESC, SORT_REGULAR, SORT_NUMERIC, SORT_STRING
+ * @return array
+ */
+ public function sortEventsWithOrder(array $events, int $sortOrder = SORT_ASC): array
+ {
+ $extendedEvents = array();
+ $timestamp = array();
+
+ foreach ($events as $key => $anEvent) {
+ $extendedEvents[] = $anEvent;
+ $timestamp[$key] = $anEvent->dtstart_array[2];
+ }
+
+ array_multisort($timestamp, $sortOrder, $extendedEvents);
+
+ return $extendedEvents;
+ }
+
+ /**
+ * Checks if a time zone is valid (IANA, CLDR, or Windows)
+ *
+ * @param string $timeZone
+ * @return boolean
+ */
+ protected function isValidTimeZoneId(string $timeZone): bool
+ {
+ return $this->isValidIanaTimeZoneId($timeZone) !== false
+ || $this->isValidCldrTimeZoneId($timeZone) !== false
+ || $this->isValidWindowsTimeZoneId($timeZone) !== false;
+ }
+
+ /**
+ * Checks if a time zone is a valid IANA time zone
+ *
+ * @param string $timeZone
+ * @return boolean
+ */
+ protected function isValidIanaTimeZoneId(string $timeZone): bool
+ {
+ if (in_array($timeZone, $this->validIanaTimeZones)) {
+ return true;
+ }
+
+ $valid = array();
+ $tza = timezone_abbreviations_list();
+
+ foreach ($tza as $zone) {
+ foreach ($zone as $item) {
+ $valid[$item['timezone_id']] = true;
+ }
+ }
+
+ unset($valid['']);
+
+ if (isset($valid[$timeZone]) || in_array($timeZone, timezone_identifiers_list(DateTimeZone::ALL_WITH_BC))) {
+ $this->validIanaTimeZones[] = $timeZone;
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks if a time zone is a valid CLDR time zone
+ *
+ * @param string $timeZone
+ * @return boolean
+ */
+ public function isValidCldrTimeZoneId(string $timeZone): bool
+ {
+ return array_key_exists(html_entity_decode($timeZone), self::$cldrTimeZonesMap);
+ }
+
+ /**
+ * Checks if a time zone is a recognised Windows (non-CLDR) time zone
+ *
+ * @param string $timeZone
+ * @return boolean
+ */
+ public function isValidWindowsTimeZoneId(string $timeZone): bool
+ {
+ return array_key_exists(html_entity_decode($timeZone), self::$windowsTimeZonesMap);
+ }
+
+ /**
+ * Parses a duration and applies it to a date
+ *
+ * @return integer|DateTime
+ */
+ protected function parseDuration(string $date, DateInterval $duration, ?string $format = self::UNIX_FORMAT)
+ {
+ $dateTime = date_create($date);
+ $dateTime->modify("{$duration->y} year");
+ $dateTime->modify("{$duration->m} month");
+ $dateTime->modify("{$duration->d} day");
+ $dateTime->modify("{$duration->h} hour");
+ $dateTime->modify("{$duration->i} minute");
+ $dateTime->modify("{$duration->s} second");
+
+ if (is_null($format)) {
+ $output = $dateTime;
+ } elseif ($format === self::UNIX_FORMAT) {
+ $output = $dateTime->getTimestamp();
+ } else {
+ $output = $dateTime->format($format);
+ }
+
+ return $output;
+ }
+
+ /**
+ * Removes unprintable ASCII and UTF-8 characters
+ *
+ * @param string $data
+ * @return string
+ */
+ protected function removeUnprintableChars(string $data): string
+ {
+ return preg_replace('/[\x00-\x1F\x7F\xA0]/u', '', $data);
+ }
+
+ /**
+ * Places double-quotes around texts that have characters not permitted
+ * in parameter-texts, but are permitted in quoted-texts.
+ *
+ * @param string $candidateText
+ * @return string
+ */
+ protected function escapeParamText(string $candidateText): string
+ {
+ if (strpbrk($candidateText, ':;,') !== false) {
+ return '"' . $candidateText . '"';
+ }
+
+ return $candidateText;
+ }
+
+ /**
+ * Parses a list of excluded dates
+ * to be applied to an Event
+ *
+ * @param array $event
+ * @return array
+ */
+ public function parseExdates(array $event): array
+ {
+ if (empty($event['EXDATE_array'])) {
+ return array();
+ }
+ $exdates = $event['EXDATE_array'];
+
+ $output = array();
+ $currentTimeZone = $this->defaultTimeZone;
+
+ foreach ($exdates as $subArray) {
+ end($subArray);
+ $finalKey = key($subArray);
+
+ foreach (array_keys($subArray) as $key) {
+ if ($key === 'TZID') {
+ $currentTimeZone = $subArray[$key];
+ } elseif (is_numeric($key)) {
+ $icalDate = $subArray[$key];
+
+ if (substr($icalDate, -1) === 'Z') {
+ $currentTimeZone = self::TIME_ZONE_UTC;
+ }
+
+ $output[] = new DateTimeImmutable($icalDate, $this->timeZoneStringToDateTimeZone($currentTimeZone));
+
+ if ($key === $finalKey) {
+ // Reset to default
+ $currentTimeZone = $this->defaultTimeZone;
+ }
+ }
+ }
+ }
+
+ return $output;
+ }
+
+ /**
+ * Checks if a date string is a valid date
+ *
+ * @param string $value
+ * @return boolean
+ */
+ public function isValidDate(string $value): bool
+ {
+ if (!$value) {
+ return false;
+ }
+
+ try {
+ new DateTime($value);
+
+ return true;
+ } catch (Exception $exception) {
+ return false;
+ }
+ }
+
+ /**
+ * Returns a `DateTimeZone` object based on a string containing a time zone name.
+ * Falls back to the default time zone if string passed not a recognised time zone.
+ *
+ * @param DateTimeZone|string $timeZoneString
+ * @return DateTimeZone
+ */
+ public function timeZoneStringToDateTimeZone($timeZoneString): DateTimeZone
+ {
+ if ($timeZoneString instanceof DateTimeZone)
+ return $timeZoneString;
+ // Some time zones contain characters that are not permitted in param-texts,
+ // but are within quoted texts. We need to remove the quotes as they're not
+ // actually part of the time zone.
+ $timeZoneString = trim($timeZoneString, '"');
+ $timeZoneString = html_entity_decode($timeZoneString);
+
+ if ($this->isValidIanaTimeZoneId($timeZoneString)) {
+ return new DateTimeZone($timeZoneString);
+ }
+
+ if ($this->isValidCldrTimeZoneId($timeZoneString)) {
+ return new DateTimeZone(self::$cldrTimeZonesMap[$timeZoneString]);
+ }
+
+ if ($this->isValidWindowsTimeZoneId($timeZoneString)) {
+ return new DateTimeZone(self::$windowsTimeZonesMap[$timeZoneString]);
+ }
+
+ return new DateTimeZone($this->defaultTimeZone);
+ }
+}
diff --git a/modules-available/locationinfo/inc/infopanel.inc.php b/modules-available/locationinfo/inc/infopanel.inc.php
index 7b0c6fe0..1a0e9b67 100644
--- a/modules-available/locationinfo/inc/infopanel.inc.php
+++ b/modules-available/locationinfo/inc/infopanel.inc.php
@@ -7,16 +7,16 @@ class InfoPanel
* Gets the config of the location.
*
* @param int $locationID ID of the location
- * @param mixed $config the panel config will be returned here
- * @return string|bool paneltype, false if not exists
+ * @param ?array $config the panel config will be returned here
+ * @return ?string panel type, null if not exists
*/
- public static function getConfig($paneluuid, &$config)
+ public static function getConfig(string $paneluuid, ?array &$config): ?string
{
$panel = Database::queryFirst('SELECT panelname, panelconfig, paneltype, locationids FROM locationinfo_panel WHERE paneluuid = :paneluuid',
compact('paneluuid'));
if ($panel === false) {
- return false;
+ return null;
}
$config = LocationInfo::defaultPanelConfig($panel['paneltype']);
@@ -65,7 +65,7 @@ class InfoPanel
$config['locations'][$lid]['config'] = $overrides[$lid];
}
}
- self::appendMachineData($config['locations'], $lids, true);
+ self::appendMachineData($config['locations'], $lids, true, $config['hostname']);
}
self::appendOpeningTimes($config['locations'], $lids);
@@ -87,13 +87,10 @@ class InfoPanel
* @param array $array location list to populate with machine data
* @param bool $withPosition Defines if coords should be included or not.
*/
- public static function appendMachineData(&$array, $idList = false, $withPosition = false)
+ public static function appendMachineData(array &$array, array $idList, bool $withPosition = false, bool $withHostname = false): void
{
- if (empty($array) && $idList === false)
+ if (empty($idList))
return;
- if ($idList === false) {
- $idList = array_keys($array);
- }
$ignoreList = array();
if (Module::isAvailable('runmode')) {
@@ -101,13 +98,21 @@ class InfoPanel
$ignoreList = RunMode::getAllClients(false, false);
}
- $positionCol = $withPosition ? 'm.position,' : '';
- $query = "SELECT m.locationid, m.machineuuid, $positionCol m.logintime, m.lastseen, m.lastboot, m.state FROM machine m
+ $extraCols = '';
+ if ($withPosition) {
+ $extraCols .= 'm.position,';
+ }
+ if ($withHostname) {
+ $extraCols .= 'm.hostname,';
+ }
+ $query = "SELECT m.locationid, m.fixedlocationid, m.machineuuid, $extraCols m.logintime,
+ m.lastseen, m.lastboot, m.state, m.currentrunmode
+ FROM machine m
WHERE m.locationid IN (:idlist)";
$dbquery = Database::simpleQuery($query, array('idlist' => $idList));
// Iterate over matching machines
- while ($row = $dbquery->fetch(PDO::FETCH_ASSOC)) {
+ foreach ($dbquery as $row) {
if (isset($ignoreList[$row['machineuuid']]))
continue;
settype($row['locationid'], 'int');
@@ -119,7 +124,8 @@ class InfoPanel
}
// Compact the pc data in one array.
$pc = array('id' => $row['machineuuid']);
- if ($withPosition && !empty($row['position'])) {
+ if ($withPosition && $row['locationid'] == $row['fixedlocationid'] && !empty($row['position'])) {
+ // check fixed* == locationid to ignore stale position data in relocated clients
$position = json_decode($row['position'], true);
if (isset($position['gridCol']) && isset($position['gridRow'])) {
$pc['x'] = $position['gridCol'];
@@ -129,6 +135,19 @@ class InfoPanel
}
}
}
+ if ($withHostname) {
+ if (ip2long($row['hostname']) !== false) {
+ $pc['host'] = $row['hostname'];
+ } else {
+ $i = strpos($row['hostname'], '.');
+ if ($i === false) {
+ $pc['host'] = $row['hostname'];
+ } else {
+ $pc['host'] = substr($row['hostname'], 0, $i);
+ }
+ }
+ }
+ $pc['runmode'] = $row['currentrunmode'];
$pc['pcState'] = LocationInfo::getPcState($row);
//$pc['pcState'] = ['BROKEN', 'OFFLINE', 'IDLE', 'OCCUPIED', 'STANDBY'][mt_rand(0,4)]; // XXX
@@ -143,17 +162,17 @@ class InfoPanel
* @param array $array list of locations, indexed by locationId
* @param int[] $idList list of locations
*/
- public static function appendOpeningTimes(&$array, $idList)
+ public static function appendOpeningTimes(array &$array, array $idList): void
{
// First, lets get all the parent ids for the given locations
// in case we need to get inherited opening times
$allIds = self::getLocationsWithParents($idList);
if (empty($allIds))
return;
- $res = Database::simpleQuery("SELECT locationid, openingtime FROM locationinfo_locationconfig
+ $res = Database::simpleQuery("SELECT locationid, openingtime FROM location
WHERE locationid IN (:lids)", array('lids' => $allIds));
$openingTimes = array();
- while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ foreach ($res as $row) {
$openingTimes[(int)$row['locationid']] = $row;
}
// Now we got all the calendars for locations and parents
@@ -185,7 +204,6 @@ class InfoPanel
$currentId = $locations[$currentId]['parentlocationid'];
}
}
- return;
}
@@ -196,12 +214,12 @@ class InfoPanel
* @param int[] $idList location ids
* @return int[] more location ids
*/
- private static function getLocationsWithParents($idList)
+ private static function getLocationsWithParents(array $idList): array
{
$locations = Location::getLocationsAssoc();
$allIds = $idList;
foreach ($idList as $id) {
- if (isset($locations[$id]) && isset($locations[$id]['parents'])) {
+ if (isset($locations[$id]['parents'])) {
$allIds = array_merge($allIds, $locations[$id]['parents']);
}
}
@@ -212,14 +230,14 @@ class InfoPanel
/**
* Format the openingtime in the frontend needed format.
- * One key per week day, wich contains an array of {
+ * One key per week day, which contains an array of {
* 'HourOpen' => hh, 'MinutesOpen' => mm,
* 'HourClose' => hh, 'MinutesClose' => mm }
*
* @param array $openingtime The opening time in the db saved format.
- * @return mixed The opening time in the frontend needed format.
+ * @return array The opening time in the frontend needed format.
*/
- private static function formatOpeningtime($openingtime)
+ private static function formatOpeningtime(array $openingtime): array
{
$result = array();
foreach ($openingtime as $entry) {
diff --git a/modules-available/locationinfo/inc/locationinfo.inc.php b/modules-available/locationinfo/inc/locationinfo.inc.php
index 0a391a83..42829a18 100644
--- a/modules-available/locationinfo/inc/locationinfo.inc.php
+++ b/modules-available/locationinfo/inc/locationinfo.inc.php
@@ -7,14 +7,14 @@ class LocationInfo
* Gets the pc data and returns it's state.
*
* @param array $pc The pc data from the db. Array('state' => xx, 'lastseen' => xxx)
- * @return int pc state
+ * @return string pc state
*/
- public static function getPcState($pc)
+ public static function getPcState(array $pc): string
{
$lastseen = (int)$pc['lastseen'];
$NOW = time();
- if ($pc['state'] === 'OFFLINE' && $NOW - $lastseen > 21 * 86400) {
+ if ($pc['state'] === 'OFFLINE' && $NOW - $lastseen > 30 * 86400) {
return "BROKEN";
}
return $pc['state'];
@@ -22,11 +22,12 @@ class LocationInfo
/**
* Return list of locationids associated with given panel.
+ *
* @param string $paneluuid panel
* @param bool $recursive if true and paneltype == SUMMARY the result is recursive with all child room ids.
* @return int[] locationIds
*/
- public static function getLocationsOr404($paneluuid, $recursive = true)
+ public static function getLocationsOr404(string $paneluuid, bool $recursive = true): array
{
$panel = Database::queryFirst('SELECT paneltype, locationids FROM locationinfo_panel WHERE paneluuid = :paneluuid',
compact('paneluuid'));
@@ -48,7 +49,7 @@ class LocationInfo
* @param int $serverId id of server
* @param string|array $message error message to set, array of error message struct, null or false clears error.
*/
- public static function setServerError($serverId, $message)
+ public static function setServerError(int $serverId, $message): void
{
if (is_array($message)) {
$fatal = false;
@@ -86,7 +87,7 @@ class LocationInfo
*
* @return array Return a default config.
*/
- public static function defaultPanelConfig($type)
+ public static function defaultPanelConfig(string $type): array
{
if ($type === 'DEFAULT') {
return array(
@@ -97,6 +98,7 @@ class LocationInfo
'prettytime' => true,
'roomplanner' => true,
'scaledaysauto' => true,
+ 'startday' => 0,
'daystoshow' => 7,
'rotation' => 0,
'scale' => 50,
@@ -105,6 +107,7 @@ class LocationInfo
'roomupdate' => 15,
'configupdate' => 180,
'overrides' => [],
+ 'hostname' => false,
);
}
if ($type === 'SUMMARY') {
@@ -117,17 +120,120 @@ class LocationInfo
}
if ($type === 'URL') {
return array(
- 'iswhitelist' => 0,
- 'urllist' => '',
+ 'whitelist' => '*',
+ 'blacklist' => '',
'insecure-ssl' => 0,
'reload-minutes' => 0,
'split-login' => 0,
'browser' => 'slx-browser',
'interactive' => 0,
'bookmarks' => '',
+ 'allow-tty' => '',
+ 'url' => '',
+ 'zoom-factor' => 100,
);
}
return array();
}
+ /**
+ * Gets the calendar of the given ids.
+ *
+ * @param int[] $idList list with the location ids.
+ * @return array Calendar.
+ */
+ public static function getCalendar(array $idList, bool $forceCached = false): array
+ {
+ if (empty($idList))
+ return [];
+
+ $resultArray = array();
+
+ if ($forceCached) {
+ $res = Database::simpleQuery("SELECT locationid, calendar FROM locationinfo_locationconfig
+ WHERE Length(calendar) > 10 AND lastcalendarupdate > UNIX_TIMESTAMP() - 86400*3");
+ foreach ($res as $row) {
+ $resultArray[] = [
+ 'id' => (int)$row['locationid'],
+ 'calendar' => json_decode($row['calendar'], true),
+ ];
+ }
+ return $resultArray;
+ }
+
+ // Build SQL query for multiple ids.
+ $query = "SELECT l.locationid, l.serverid, l.serverlocationid, s.servertype, s.credentials
+ FROM `locationinfo_locationconfig` AS l
+ INNER JOIN locationinfo_coursebackend AS s ON (s.serverid = l.serverid)
+ WHERE l.locationid IN (:idlist)
+ ORDER BY s.servertype ASC";
+ $dbquery = Database::simpleQuery($query, array('idlist' => array_values($idList)));
+
+ $serverList = array();
+ foreach ($dbquery as $dbresult) {
+ if (!isset($serverList[$dbresult['serverid']])) {
+ $serverList[$dbresult['serverid']] = array(
+ 'credentials' => (array)json_decode($dbresult['credentials'], true),
+ 'type' => $dbresult['servertype'],
+ 'idlist' => array()
+ );
+ }
+ $serverList[$dbresult['serverid']]['idlist'][] = $dbresult['locationid'];
+ }
+
+ foreach ($serverList as $serverid => $server) {
+ $serverInstance = CourseBackend::getInstance($server['type']);
+ if ($serverInstance === false) {
+ EventLog::warning('Cannot fetch schedule for location (' . implode(', ', $server['idlist']) . ')'
+ . ': Backend type ' . $server['type'] . ' unknown. Disabling location.');
+ Database::exec("UPDATE locationinfo_locationconfig SET serverid = NULL WHERE locationid IN (:lid)",
+ array('lid' => $server['idlist']));
+ continue;
+ }
+ $credentialsOk = $serverInstance->setCredentials($serverid, $server['credentials']);
+
+ if ($credentialsOk) {
+ $calendarFromBackend = $serverInstance->fetchSchedule($server['idlist']);
+ } else {
+ $calendarFromBackend = array();
+ }
+
+ LocationInfo::setServerError($serverid, $serverInstance->getErrors());
+
+ if (is_array($calendarFromBackend)) {
+ foreach ($calendarFromBackend as $key => $value) {
+ $resultArray[] = array(
+ 'id' => (int)$key,
+ 'calendar' => $value,
+ );
+ }
+ }
+ }
+ return $resultArray;
+ }
+
+ public static function getAllCalendars(bool $forceCached): array
+ {
+ $locations = Database::queryColumnArray("SELECT locationid FROM location");
+ $calendars = [];
+ foreach (LocationInfo::getCalendar($locations, $forceCached) as $cal) {
+ if (empty($cal['calendar']))
+ continue;
+ $calendars[$cal['id']] = $cal['calendar'];
+ }
+ return $calendars;
+ }
+
+ public static function extractCurrentEvent(array $calendar): string
+ {
+ $NOW = time();
+ foreach ($calendar as $event) {
+ $start = strtotime($event['start']);
+ $end = strtotime($event['end']) + 60;
+ if ($NOW >= $start && $NOW <= $end)
+ return $event['title'];
+ }
+ return '';
+ }
+
}
diff --git a/modules-available/locationinfo/inc/locationinfohooks.inc.php b/modules-available/locationinfo/inc/locationinfohooks.inc.php
index b10be51b..8ec217cc 100644
--- a/modules-available/locationinfo/inc/locationinfohooks.inc.php
+++ b/modules-available/locationinfo/inc/locationinfohooks.inc.php
@@ -5,9 +5,9 @@ class LocationInfoHooks
/**
* @param string $uuid panel uuid
- * @return bool|string panel name if exists, false otherwise
+ * @return false|string panel name if exists, false otherwise
*/
- public static function getPanelName($uuid)
+ public static function getPanelName(string $uuid)
{
$ret = Database::queryFirst('SELECT panelname FROM locationinfo_panel WHERE paneluuid = :uuid', compact('uuid'));
if ($ret === false)
@@ -18,14 +18,11 @@ class LocationInfoHooks
/**
* Hook called by runmode module where we should modify the client config according to our
* needs. Disable standby/logout timeouts, enable autologin, set URL.
- *
- * @param $machineUuid
- * @param $panelUuid
*/
- public static function configHook($machineUuid, $panelUuid)
+ public static function configHook(string $machineUuid, string $panelUuid): void
{
$type = InfoPanel::getConfig($panelUuid, $data);
- if ($type === false)
+ if ($type === null)
return; // TODO: Invalid panel - what should we do?
if ($type === 'URL') {
// Check if we should set the insecure SSL mode (accept invalid/self signed certs etc.)
@@ -36,39 +33,67 @@ class LocationInfoHooks
ConfigHolder::add('SLX_BROWSER_RELOAD_SECS', $data['reload-minutes'] * 60);
}
ConfigHolder::add('SLX_BROWSER_URL', $data['url']);
- ConfigHolder::add('SLX_BROWSER_URLLIST', $data['urllist']);
- ConfigHolder::add('SLX_BROWSER_IS_WHITELIST', $data['iswhitelist']);
+ // Mangle non-upgraded panels
+ if (empty($data['blacklist']) && $data['whitelist'] === '*' && !empty($data['urllist'])) {
+ if ($data['iswhitelist']) {
+ $data['whitelist'] = str_replace(' ', "\n", $data['urllist']);
+ } else {
+ $data['blacklist'] = str_replace(' ', "\n", $data['urllist']);
+ }
+ }
+ ConfigHolder::add('SLX_BROWSER_WHITELIST', self::mangleList($data['whitelist']));
+ ConfigHolder::add('SLX_BROWSER_BLACKLIST', self::mangleList($data['blacklist']));
// Additionally, update runmode "isclient" flag depending on whether split-login is allowed or not
if (isset($data['split-login']) && $data['split-login']) {
RunMode::updateClientFlag($machineUuid, 'locationinfo', true);
} else { // Automatic login
RunMode::updateClientFlag($machineUuid, 'locationinfo', false);
- ConfigHolder::add('SLX_AUTOLOGIN', '1', 1000);
- }
- if (isset($data['interactive']) && $data['interactive']) {
- ConfigHolder::add('SLX_BROWSER_INTERACTIVE', '1', 1000);
+ ConfigHolder::add('SLX_AUTOLOGIN', 'ON', 1000);
+ ConfigHolder::add('SLX_ADDONS', '', 1000);
}
if (!empty($data['browser'])) {
if ($data['browser'] === 'chromium') {
$browser = 'chromium chrome';
} else {
$browser = 'slxbrowser slx-browser';
+ $data['interactive'] = (isset($data['split-login']) && $data['split-login']);
}
ConfigHolder::add('SLX_BROWSER', $browser, 1000);
}
+ if (isset($data['interactive']) && $data['interactive']) {
+ ConfigHolder::add('SLX_BROWSER_INTERACTIVE', '1', 1000);
+ }
if (!empty($data['bookmarks'])) {
ConfigHolder::add('SLX_BROWSER_BOOKMARKS', $data['bookmarks'], 1000);
}
+ if ($data['allow-tty'] === 'yes' || $data['allow-tty'] === 'no') {
+ ConfigHolder::add('SLX_TTY_SWITCH', $data['allow-tty'], 1000);
+ }
+ if (($data['zoom-factor'] ?? 100) != 100) {
+ ConfigHolder::add('SLX_BROWSER_ZOOM', $data['zoom-factor']);
+ }
} else {
// Not URL panel
ConfigHolder::add('SLX_BROWSER_URL', 'http://' . $_SERVER['SERVER_ADDR'] . '/panel/' . $panelUuid);
- ConfigHolder::add('SLX_BROWSER_INSECURE', '1'); // TODO: Sat server might redirect to HTTPS, which in turn could have a self-signed cert - push to client
- ConfigHolder::add('SLX_AUTOLOGIN', '1', 1000);
+ ConfigHolder::add('SLX_AUTOLOGIN', 'ON', 1000);
+ ConfigHolder::add('SLX_ADDONS', '', 1000);
+ }
+ $al = ConfigHolder::get('SLX_AUTOLOGIN');
+ if (!empty($al) && $al !== 'OFF' && $al != 0) {
+ ConfigHolder::add('SLX_SHUTDOWN_TIMEOUT', '', 1000);
}
- ConfigHolder::add('SLX_ADDONS', '', 1000);
ConfigHolder::add('SLX_LOGOUT_TIMEOUT', '', 1000);
ConfigHolder::add('SLX_SCREEN_STANDBY_TIMEOUT', '', 1000);
ConfigHolder::add('SLX_SYSTEM_STANDBY_TIMEOUT', '', 1000);
}
+ /**
+ * Turn multiline list into space separated list, removing any
+ * comments (starting with #)
+ */
+ private static function mangleList(string $list): string
+ {
+ return preg_replace('/\s*(#[^\n]*)?(\n|$)/', ' ', $list);
+ }
+
} \ No newline at end of file