summaryrefslogtreecommitdiffstats
path: root/modules-available/locationinfo
diff options
context:
space:
mode:
Diffstat (limited to 'modules-available/locationinfo')
-rw-r--r--modules-available/locationinfo/api.inc.php85
-rw-r--r--modules-available/locationinfo/clientscript.js153
-rw-r--r--modules-available/locationinfo/config.json2
-rw-r--r--modules-available/locationinfo/exchange-includes/jamesiarmes/PhpEws/ArrayType/ArrayOfStringsType.php2
-rw-r--r--modules-available/locationinfo/exchange-includes/jamesiarmes/PhpEws/Autodiscover.php2
-rw-r--r--modules-available/locationinfo/exchange-includes/jamesiarmes/PhpNtlm/SoapClient.php5
-rw-r--r--modules-available/locationinfo/frontend/frontendscript.js3
-rw-r--r--modules-available/locationinfo/inc/coursebackend.inc.php94
-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.php53
-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.php39
-rw-r--r--modules-available/locationinfo/inc/locationinfo.inc.php121
-rw-r--r--modules-available/locationinfo/inc/locationinfohooks.inc.php47
-rw-r--r--modules-available/locationinfo/install.inc.php3
-rw-r--r--modules-available/locationinfo/lang/de/backend-hisinone.json10
-rw-r--r--modules-available/locationinfo/lang/de/backend-ical.json16
-rw-r--r--modules-available/locationinfo/lang/de/messages.json4
-rw-r--r--modules-available/locationinfo/lang/de/module.json10
-rw-r--r--modules-available/locationinfo/lang/de/template-tags.json37
-rw-r--r--modules-available/locationinfo/lang/en/backend-hisinone.json12
-rw-r--r--modules-available/locationinfo/lang/en/backend-ical.json16
-rw-r--r--modules-available/locationinfo/lang/en/messages.json4
-rw-r--r--modules-available/locationinfo/lang/en/module.json12
-rw-r--r--modules-available/locationinfo/lang/en/template-tags.json40
-rw-r--r--modules-available/locationinfo/page.inc.php381
-rw-r--r--modules-available/locationinfo/style.css12
-rw-r--r--modules-available/locationinfo/templates/ajax-config-location.html191
-rw-r--r--modules-available/locationinfo/templates/ajax-config-server.html77
-rwxr-xr-xmodules-available/locationinfo/templates/frontend-default.html96
-rw-r--r--modules-available/locationinfo/templates/frontend-summary.html10
-rw-r--r--modules-available/locationinfo/templates/page-config-panel-default.html500
-rw-r--r--modules-available/locationinfo/templates/page-config-panel-summary.html172
-rw-r--r--modules-available/locationinfo/templates/page-config-panel-url.html364
-rw-r--r--modules-available/locationinfo/templates/page-locations.html40
-rw-r--r--modules-available/locationinfo/templates/page-servers.html5
-rw-r--r--modules-available/locationinfo/templates/server-prop-bool.html12
-rw-r--r--modules-available/locationinfo/templates/server-prop-dropdown.html12
-rw-r--r--modules-available/locationinfo/templates/server-prop-generic.html12
44 files changed, 3733 insertions, 1859 deletions
diff --git a/modules-available/locationinfo/api.inc.php b/modules-available/locationinfo/api.inc.php
index d3ff9ebd..24919ba1 100644
--- a/modules-available/locationinfo/api.inc.php
+++ b/modules-available/locationinfo/api.inc.php
@@ -14,7 +14,7 @@ function HandleParameters()
$get = Request::get('get', 0, 'string');
$uuid = Request::get('uuid', false, 'string');
- $output = false;
+ $output = null;
if ($get === "timestamp") {
$output = array('ts' => getLastChangeTs($uuid));
} elseif ($get === "machines") {
@@ -24,7 +24,7 @@ function HandleParameters()
$output = array_values($output);
} elseif ($get === "config") {
$type = InfoPanel::getConfig($uuid, $output);
- if ($type === false) {
+ if ($type === null) {
http_response_code(404);
die('Panel not found');
}
@@ -36,9 +36,9 @@ function HandleParameters()
$output = getLocationTree($locationIds);
} elseif ($get === "calendar") {
$locationIds = LocationInfo::getLocationsOr404($uuid);
- $output = getCalendar($locationIds);
+ $output = LocationInfo::getCalendar($locationIds);
}
- if ($output !== false) {
+ if ($output !== null) {
Header('Content-Type: application/json; charset=utf-8');
echo json_encode($output);
} else {
@@ -59,7 +59,7 @@ function HandleParameters()
* @param string $paneluuid panels uuid
* @return int UNIX_TIMESTAMP
*/
-function getLastChangeTs($paneluuid)
+function getLastChangeTs(string $paneluuid): int
{
$panel = Database::queryFirst('SELECT lastchange, locationids FROM locationinfo_panel WHERE paneluuid = :paneluuid',
compact('paneluuid'));
@@ -84,7 +84,7 @@ function getLastChangeTs($paneluuid)
* @param int[] $idList list of the location ids.
* @return array aggregated PC states
*/
-function getPcStates($idList, $paneluuid)
+function getPcStates(array $idList, string $paneluuid): array
{
$pcStates = array();
foreach ($idList as $id) {
@@ -130,18 +130,17 @@ function getPcStates($idList, $paneluuid)
* @param int[] $idList Array list of the locations.
* @return array location tree data
*/
-function getLocationTree($idList)
+function getLocationTree(array $idList): array
{
if (in_array(0, $idList)) {
return array_values(Location::getTree());
}
$locations = Location::getTree();
- $ret = findLocations($locations, $idList);
- return $ret;
+ return findLocations($locations, $idList);
}
-function findLocations($locations, $idList)
+function findLocations(array $locations, array $idList): array
{
$ret = array();
foreach ($locations as $location) {
@@ -153,69 +152,3 @@ function findLocations($locations, $idList)
}
return $ret;
}
-
-// ########## <Calendar> ###########
-/**
- * Gets the calendar of the given ids.
- *
- * @param int[] $idList list with the location ids.
- * @return array Calendar.
- */
-function getCalendar($idList)
-{
- if (empty($idList))
- return [];
-
- // 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();
- while ($dbresult = $dbquery->fetch(PDO::FETCH_ASSOC)) {
- if (!isset($serverList[$dbresult['serverid']])) {
- $serverList[$dbresult['serverid']] = array(
- 'credentials' => json_decode($dbresult['credentials'], true),
- 'type' => $dbresult['servertype'],
- 'idlist' => array()
- );
- }
- $serverList[$dbresult['serverid']]['idlist'][] = $dbresult['locationid'];
- }
-
- $resultArray = array();
- 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' => $key,
- 'calendar' => $value,
- );
- }
- }
- }
- return $resultArray;
-}
-
-// ########## </Calendar> ##########
diff --git a/modules-available/locationinfo/clientscript.js b/modules-available/locationinfo/clientscript.js
deleted file mode 100644
index 25c255fb..00000000
--- a/modules-available/locationinfo/clientscript.js
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- * Generic helpers.
- */
-
-/**
- * Initialize timepicker on given element.
- */
-function setTimepicker($e) {
- $e.timepicker({
- minuteStep: 15,
- appendWidgetTo: 'body',
- showSeconds: false,
- showMeridian: false,
- defaultTime: false
- });
-}
-
-function getTime(str) {
- if (!str) return false;
- str = str.split(':');
- if (str.length !== 2) return false;
- var h = parseInt(str[0].replace(/^0/, ''));
- var m = parseInt(str[1].replace(/^0/, ''));
- if (h < 0 || h > 23) return false;
- if (m < 0 || m > 59) return false;
- return h * 60 + m;
-}
-
-const allDays = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
-
-/*
- * Opening times related...
- */
-
-var slxIdCounter = 0;
-
-/**
- * Adds a new opening time to the table in expert mode.
- */
-function newOpeningTime(vals) {
- var $row = $('#expert-template').find('div.row').clone();
- if (vals['days'] && Array.isArray(vals['days'])) {
- for (var i = 0; i < allDays.length; ++i) {
- $row.find('.i-' + allDays[i]).prop('checked', vals['days'].indexOf(allDays[i]) !== -1);
- }
- }
- $row.find('input').each(function() {
- var $inp = $(this);
- if ($inp.length === 0) return;
- slxIdCounter++;
- $inp.prop('id', 'id-inp-' + slxIdCounter);
- $inp.siblings('label').prop('for', 'id-inp-' + slxIdCounter);
- });
- $row.find('.i-openingtime').val(vals['openingtime']);
- $row.find('.i-closingtime').val(vals['closingtime']);
- $('#expert-table').append($row);
- return $row;
-}
-
-/**
- * Convert fields from simple mode view to entries in expert mode.
- * @returns {Array}
- */
-function simpleToExpert() {
- var retval = [];
- if ($('#week-open').val() || $('#week-close').val()) {
- retval.push({
- 'days': ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
- 'openingtime': $('#week-open').val(),
- 'closingtime': $('#week-close').val(),
- 'tag': '#week'
- });
- }
- if ($('#saturday-open').val() || $('#saturday-close').val()) {
- retval.push({
- 'days': ['Saturday'],
- 'openingtime': $('#saturday-open').val(),
- 'closingtime': $('#saturday-close').val(),
- 'tag': '#saturday'
- });
- }
- if ($('#sunday-open').val() || $('#sunday-close').val()) {
- retval.push({
- 'days': ['Sunday'],
- 'openingtime': $('#sunday-open').val(),
- 'closingtime': $('#sunday-close').val(),
- 'tag': '#sunday'
- });
- }
- return retval;
-}
-
-/**
- * Triggered when the form is submitted
- */
-function submitLocationSettings(event) {
- var schedule, s, e;
- var badFormat = false;
- $('#settings-outer').find('.red-bg').removeClass('red-bg');
- if ($('#week-open').length > 0) {
- schedule = simpleToExpert();
- for (var i = 0; i < schedule.length; ++i) {
- s = getTime(schedule[i].openingtime);
- e = getTime(schedule[i].closingtime);
- if (s === false) {
- $(schedule[i].tag + '-open').addClass('red-bg');
- badFormat = true;
- }
- if (e === false || e <= s) {
- $(schedule[i].tag + '-close').addClass('red-bg');
- badFormat = true;
- }
- }
- } else {
- // Serialize
- schedule = [];
- $('#expert-table').find('.expert-row').each(function () {
- var $t = $(this);
- if ($t.find('.i-delete').is(':checked')) return; // Skip marked as delete
- var entry = {
- 'days': [],
- 'openingtime': $t.find('.i-openingtime').val(),
- 'closingtime': $t.find('.i-closingtime').val()
- };
- for (var i = 0; i < allDays.length; ++i) {
- if ($t.find('.i-' + allDays[i]).is(':checked')) {
- entry['days'].push(allDays[i]);
- }
- }
- if (entry.openingtime.length === 0 && entry.closingtime.length === 0 && entry.days.length === 0) return; // Also ignore empty lines
- s = getTime(entry.openingtime);
- e = getTime(entry.closingtime);
- if (s === false) {
- $t.find('.i-openingtime').addClass('red-bg');
- badFormat = true;
- }
- if (e === false || e <= s) {
- $t.find('.i-closingtime').addClass('red-bg');
- badFormat = true;
- }
- if (entry.days.length === 0) {
- $t.find('.days-box').addClass('red-bg');
- badFormat = true;
- }
- if (badFormat) return;
- schedule.push(entry);
- });
- }
- if (badFormat) {
- event.preventDefault();
- }
- $('#json-openingtimes').val(JSON.stringify(schedule));
-} \ No newline at end of file
diff --git a/modules-available/locationinfo/config.json b/modules-available/locationinfo/config.json
index 15298ea1..4fa2859e 100644
--- a/modules-available/locationinfo/config.json
+++ b/modules-available/locationinfo/config.json
@@ -1,8 +1,6 @@
{
"category": "main.content",
"dependencies": [
- "js_jqueryui",
- "bootstrap_timepicker",
"locations",
"bootstrap_switch"
]
diff --git a/modules-available/locationinfo/exchange-includes/jamesiarmes/PhpEws/ArrayType/ArrayOfStringsType.php b/modules-available/locationinfo/exchange-includes/jamesiarmes/PhpEws/ArrayType/ArrayOfStringsType.php
index 6443d31d..28792929 100644
--- a/modules-available/locationinfo/exchange-includes/jamesiarmes/PhpEws/ArrayType/ArrayOfStringsType.php
+++ b/modules-available/locationinfo/exchange-includes/jamesiarmes/PhpEws/ArrayType/ArrayOfStringsType.php
@@ -32,6 +32,6 @@ class ArrayOfStringsType extends ArrayType
*/
public function __toString()
{
- return $this->String;
+ return implode(' + ', $this->String);
}
}
diff --git a/modules-available/locationinfo/exchange-includes/jamesiarmes/PhpEws/Autodiscover.php b/modules-available/locationinfo/exchange-includes/jamesiarmes/PhpEws/Autodiscover.php
index 8198137d..8c60a4c8 100644
--- a/modules-available/locationinfo/exchange-includes/jamesiarmes/PhpEws/Autodiscover.php
+++ b/modules-available/locationinfo/exchange-includes/jamesiarmes/PhpEws/Autodiscover.php
@@ -891,6 +891,6 @@ class Autodiscover
protected function tryViaUrl($url, $timeout = 6)
{
$result = $this->doNTLMPost($url, $timeout);
- return ($result ? true : false);
+ return (bool)$result;
}
}
diff --git a/modules-available/locationinfo/exchange-includes/jamesiarmes/PhpNtlm/SoapClient.php b/modules-available/locationinfo/exchange-includes/jamesiarmes/PhpNtlm/SoapClient.php
index 21c77cbf..98f23dfa 100644
--- a/modules-available/locationinfo/exchange-includes/jamesiarmes/PhpNtlm/SoapClient.php
+++ b/modules-available/locationinfo/exchange-includes/jamesiarmes/PhpNtlm/SoapClient.php
@@ -26,6 +26,9 @@ class SoapClient extends \SoapClient
*/
protected $options;
+ protected $__last_response;
+ protected $__last_request_headers;
+
/**
* {@inheritdoc}
*
@@ -68,7 +71,7 @@ class SoapClient extends \SoapClient
/**
* {@inheritdoc}
*/
- public function __doRequest($request, $location, $action, $version, $one_way = 0)
+ public function __doRequest($request, $location, $action, $version, $oneWay = 0)
{
$headers = $this->buildHeaders($action);
$this->__last_request = $request;
diff --git a/modules-available/locationinfo/frontend/frontendscript.js b/modules-available/locationinfo/frontend/frontendscript.js
index efe4d5b6..f39f2be8 100644
--- a/modules-available/locationinfo/frontend/frontendscript.js
+++ b/modules-available/locationinfo/frontend/frontendscript.js
@@ -67,6 +67,9 @@ function GetTimeDiferenceAsString(a, b, globalConfig) {
str += hours + 'h ';
}
str += minutes + 'min ';
+ if (globalConfig && !globalConfig.eco) {
+ str += seconds + 's ';
+ }
return str;
}
diff --git a/modules-available/locationinfo/inc/coursebackend.inc.php b/modules-available/locationinfo/inc/coursebackend.inc.php
index a66d35a3..ea1bebac 100644
--- a/modules-available/locationinfo/inc/coursebackend.inc.php
+++ b/modules-available/locationinfo/inc/coursebackend.inc.php
@@ -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 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 44847ce2..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 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 001f31ea..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']);
@@ -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, $withHostname = 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')) {
@@ -108,12 +105,14 @@ class InfoPanel
if ($withHostname) {
$extraCols .= 'm.hostname,';
}
- $query = "SELECT m.locationid, m.machineuuid, $extraCols m.logintime, m.lastseen, m.lastboot, m.state, m.currentrunmode FROM machine m
+ $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');
@@ -125,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'];
@@ -162,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
@@ -204,7 +204,6 @@ class InfoPanel
$currentId = $locations[$currentId]['parentlocationid'];
}
}
- return;
}
@@ -215,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']);
}
}
@@ -236,9 +235,9 @@ class InfoPanel
* '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 5a66f15a..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,
@@ -118,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 15c4dc19..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,14 +33,22 @@ 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);
+ ConfigHolder::add('SLX_AUTOLOGIN', 'ON', 1000);
ConfigHolder::add('SLX_ADDONS', '', 1000);
}
if (!empty($data['browser'])) {
@@ -61,16 +66,34 @@ class LocationInfoHooks
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_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
diff --git a/modules-available/locationinfo/install.inc.php b/modules-available/locationinfo/install.inc.php
index 7e58315b..42bc8234 100644
--- a/modules-available/locationinfo/install.inc.php
+++ b/modules-available/locationinfo/install.inc.php
@@ -6,7 +6,6 @@ $t1 = $res[] = tableCreate('locationinfo_locationconfig', '
`locationid` INT(11) NOT NULL,
`serverid` INT(10) UNSIGNED,
`serverlocationid` VARCHAR(150),
- `openingtime` BLOB,
`calendar` BLOB,
`lastcalendarupdate` INT(10) UNSIGNED NOT NULL DEFAULT 0,
`lastuse` INT(10) UNSIGNED NOT NULL DEFAULT 0,
@@ -53,7 +52,7 @@ if ($t1 === UPDATE_NOOP) {
Database::exec('INSERT INTO locationinfo_panel (paneluuid, panelname, locationids, paneltype, panelconfig, lastchange)'
. " SELECT UUID(), Concat('Import: ', l.locationname), o.locationid, 'DEFAULT', o.config, 0 "
. " FROM locationinfo_locationconfig o INNER JOIN location l USING (locationid)"
- . ' WHERE Length(o.config) > 10 OR Length(o.openingtime) > 10');
+ . ' WHERE Length(o.config) > 10');
}
Database::exec("ALTER TABLE locationinfo_locationconfig CHANGE `serverid` `serverid` INT(10) UNSIGNED NULL");
tableDropColumn('locationinfo_locationconfig', 'hidden');
diff --git a/modules-available/locationinfo/lang/de/backend-hisinone.json b/modules-available/locationinfo/lang/de/backend-hisinone.json
index 6ea1a933..5a2ad69f 100644
--- a/modules-available/locationinfo/lang/de/backend-hisinone.json
+++ b/modules-available/locationinfo/lang/de/backend-hisinone.json
@@ -1,14 +1,6 @@
{
"baseUrl": "Basis-URL",
- "baseUrl_helptext": "URL zur HisInOne-Installation",
- "open": "Service",
- "open_helptext": "Legt den zu verwendenden Web Service fest. OpenCourseService bietet anonymisierte Belegungspl\u00e4ne und erfordert keine Authentifizierung und ist i.d.R. bevorzugt.",
- "password": "Passwort",
- "password_helptext": "Das Passwort, das in HisInOne verwendet wird.",
- "role": "Rolle",
- "role_helptext": "Die Rolle die der Nutzername in HisInOne verwendet.",
- "username": "Nutzername",
- "username_helptext": "Der Nutzername, der in HisInOne verwendet wird.",
+ "baseUrl_helptext": "URL zur HisInOne-Installation, bzw. die URL, um einen Raumbelegungsplan als iCal-Datei herunterzuladen, z.B. \"https:\/\/<hisinoneserver>\/qisserver\/pages\/cm\/exa\/timetable\/roomScheduleCalendarExport.faces?roomId=\"",
"verifyCert": "Zertifikat pr\u00fcfen",
"verifyCert_helptext": "Wenn das Zertifikat abgelaufen ist, oder von keiner bekannten CA ausgestellt wurde, wird die Verbindung abgelehnt.",
"verifyHostname": "Hostnamen pr\u00fcfen",
diff --git a/modules-available/locationinfo/lang/de/backend-ical.json b/modules-available/locationinfo/lang/de/backend-ical.json
new file mode 100644
index 00000000..9a91eb9f
--- /dev/null
+++ b/modules-available/locationinfo/lang/de/backend-ical.json
@@ -0,0 +1,16 @@
+{
+ "authMethod": "Authentifizierung",
+ "authMethod_helptext": "Falls eine Authentifizierung per HTTP-Header erforderlich ist, kann hier die gew\u00fcnschte Methode gew\u00e4hlt werden.",
+ "baseUrl": "Basis-URL",
+ "baseUrl_helptext": "URL zum iCal-File f\u00fcr diesen Raum. Ersetzen Sie den Part, der den Raum identifiziert durch %ID%, z.B. \"http:\/\/example.com\/calendars\/%ID%\". Die spezifische ID tragen Sie dann in der Raum\u00fcbersicht f\u00fcr jeden Raum individuell ein.",
+ "pass": "Passwort",
+ "pass_helptext": "Optional. Passwort f\u00fcr Authentifizierung.",
+ "testId": "Testraum",
+ "testId_helptext": "Optional. Tragen Sie hier eine g\u00fcltige Raum-ID f\u00fcr dieses Backend ein, um diese f\u00fcr Verbondungs-checks zu nutzen (Klick auf blauen \"Verbindung pr\u00fcfen\" Button in Backend-\u00dcbersicht).",
+ "user": "Nutzername f\u00fcr Authentifizierung",
+ "user_helptext": "Optional. Nutzername f\u00fcr Authentifizierung.",
+ "verifyCert": "Zertifikat pr\u00fcfen",
+ "verifyCert_helptext": "Wenn das Zertifikat abgelaufen ist, oder von keiner bekannten CA ausgestellt wurde, wird die Verbindung abgelehnt.",
+ "verifyHostname": "Hostnamen pr\u00fcfen",
+ "verifyHostname_helptext": "Der im Zertifikat angegebene Hostname muss mit dem Hostnamen aus der URL \u00fcbereinstimmen, sonst wird die Verbindung abgelehnt."
+} \ No newline at end of file
diff --git a/modules-available/locationinfo/lang/de/messages.json b/modules-available/locationinfo/lang/de/messages.json
index bcaf687d..fe024d44 100644
--- a/modules-available/locationinfo/lang/de/messages.json
+++ b/modules-available/locationinfo/lang/de/messages.json
@@ -1,9 +1,5 @@
{
"config-saved": "Einstellungen erfolgreich gespeichert.",
- "ignored-invalid-end": "Eintrag mit ung\u00fcltiger Endzeit ignoriert",
- "ignored-invalid-range": "Eintrag mit ung\u00fcltiger Range ignoriert",
- "ignored-invalid-start": "Eintrag mit ung\u00fcltiger Startzeit ignoriert",
- "ignored-line-no-days": "Eintrag ohne ausgew\u00e4hlte Tage ignoriert",
"invalid-backend-type": "Ung\u00fcltiger Backend-Typ '{{0}}'",
"invalid-panel-id": "Ung\u00fcltige Panel-ID '{{0}}'",
"invalid-panel-type": "Ung\u00fcltiger Panel-Typ '{{0}}'",
diff --git a/modules-available/locationinfo/lang/de/module.json b/modules-available/locationinfo/lang/de/module.json
index c344581c..a285351e 100644
--- a/modules-available/locationinfo/lang/de/module.json
+++ b/modules-available/locationinfo/lang/de/module.json
@@ -1,3 +1,11 @@
{
- "module_name": "Infoscreen"
+ "friday": "Freitag",
+ "module_name": "Infoscreen",
+ "monday": "Montag",
+ "page_title": "Inforscreens und Rechercheterminals",
+ "saturday": "Samstag",
+ "sunday": "Sonntag",
+ "thursday": "Donnerstag",
+ "tuesday": "Dienstag",
+ "wednesday": "Mittwoch"
} \ No newline at end of file
diff --git a/modules-available/locationinfo/lang/de/template-tags.json b/modules-available/locationinfo/lang/de/template-tags.json
index 14bda6b4..fe6a3e53 100644
--- a/modules-available/locationinfo/lang/de/template-tags.json
+++ b/modules-available/locationinfo/lang/de/template-tags.json
@@ -1,9 +1,13 @@
{
"lang_addServer": "Server",
+ "lang_allowTtySwitch": "Wechsel auf Textkonsole erlauben",
+ "lang_allowTtySwitchTooltip": "Legt fest, ob ein wechsel auf die Textkonsole mittels Strg-Alt-F1 erlaubt ist",
"lang_autoScale": "Auto Tage",
"lang_autoscaleTooltip": "Berechnet anhand der Bildschirmbreite die optimale Anzahl an Tagen, die der Kalender anzeigt",
"lang_backend": "Backend",
+ "lang_backendSettings": "Backend konfigurieren",
"lang_backends": "Backends",
+ "lang_blacklist": "Blacklist",
"lang_bookmarks": "Lesezeichen",
"lang_bookmarksTooltip": "F\u00fcge Lesezeichen hinzu, die der Browser erhalten soll",
"lang_browser": "Browser",
@@ -13,14 +17,12 @@
"lang_checkConnection": "Verbindung pr\u00fcfen",
"lang_chromium": "Chromium",
"lang_closed": "Geschlossen",
- "lang_closingTime": "Schlie\u00dfungszeit",
"lang_configOverride": "Konfiguration \u00fcberschreiben",
"lang_countIp": "\u00fcber IP-Adressbereich",
"lang_countRoomplan": "\u00fcber Raumplaner",
"lang_createPanel": "Panel anlegen",
"lang_credentials": "Anmeldung",
"lang_currentDay": "Aktueller Tag",
- "lang_day": "Tag",
"lang_daysToShow": "Tage",
"lang_daysToShowTooltip": "Legt die Anzahl an Tagen im Kalender fest, die angezeigt werden",
"lang_defaultPanel": "Standard-Panel",
@@ -29,25 +31,26 @@
"lang_displayName": "Name",
"lang_displayNameTooltip": "Anzeigename f\u00fcr dieses Panel",
"lang_ecoMode": "E-Ink Modus",
- "lang_ecoTooltip": "Anstelle der Farb-basierten PC-Status Bilder, werden Symbol-basierte PC Bilder verwendet",
+ "lang_ecoTooltip": "Niedrigere Aktualisierungsrate, Countdown ohne Sekunden",
"lang_editDefaultPanelHints": "Hier k\u00f6nnen Sie ein Panel (z.B. digitales T\u00fcrschild) in Aussehen und Funktionsweise definieren. Um im Kalender \u00d6ffnungszeiten anzeigen zu k\u00f6nnen, m\u00fcssen Sie im Tab \"Raum-\/Ortsbezogene Einstellungen\" f\u00fcr den ausgew\u00e4hlten Raum entsprechend \u00d6ffnungszeiten eintragen. Damit im Kalender Veranstaltungen und andere Termine angezeigt werden k\u00f6nnen, muss ein funktionierendes Backend konfiguriert und den ausgew\u00e4hlten R\u00e4umen zugewiesen worden sein.",
"lang_editPanel": "Panel bearbeiten",
"lang_editSummaryPanelHints": "Hier k\u00f6nnen Sie ein \u00dcbersichts-Panel definieren. Das Panel zeigt eine \u00dcbersicht der in den R\u00e4umen enthalten PCs.",
"lang_editUrlPanelHints": "Hier k\u00f6nnen Sie konfigurieren, welche URL das Panel aufrufen soll. Dies erm\u00f6glicht Ihnen z.B. in Eingangsbereichen aktuelle Meldungen der Hochschule oder sonstige Webseiten anzuzeigen.",
"lang_entryName": "Name",
"lang_error": "Fehler",
- "lang_expertMode": "Expertenmodus",
"lang_for": "f\u00fcr",
"lang_fourLocsHint": "Hier k\u00f6nnen Sie bis zu vier Orte ausw\u00e4hlen, die in diesem Panel angezeigt werden.",
"lang_free": "Ge\u00f6ffnet",
"lang_friday": "Freitag",
"lang_general": "Allgemein",
"lang_generalSettings": "Allgemeine Einstellungen",
+ "lang_goToLocation": "Gehe zu Raum",
+ "lang_goToLocationWarning": "Weiterleitung zum Raum-Modul",
"lang_hostnameTooltip": "Zeige kurzen Hostnamen in Rechner-Piktogramm",
"lang_ignoreSslTooltip": "Akzeptiere ung\u00fcltige, abgelaufene oder selbstsignierte SSL-Zertifikate",
"lang_insecureSsl": "Unsicheres SSL",
"lang_interactive": "Interaktiver Browser",
- "lang_interactiveTooltip": "Aktivieren, um regul\u00e4res Surfen zuzulassen",
+ "lang_interactiveTooltip": "Volles UI anzeigen (tabs, bookmarks, ...)",
"lang_language": "Sprache",
"lang_languageTooltip": "Legt die Sprache der angezeigten Oberfl\u00e4che fest",
"lang_lastCalendarUpdate": "Kalender Update",
@@ -55,7 +58,7 @@
"lang_locationSettings": "Raum-\/Ortsbezogene Einstellungen",
"lang_locations": "Orte",
"lang_locationsTable": "R\u00e4ume \/ Orte",
- "lang_locationsTableHints": "Hier k\u00f6nnen Sie f\u00fcr die R\u00e4ume und Orte Ihrer Einrichtung \u00d6ffnungszeiten hinterlegen, sowie die Verkn\u00fcpfung mit Raum-IDs aus konfigurierten Backends (z.B. HISinOne) vornehmen, damit Belegungspl\u00e4ne abgerufen werden k\u00f6nnen.",
+ "lang_locationsTableHints": "Hier k\u00f6nnen Sie f\u00fcr die R\u00e4ume und Orte Ihrer Einrichtung die Verkn\u00fcpfung mit Raum-IDs aus konfigurierten Backends (z.B. HISinOne) vornehmen, damit Belegungspl\u00e4ne abgerufen werden k\u00f6nnen.",
"lang_locsHint": "Hier k\u00f6nnen Sie die Orte ausw\u00e4hlen, die in diesem Panel angezeigt werden.",
"lang_longFri": "Freitag",
"lang_longMon": "Montag",
@@ -71,7 +74,6 @@
"lang_mode3": "Raum",
"lang_mode4": "Wechselnd",
"lang_modeTooltip": "Die Anzeigemodi, welche das Frontend unterst\u00fctzt",
- "lang_monTilFr": "Montag - Freitag",
"lang_monday": "Montag",
"lang_nameTooltip": "Legt den Namen des Servers fest",
"lang_noLocationsWarning": "Bitte w\u00e4hlen Sie mindestens einen Ort aus, der vom Panel angezeigt werden soll.",
@@ -88,8 +90,8 @@
"lang_prettytimeTooltip": "Verwende ein anderes Anzeigeformat f\u00fcr die Uhrzeit",
"lang_recursiveServerSet": "Auch f\u00fcr alle untergeordneten R\u00e4ume setzen",
"lang_recursiveSetTooltip": "Wenn aktiviert, wird der Backend-Server auch f\u00fcr alle untergeordneten R\u00e4ume auf den hier gew\u00e4hlten Wert gesetzt",
- "lang_reloadIntervalMins": "Neuladen alle X Minuten",
- "lang_reloadIntervalTooltip": "Setzen Sie dieses Feld auf einen Wert > 0 (in Minuten), um die Seite auf den Clients regelm\u00e4\u00dfig neu zu laden. Feld auf 0 setzen oder leer lassen deaktiviert diese Funktion.",
+ "lang_reloadIntervalMins": "Neuladen der Startseite alle X Minuten (bei Inaktivit\u00e4t)",
+ "lang_reloadIntervalTooltip": "Setzen Sie dieses Feld auf einen Wert > 0 (in Minuten), um die Seite auf den Clients regelm\u00e4\u00dfig neu zu laden. Der Z\u00e4hler wird bei Nutzeraktivit\u00e4t (Maus\/Tastatur) zur\u00fcckgesetzt, sodass nicht mitten in einer Sitzung zur\u00fcck auf die Startseite navigiert wird. Feld auf 0 setzen oder leer lassen deaktiviert diese Funktion.",
"lang_remoteSchedule": "Abruf Belegungsplan",
"lang_room": "Raum",
"lang_roomId": "Raum ID",
@@ -114,19 +116,12 @@
"lang_serverTooltip": "Legt fest, von welchem Backend-Server die Kalenderdaten bezogen werden",
"lang_serverType": "Typ",
"lang_shortFri": "Fr",
- "lang_shortFriday": "Fr",
"lang_shortMon": "Mo",
- "lang_shortMonday": "Mo",
"lang_shortSat": "Sa",
- "lang_shortSaturday": "Sa",
"lang_shortSun": "So",
- "lang_shortSunday": "So",
"lang_shortThu": "Do",
- "lang_shortThursday": "Do",
"lang_shortTue": "Di",
- "lang_shortTuesday": "Di",
"lang_shortWed": "Mi",
- "lang_shortWednesday": "Mi",
"lang_showHostname": "Hostname anzeigen",
"lang_showLog": "Log",
"lang_slxbrowser": "SLX Browser",
@@ -145,14 +140,16 @@
"lang_typeTooltip": "Legt fest um welchen Server-Typ es sich handelt",
"lang_updateRates": "Aktualisierungsintervall",
"lang_url": "URL",
- "lang_urlBlacklist": "Blacklist",
- "lang_urlListHelp": "Sie k\u00f6nnen hier eine Liste von URLs oder Hostnamen angeben, die dann entweder als Whitelist oder Blacklist interpretiert wird. Unterst\u00fctzt werden die Sonderzeichen '?' (ein beliebiges Zeichen), '*' (beliebig viele Zeichen, au\u00dfer '\/') und '**' (beliebig viele Zeichen, inkl. '\/') als Platzhalter. Beispielangaben sind \"*.wikipedia.org\", \"https:\/\/www.bwlehrpool.de\/**\" oder \"*:\/\/*.uni-freiburg.de\/*.html\".",
+ "lang_urlListHelp": "Sie k\u00f6nnen hier Listen von URLs oder Hostnamen angeben, die dann als Whitelist bzw. Blacklist interpretiert werden. Je nach gew\u00e4hltem Browser hat entweder die Whitelist Vorrang, oder die \"genauere\" Regel. Wenn keine Regel zutrifft, wird der Zugriff erlaubt, es sei denn, es gibt den Eintrag \"*\" in der Blacklist. Je nach verwendetem Browser wird \"*\" als Wildcard an verschiedenen Stellen unterst\u00fctzt. Sie k\u00f6nnen Kommentare hinter oder zwischen den Zeilen mittels \"#\" einleiten. Weitere Informationen finden Sie im bwLehrpool-Wiki.",
"lang_urlPanel": "URL-Panel",
"lang_urlTooltip": "URL die aufgerufen wird",
- "lang_urlWhitelist": "Whitelist",
+ "lang_useDefault": "Vorgabe f\u00fcr Raum\/Rechner verwenden",
"lang_useRoomplanner": "Rechner z\u00e4hlen",
"lang_vertical": "Vertikaler Modus",
"lang_verticalTooltip": "Legt fest, ob Kalender und Raum \u00fcbereinander angezeigt werden sollen",
"lang_wednesday": "Mittwoch",
- "lang_when": "Wann"
+ "lang_when": "Wann",
+ "lang_whitelist": "Whitelist",
+ "lang_zoomFactor": "Zoom-Faktor",
+ "lang_zoomFactorTooltip": "Initialer Zoom-Faktor beim Start des Panels. Je nach gew\u00e4hltem Browser kann der Faktor vom Benutzer angepasst werden."
} \ No newline at end of file
diff --git a/modules-available/locationinfo/lang/en/backend-hisinone.json b/modules-available/locationinfo/lang/en/backend-hisinone.json
index 616b4c83..ee44a62e 100644
--- a/modules-available/locationinfo/lang/en/backend-hisinone.json
+++ b/modules-available/locationinfo/lang/en/backend-hisinone.json
@@ -1,16 +1,8 @@
{
"baseUrl": "Base URL",
- "baseUrl_helptext": "URL to HisInOne installation",
- "open": "Service",
- "open_helptext": "Sets the Web Service to use. OpenCourseService is anonymized and doesn't require authentication, so it's usually the preferred way.",
- "password": "Password",
- "password_helptext": "Account password. Only required if using CourseService",
- "role": "Role",
- "role_helptext": "Role of the user accessing the CourseService.",
- "username": "Username",
- "username_helptext": "Authenticating user (only required for CourseService).",
+ "baseUrl_helptext": "URL to HisInOne installation, or more precisely the URL to download a room's events as an iCal file, usually something like \"https:\/\/<hisinoneserver>\/qisserver\/pages\/cm\/exa\/timetable\/roomScheduleCalendarExport.faces?roomId=\"",
"verifyCert": "Verify certificate",
"verifyCert_helptext": "If the certificate expired or was not signed by a known CA, the connection will be aborted.",
"verifyHostname": "Verify host name",
"verifyHostname_helptext": "The certificate's host name must match the host name given in the URL, otherwise the connection will be aborted."
-}
+} \ No newline at end of file
diff --git a/modules-available/locationinfo/lang/en/backend-ical.json b/modules-available/locationinfo/lang/en/backend-ical.json
new file mode 100644
index 00000000..a5b26efd
--- /dev/null
+++ b/modules-available/locationinfo/lang/en/backend-ical.json
@@ -0,0 +1,16 @@
+{
+ "authMethod": "Athentication",
+ "authMethod_helptext": "If backend requires authentication via HTTP header, select appropriate method here.",
+ "baseUrl": "Base URL",
+ "baseUrl_helptext": "URL to iCal file for this room. Replace the part of the URL that identifies a specific room by %ID%, f.i. \"http:\/\/example.com\/locations\/%ID%\". Then switch to the \"location-specific settings\" tab and add the according ID to each room.",
+ "pass": "Password",
+ "pass_helptext": "Optional. Password for authentication.",
+ "testId": "Test room",
+ "testId_helptext": "Optional. Provide a valid room id for this backend for use when clicking the blue \"check connection\" button in the backend list.",
+ "user": "Username",
+ "user_helptext": "Optional. Username for authentication.",
+ "verifyCert": "Verify certificate",
+ "verifyCert_helptext": "If the certificate expired or was not signed by a known CA, the connection will be aborted.",
+ "verifyHostname": "Verify host name",
+ "verifyHostname_helptext": "The certificate's host name must match the host name given in the URL, otherwise the connection will be aborted."
+} \ No newline at end of file
diff --git a/modules-available/locationinfo/lang/en/messages.json b/modules-available/locationinfo/lang/en/messages.json
index 5111a408..c459f1ee 100644
--- a/modules-available/locationinfo/lang/en/messages.json
+++ b/modules-available/locationinfo/lang/en/messages.json
@@ -1,9 +1,5 @@
{
"config-saved": "Config successfully saved.",
- "ignored-invalid-end": "Ignored entry with invalid end time",
- "ignored-invalid-range": "Ignored entry with invalid range",
- "ignored-invalid-start": "Ignored entry with invalid start time",
- "ignored-line-no-days": "Ignored entry with no days selected",
"invalid-backend-type": "Invalid backend type '{{0}}'",
"invalid-panel-id": "Invalid panel id '{{0}}'",
"invalid-panel-type": "Invalid panel type '{{0}}'",
diff --git a/modules-available/locationinfo/lang/en/module.json b/modules-available/locationinfo/lang/en/module.json
index 2fd14353..183b1877 100644
--- a/modules-available/locationinfo/lang/en/module.json
+++ b/modules-available/locationinfo/lang/en/module.json
@@ -1,3 +1,11 @@
{
- "module_name": "Infoscreen"
-}
+ "friday": "Friday",
+ "module_name": "Infoscreen",
+ "monday": "Monday",
+ "page_title": "Info screens and research terminals",
+ "saturday": "Saturday",
+ "sunday": "Sunday",
+ "thursday": "Thursday",
+ "tuesday": "Tuesday",
+ "wednesday": "Wednesday"
+} \ No newline at end of file
diff --git a/modules-available/locationinfo/lang/en/template-tags.json b/modules-available/locationinfo/lang/en/template-tags.json
index 505930b8..5f612d16 100644
--- a/modules-available/locationinfo/lang/en/template-tags.json
+++ b/modules-available/locationinfo/lang/en/template-tags.json
@@ -1,27 +1,28 @@
{
"lang_addServer": "Server",
+ "lang_allowTtySwitch": "Allow switching to text console",
+ "lang_allowTtySwitchTooltip": "Set whether the user can switch to the text console via Ctrl-Alt-F1",
"lang_autoScale": "Auto Days",
"lang_autoscaleTooltip": "Calculates the optimum amount of days to show from the display width",
"lang_backend": "Backend",
+ "lang_backendSettings": "Configure backend",
"lang_backends": "Backends",
+ "lang_blacklist": "Blacklist",
"lang_bookmarks": "Bookmarks",
"lang_bookmarksTooltip": "Add bookmarks to the browser",
"lang_browser": "Browser",
"lang_browserTooltip": "Which browser shall be used",
- "lang_calendar": "Calendar",
"lang_calendarUpdate": "Calendar Update",
"lang_calupdateTooltip": "Time the calendar queries for updates (in minutes)",
"lang_checkConnection": "Check connection",
"lang_chromium": "Chromium",
"lang_closed": "Closed",
- "lang_closingTime": "Closing time",
"lang_configOverride": "Configuration override",
"lang_countIp": "by IP-Range",
"lang_countRoomplan": "by Roomplanner",
"lang_createPanel": "Create panel",
"lang_credentials": "Login",
"lang_currentDay": "Current Day",
- "lang_day": "Day",
"lang_daysToShow": "Days",
"lang_daysToShowTooltip": "Defines the amount of days to show in the calendar",
"lang_defaultPanel": "Default panel",
@@ -30,33 +31,34 @@
"lang_displayName": "Name",
"lang_displayNameTooltip": "Display name for this panel",
"lang_ecoMode": "E-Ink mode",
- "lang_ecoTooltip": "Symbolic based pc state pictures are used instead of the colour based ones",
+ "lang_ecoTooltip": "Lower update rate, countdown doesn't show seconds",
"lang_editDefaultPanelHints": "Here you can define panel properties for e.g. a digital door sign. To show opening times for a room you need to define corresponding times in the settings.\r\nIf you want to show calendar events you have to define a functioning backend first and link it to corresponding rooms.",
"lang_editPanel": "Edit panel",
"lang_editSummaryPanelHints": "Here you can define a summary panel which shows a overview of clients in your locations.",
"lang_editUrlPanelHints": "Here you can define which URL is opened by the panel. This enables you to show news about your university or any other website.",
"lang_entryName": "Name",
"lang_error": "Error",
- "lang_expertMode": "Expert mode",
"lang_for": "for",
"lang_fourLocsHint": "You can pick up to four locations that will be shown in this panel.",
"lang_free": "Open",
"lang_friday": "Friday",
"lang_general": "General",
"lang_generalSettings": "General Settings",
+ "lang_goToLocation": "GoTo Location",
+ "lang_goToLocationWarning": "Forward to Location-Module",
"lang_hostnameTooltip": "Show simple hostname inside computer icon",
"lang_ignoreSslTooltip": "Accept invalid, expired or self-signed ssl certificates",
"lang_insecureSsl": "Insecure SSL",
"lang_interactive": "Interactive Browser",
- "lang_interactiveTooltip": "Activate to allow regular websurfing",
+ "lang_interactiveTooltip": "Show full browser UI (tabs, bookmarks, ...)",
"lang_language": "Language",
"lang_languageTooltip": "The language the frontend uses",
"lang_lastCalendarUpdate": "Calendar update",
"lang_locationName": "Name",
- "lang_locationSettings": "Location specific settings",
+ "lang_locationSettings": "Location-specific settings",
"lang_locations": "Locations",
"lang_locationsTable": "Rooms \/ Locations",
- "lang_locationsTableHints": "Here you can define opening times for your locations and link the location ID to a configured backend (e.g. HISinOne) to show calendar events.",
+ "lang_locationsTableHints": "Here you can link the location ID to a configured backend (e.g. HISinOne) to show calendar events.",
"lang_locsHint": "You can pick up the locations that will be shown in this panel.",
"lang_longFri": "Friday",
"lang_longMon": "Monday",
@@ -72,7 +74,6 @@
"lang_mode3": "Room",
"lang_mode4": "Switching",
"lang_modeTooltip": "The display modes the frontend supports",
- "lang_monTilFr": "Monday - Friday",
"lang_monday": "Monday",
"lang_nameTooltip": "Defines the name of the server",
"lang_noLocationsWarning": "Please select at least one location this panel should display.",
@@ -89,8 +90,8 @@
"lang_prettytimeTooltip": "Use a different display format for the time",
"lang_recursiveServerSet": "Also set for all child locations",
"lang_recursiveSetTooltip": "If checked, all direct and indirect child locations will be configured to use the backend server selected above",
- "lang_reloadIntervalMins": "Reload every X minutes",
- "lang_reloadIntervalTooltip": "Set this field to a value > 0 (in minutes) to reload the page periodically. Set to 0 or leave blank to disable.",
+ "lang_reloadIntervalMins": "Reload start URL every X minutes (when idle)",
+ "lang_reloadIntervalTooltip": "Set this field to a value > 0 (in minutes) to reload the page periodically. The idle timeout will reset on user input (mouse\/keyboard), so the page will not reset while a user is using the browser. Set to 0 or leave blank to disable any resets.",
"lang_remoteSchedule": "Time table retrieval",
"lang_room": "Room",
"lang_roomId": "Room ID",
@@ -115,19 +116,12 @@
"lang_serverTooltip": "Defines from which server the room queries the calendar data",
"lang_serverType": "Type",
"lang_shortFri": "Fri",
- "lang_shortFriday": "Fri",
"lang_shortMon": "Mon",
- "lang_shortMonday": "Mon",
"lang_shortSat": "Sat",
- "lang_shortSaturday": "Sat",
"lang_shortSun": "Sun",
- "lang_shortSunday": "Sun",
"lang_shortThu": "Thu",
- "lang_shortThursday": "Thu",
"lang_shortTue": "Tue",
- "lang_shortTuesday": "Tue",
"lang_shortWed": "Wed",
- "lang_shortWednesday": "Wed",
"lang_showHostname": "Show hostname",
"lang_showLog": "Log",
"lang_slxbrowser": "SLX Browser",
@@ -146,14 +140,16 @@
"lang_typeTooltip": "Defines on which type of server you want to connect to",
"lang_updateRates": "Update rates",
"lang_url": "URL",
- "lang_urlBlacklist": "Blacklist",
- "lang_urlListHelp": "You can specify a list of URLs or host names that will be used as either a blacklist or a whitelist. You can use the special characters '?' to represent any character, '*' to represent any number of characters excluding '\/', or '**' meaning any number of characters, including '\/'. Examples are \"*.wikipedia.org\", \"https:\/\/www.bwlehrpool.de\/**\" or \"*:\/\/*.uni-freiburg.de\/*.html\".",
+ "lang_urlListHelp": "You can specify lists of URLs or hostnames here, which are then interpreted as a whitelist or blacklist. Depending on the selected browser, either the whitelist or the \"more precise\" rule takes precedence. If no rule applies, access will be allowed unless there is an entry \"*\" in the blacklist. Depending on the browser used, \"*\" is supported as a wildcard in different places. You can introduce comments at the end of or between lines using \"#\". Further information can be found in the bwLehrpool-Wiki.",
"lang_urlPanel": "URL panel",
"lang_urlTooltip": "URL which is shown by the panel",
- "lang_urlWhitelist": "Whitelist",
+ "lang_useDefault": "Use setting from location\/machine",
"lang_useRoomplanner": "Count PCs",
"lang_vertical": "Vertical mode",
"lang_verticalTooltip": "Defines whether the room and calendar are shown above each other",
"lang_wednesday": "Wednesday",
- "lang_when": "When"
+ "lang_when": "When",
+ "lang_whitelist": "Whitelist",
+ "lang_zoomFactor": "Zoom level",
+ "lang_zoomFactorTooltip": "Initial zoom level at startup. Depending on selected browser, this can later be changed by the user."
} \ No newline at end of file
diff --git a/modules-available/locationinfo/page.inc.php b/modules-available/locationinfo/page.inc.php
index 0885f446..63a02ba2 100644
--- a/modules-available/locationinfo/page.inc.php
+++ b/modules-available/locationinfo/page.inc.php
@@ -81,7 +81,7 @@ class Page_LocationInfo extends Page
$this->showLocationsTable();
break;
case 'backends':
- $this->showBackendsTable($backends);
+ $this->showBackendsTable($backends ?? []);
break;
case 'edit-panel':
$this->showPanelConfig();
@@ -100,7 +100,7 @@ class Page_LocationInfo extends Page
/**
* Deletes the server from the db.
*/
- private function deleteServer($id)
+ private function deleteServer($id): void
{
User::assertPermission('backend.edit');
if ($id === 0) {
@@ -113,7 +113,7 @@ class Page_LocationInfo extends Page
}
}
- private function deletePanel()
+ private function deletePanel(): void
{
$id = Request::post('uuid', false, 'string');
if ($id === false) {
@@ -130,25 +130,27 @@ class Page_LocationInfo extends Page
}
}
- private function getTime($str)
+ private static function getTime(string $str): ?int
{
$str = explode(':', $str);
- if (count($str) !== 2) return false;
- if ($str[0] < 0 || $str[0] > 23 || $str[1] < 0 || $str[1] > 59) return false;
+ if (count($str) !== 2)
+ return null;
+ if ($str[0] < 0 || $str[0] > 23 || $str[1] < 0 || $str[1] > 59)
+ return null;
return $str[0] * 60 + $str[1];
}
- private function writeLocationConfig()
+ private function writeLocationConfig(): void
{
// Check locations
$locationid = Request::post('locationid', false, 'int');
if ($locationid === false) {
Message::addError('main.parameter-missing', 'locationid');
- return false;
+ return;
}
if (Location::get($locationid) === false) {
Message::addError('location.invalid-location-id', $locationid);
- return false;
+ return;
}
User::assertPermission('location.edit', $locationid);
@@ -167,101 +169,20 @@ class Page_LocationInfo extends Page
$ignoreServer = 0;
}
- // Opening times
- $openingtimes = Request::post('openingtimes', '', 'string');
- if ($openingtimes !== '') {
- $openingtimes = json_decode($openingtimes, true);
- if (!is_array($openingtimes)) {
- $openingtimes = '';
- } else {
- $mangled = array();
- foreach (array_keys($openingtimes) as $key) {
- $entry = $openingtimes[$key];
- if (!isset($entry['days']) || !is_array($entry['days']) || empty($entry['days'])) {
- Message::addError('ignored-line-no-days');
- continue;
- }
- $s = $this->getTime($entry['openingtime']);
- $e = $this->getTime($entry['closingtime']);
- if ($s === false) {
- Message::addError('ignored-invalid-start', $entry['openingtime']);
- continue;
- }
- if ($e === false) {
- Message::addError('ignored-invalid-end', $entry['closingtime']);
- continue;
- }
- if ($e <= $s) {
- Message::addError('ignored-invalid-range', $entry['openingtime'], $entry['closingtime']);
- continue;
- }
- unset($entry['tag']);
- $mangled[] = $entry;
- }
- if (empty($mangled)) {
- $openingtimes = null;
- } else {
- $openingtimes = json_encode($mangled);
- }
- }
- }
$NOW = time();
- // Check if openingtimes changed
- $res = Database::queryFirst('SELECT openingtime FROM locationinfo_locationconfig WHERE locationid = :locationid', compact('locationid'));
- $otChanged = $res === false || $res['openingtime'] !== $openingtimes;
- Database::exec("INSERT INTO `locationinfo_locationconfig` (locationid, serverid, serverlocationid, openingtime, lastcalendarupdate, lastchange)
- VALUES (:id, :insertserverid, :serverlocationid, :openingtimes, 0, :now)
+ Database::exec("INSERT INTO `locationinfo_locationconfig` (locationid, serverid, serverlocationid, lastcalendarupdate, lastchange)
+ VALUES (:id, :insertserverid, :serverlocationid, 0, :now)
ON DUPLICATE KEY UPDATE serverid = IF(:ignore_server AND serverid IS NULL, NULL, :serverid), serverlocationid = VALUES(serverlocationid),
- openingtime = VALUES(openingtime), lastcalendarupdate = 0, lastchange = VALUES(lastchange)", array(
+ lastcalendarupdate = 0, lastchange = VALUES(lastchange)", array(
'id' => $locationid,
'insertserverid' => $insertServerId,
'serverid' => $serverid,
- 'openingtimes' => $openingtimes,
'serverlocationid' => $serverlocationid,
'ignore_server' => $ignoreServer,
'now' => $NOW,
));
- if ($otChanged) {
- $tree = Location::getLocationsAssoc();
- $todo = array();
- $done = array();
- foreach ($tree as $l) {
- if ($l['parentlocationid'] == $locationid) {
- $todo[] = $l['locationid'];
- }
- }
- while (!empty($todo)) {
- $loc = array_pop($todo);
- if (in_array($loc, $done))
- continue;
- $done[] = $loc;
- // See if this one inherits
- $res = Database::queryFirst('SELECT openingtime FROM locationinfo_locationconfig WHERE locationid = :loc', compact('loc'));
- if ($res === false) {
- $res = Database::exec('INSERT INTO locationinfo_locationconfig (locationid, lastchange)
- VALUES (:locationid, :now) ON DUPLICATE KEY UPDATE lastchange = :now',
- array('locationid' => $loc, 'now' => $NOW));
- } elseif (strlen($res['openingtime']) < 5) {
- $res = Database::exec('UPDATE locationinfo_locationconfig SET lastchange = :now, openingtime = NULL
- WHERE locationid = :locationid',
- array('locationid' => $loc, 'now' => $NOW));
- } else {
- $res = 0;
- }
- if ($res > 0) {
- // Row was updated, which means the openingtime column was empty, which means the openingtime is inherited, descend further
- $todo = array_merge($todo, $tree[$loc]['children']);
- foreach ($tree as $l) {
- if ($l['parentlocationid'] == $loc) {
- $todo[] = $l['locationid'];
- }
- }
- }
- }
- }
-
if ($changeServerRecursive) {
// Recursive overwriting of serverid
$children = Location::getRecursiveFlat($locationid);
@@ -279,29 +200,23 @@ class Page_LocationInfo extends Page
));
}
}
-
- return true;
}
/**
* Get all location ids from the locationids parameter, which is comma separated, then split
- * and remove any ids that don't exist. The cleaned list will be returned
+ * and remove any ids that don't exist. The cleaned list will be returned.
+ * Will show error and redirect to main page if parameter is missing
+ *
* @param bool $failIfEmpty Show error and redirect to main page if parameter is missing or list is empty
* @return array list of locations from parameter
*/
- private function getLocationIdsFromRequest($failIfEmpty)
+ private function getLocationIdsFromRequest(): array
{
- $locationids = Request::post('locationids', false, 'string');
- if ($locationids === false) {
- if (!$failIfEmpty)
- return array();
- Message::addError('main.parameter-missing', 'locationids');
- Util::redirect('?do=locationinfo');
- }
+ $locationids = Request::post('locationids', Request::REQUIRED_EMPTY, 'string');
$locationids = explode(',', $locationids);
$all = array_map(function ($item) { return $item['locationid']; }, Location::queryLocations());
$locationids = array_filter($locationids, function ($item) use ($all) { return in_array($item, $all); });
- if ($failIfEmpty && empty($locationids)) {
+ if (empty($locationids)) {
Message::addError('main.parameter-empty', 'locationids');
Util::redirect('?do=locationinfo');
}
@@ -311,7 +226,7 @@ class Page_LocationInfo extends Page
/**
* Updated the config in the db.
*/
- private function writePanelConfig()
+ private function writePanelConfig(): void
{
// UUID - existing or new
$paneluuid = Request::post('uuid', false, 'string');
@@ -334,7 +249,7 @@ class Page_LocationInfo extends Page
}
// Permission
- $this->assertPanelPermission($paneluuid, 'panel.edit', $params['locationids']);
+ $this->assertPanelPermission($paneluuid, 'panel.edit', $params['locationids'] ?? []);
if ($paneluuid === 'new') {
$paneluuid = Util::randomUuid();
@@ -357,10 +272,13 @@ class Page_LocationInfo extends Page
Util::redirect('?do=locationinfo');
}
- private function preparePanelConfigDefault()
+ /**
+ * @return array{config: array, locationids: array}
+ */
+ private function preparePanelConfigDefault(): array
{
// Check locations
- $locationids = self::getLocationIdsFromRequest(true);
+ $locationids = self::getLocationIdsFromRequest();
if (count($locationids) > 4) {
$locationids = array_slice($locationids, 0, 4);
}
@@ -402,7 +320,7 @@ class Page_LocationInfo extends Page
'daystoshow' => Request::post('override'.$locationids[$i].'daystoshow', 7, 'int'),
'rotation' => Request::post('override'.$locationids[$i].'rotation', 0, 'int'),
'scale' => Request::post('override'.$locationids[$i].'scale', 50, 'int'),
- 'switchtime' => Request::post('override'.$locationids[$i].'switchtime', 60, 'int')
+ 'switchtime' => Request::post('override'.$locationids[$i].'switchtime', 60, 'int'),
);
$overrides[$locationids[$i]] = $overrideArray;
}
@@ -412,7 +330,10 @@ class Page_LocationInfo extends Page
return array('config' => $conf, 'locationids' => $locationids);
}
- private function preparePanelConfigUrl()
+ /**
+ * @return array{config: array, locationids: array}
+ */
+ private function preparePanelConfigUrl(): array
{
$bookmarkNames = Request::post('bookmarkNames', [], 'array');
$bookmarkUrls = Request::post('bookmarkUrls', [], 'array');
@@ -430,17 +351,22 @@ class Page_LocationInfo extends Page
'url' => Request::post('url', 'https://www.bwlehrpool.de/', 'string'),
'insecure-ssl' => Request::post('insecure-ssl', 0, 'int'),
'reload-minutes' => max(0, Request::post('reloadminutes', 0, 'int')),
- 'iswhitelist' => Request::post('iswhitelist', 0, 'int'),
- 'urllist' => preg_replace("/[\r\n\\s]+/ms", ' ', Request::post('urllist', '', 'string')),
+ 'whitelist' => preg_replace("/[\r\n]+/m", "\n", Request::post('whitelist', '', 'string')),
+ 'blacklist' => preg_replace("/[\r\n]+/m", "\n", Request::post('blacklist', '', 'string')),
'split-login' => Request::post('split-login', 0, 'bool'),
'browser' => Request::post('browser', 'slx-browser', 'string'),
'interactive' => Request::post('interactive', '0', 'bool'),
- 'bookmarks' => $bookmarkString ? $bookmarkString : '',
+ 'bookmarks' => $bookmarkString ?: '',
+ 'allow-tty' => Request::post('allow-tty', '', 'string'),
+ 'zoom-factor' => Request::post('zoom-factor', 100, 'int'),
);
return array('config' => $conf, 'locationids' => []);
}
- private function preparePanelConfigSummary()
+ /**
+ * @return array{config: array, locationids: array}
+ */
+ private function preparePanelConfigSummary(): array
{
// Build json structure
$conf = array(
@@ -453,14 +379,14 @@ class Page_LocationInfo extends Page
$conf['panelupdate'] = 15;
}
// Check locations
- $locationids = self::getLocationIdsFromRequest(true);
+ $locationids = self::getLocationIdsFromRequest();
return array('config' => $conf, 'locationids' => $locationids);
}
/**
* Updates the server settings in the db.
*/
- private function updateServerSettings()
+ private function updateServerSettings(): void
{
User::assertPermission('backend.edit');
$serverid = Request::post('id', -1, 'int');
@@ -502,10 +428,10 @@ class Page_LocationInfo extends Page
*
* @param int $id Server id which connection should be checked.
*/
- private function checkConnection($serverid = 0)
+ private function checkConnection(int $serverid = 0): void
{
if ($serverid === 0) {
- Util::traceError('checkConnection called with no server id');
+ ErrorHandler::traceError('checkConnection called with no server id');
}
User::assertPermission('backend.check');
@@ -518,7 +444,8 @@ class Page_LocationInfo extends Page
LocationInfo::setServerError($serverid, 'Unknown backend type: ' . $dbresult['servertype']);
return;
}
- $credentialsOk = $serverInstance->setCredentials($serverid, json_decode($dbresult['credentials'], true));
+ $credentialsOk = $serverInstance->setCredentials($serverid,
+ (array)json_decode($dbresult['credentials'], true));
if ($credentialsOk) {
$serverInstance->checkConnection();
@@ -527,7 +454,7 @@ class Page_LocationInfo extends Page
LocationInfo::setServerError($serverid, $serverInstance->getErrors());
}
- private function loadBackends()
+ private function loadBackends(): array
{
// Get a list of all the backend types.
$servertypes = array();
@@ -539,7 +466,7 @@ class Page_LocationInfo extends Page
// Build list of defined backends
$serverlist = array();
$dbquery2 = Database::simpleQuery("SELECT * FROM `locationinfo_coursebackend` ORDER BY servername ASC");
- while ($row = $dbquery2->fetch(PDO::FETCH_ASSOC)) {
+ foreach ($dbquery2 as $row) {
if (isset($servertypes[$row['servertype']])) {
$row['typename'] = $servertypes[$row['servertype']];
} else {
@@ -565,7 +492,7 @@ class Page_LocationInfo extends Page
/**
* Show the list of backends
*/
- private function showBackendsTable($serverlist)
+ private function showBackendsTable(array $serverlist): void
{
User::assertPermission('backend.*');
$data = array(
@@ -576,7 +503,7 @@ class Page_LocationInfo extends Page
Render::addTemplate('page-servers', $data);
}
- private function showBackendLog()
+ private function showBackendLog(): void
{
$id = Request::get('serverid', false, 'int');
if ($id === false) {
@@ -592,7 +519,7 @@ class Page_LocationInfo extends Page
$server['list'] = [];
$res = Database::simpleQuery('SELECT dateline, message FROM locationinfo_backendlog
WHERE serverid = :id ORDER BY logid DESC LIMIT 100', ['id' => $id]);
- while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ foreach ($res as $row) {
$row['dateline_s'] = Util::prettyTime($row['dateline']);
$row['class'] = substr($row['message'], 0, 3) === '[F]' ? 'text-danger' : 'text-warning';
$row['message'] = Substr($row['message'], 3);
@@ -601,7 +528,7 @@ class Page_LocationInfo extends Page
Render::addTemplate('page-server-log', $server);
}
- private function showLocationsTable()
+ private function showLocationsTable(): void
{
$allowedLocations = User::getAllowedLocations('location.edit');
if (empty($allowedLocations)) {
@@ -611,11 +538,12 @@ class Page_LocationInfo extends Page
$locations = Location::getLocations(0, 0, false, true);
// Get hidden state of all locations
- $dbquery = Database::simpleQuery("SELECT li.locationid, li.serverid, li.serverlocationid, li.openingtime, li.lastcalendarupdate, cb.servertype, cb.servername
+ $dbquery = Database::simpleQuery("SELECT li.locationid, li.serverid, li.serverlocationid, loc.openingtime, li.lastcalendarupdate, cb.servertype, cb.servername
FROM `locationinfo_locationconfig` AS li
- LEFT JOIN `locationinfo_coursebackend` AS cb USING (serverid)");
+ LEFT JOIN `locationinfo_coursebackend` AS cb USING (serverid)
+ LEFT JOIN `location` AS loc USING (locationid)");
- while ($row = $dbquery->fetch(PDO::FETCH_ASSOC)) {
+ foreach ($dbquery as $row) {
$locid = (int)$row['locationid'];
if (!isset($locations[$locid]) || !in_array($locid, $allowedLocations))
continue;
@@ -626,6 +554,7 @@ class Page_LocationInfo extends Page
}
$locations[$locid] += array(
'openingGlyph' => $glyph,
+ 'strong' => $glyph === 'ok',
'backend' => $backend,
'lastCalendarUpdate' => Util::prettyTime($row['lastcalendarupdate']), // TODO
'backendMissing' => !CourseBackend::exists($row['servertype']),
@@ -654,11 +583,20 @@ class Page_LocationInfo extends Page
));
}
- private function showPanelsTable()
+ private function showPanelsTable(): void
{
$visibleLocations = User::getAllowedLocations('panel.list');
+ if (in_array(0, $visibleLocations)) {
+ $visibleLocations = true;
+ }
$editLocations = User::getAllowedLocations('panel.edit');
+ if (in_array(0, $editLocations)) {
+ $editLocations = true;
+ }
$assignLocations = USer::getAllowedLocations('panel.assign-client');
+ if (in_array(0, $assignLocations)) {
+ $assignLocations = true;
+ }
if (empty($visibleLocations)) {
Message::addError('main.no-permission');
return;
@@ -672,7 +610,7 @@ class Page_LocationInfo extends Page
}
$panels = array();
$locations = Location::getLocationsAssoc();
- while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ foreach ($res as $row) {
if ($row['paneltype'] === 'URL') {
$url = json_decode($row['panelconfig'], true)['url'];
$row['locations'] = $row['locationurl'] = $url;
@@ -681,14 +619,16 @@ class Page_LocationInfo extends Page
} else {
$lids = explode(',', $row['locationids']);
// Permissions
- if (!empty(array_diff($lids, $visibleLocations))) {
+ if ($visibleLocations !== true && !empty(array_diff($lids, $visibleLocations))) {
continue;
}
- $row['edit_disabled'] = !empty(array_diff($lids, $editLocations)) ? 'disabled' : '';
- $row['runmode_disabled'] = !empty(array_diff($lids, $assignLocations)) ? 'disabled' : '';
+ $row['edit_disabled'] = $editLocations !== true && !empty(array_diff($lids, $editLocations))
+ ? 'disabled' : '';
+ $row['runmode_disabled'] = $assignLocations !== true && !empty(array_diff($lids, $assignLocations))
+ ? 'disabled' : '';
// Locations
$locs = array_map(function ($id) use ($locations) {
- return isset($locations[$id]) ? $locations[$id]['locationname'] : $id;
+ return isset($locations[$id]) ? $locations[$id]['locationname'] : "<<deleted=$id>>";
}, $lids);
$row['locations'] = implode(', ', $locs);
}
@@ -727,7 +667,7 @@ class Page_LocationInfo extends Page
*
* @param int $id Serverid
*/
- private function ajaxServerSettings($id)
+ private function ajaxServerSettings(int $id): void
{
User::assertPermission('backend.edit');
$oldConfig = Database::queryFirst('SELECT servername, servertype, credentials
@@ -752,12 +692,13 @@ class Page_LocationInfo extends Page
);
$backend['credentials'] = $backendInstance->getCredentialDefinitions();
foreach ($backend['credentials'] as $cred) {
+ /* @var BackendProperty $cred */
if ($backend['active'] && isset($oldCredentials[$cred->property])) {
- $cred->initForRender($oldCredentials[$cred->property]);
+ $cred->initForRender($backendInstance->mangleProperty($cred->property, $oldCredentials[$cred->property]));
} else {
$cred->initForRender();
}
- $cred->title = Dictionary::translateFile('backend-' . $s, $cred->property, true);
+ $cred->title = Dictionary::translateFile('backend-' . $s, $cred->property);
$cred->helptext = Dictionary::translateFile('backend-' . $s, $cred->property . "_helptext");
$cred->credentialsHtml = Render::parse('server-prop-' . $cred->template, (array)$cred);
}
@@ -775,10 +716,13 @@ class Page_LocationInfo extends Page
*
* @param int $id id of the location
*/
- private function ajaxConfigLocation($id)
+ private function ajaxConfigLocation(int $id): void
{
User::assertPermission('location.edit', $id);
- $locConfig = Database::queryFirst("SELECT serverid, serverlocationid, openingtime FROM `locationinfo_locationconfig` WHERE locationid = :id", array('id' => $id));
+ $locConfig = Database::queryFirst("SELECT info.serverid, info.serverlocationid, loc.openingtime
+ FROM `locationinfo_locationconfig` AS info
+ LEFT JOIN `location` AS loc USING (locationid)
+ WHERE locationid = :id", array('id' => $id));
if ($locConfig !== false) {
$openingtimes = json_decode($locConfig['openingtime'], true);
} else {
@@ -796,7 +740,7 @@ class Page_LocationInfo extends Page
WHERE locationid IN (:locations) AND serverid IS NOT NULL", array('locations' => $chain));
$chain = array_flip($chain);
$best = false;
- while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ foreach ($res as $row) {
if ($best === false || $chain[$row['locationid']] < $chain[$best['locationid']]) {
$best = $row;
}
@@ -810,7 +754,7 @@ class Page_LocationInfo extends Page
// get Server / ID list
$res = Database::simpleQuery("SELECT serverid, servername FROM locationinfo_coursebackend ORDER BY servername ASC");
$serverList = array();
- while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ foreach ($res as $row) {
if ($row['serverid'] == $locConfig['serverid']) {
$row['selected'] = 'selected';
}
@@ -821,10 +765,8 @@ class Page_LocationInfo extends Page
'id' => $id,
'serverlist' => $serverList,
'serverlocationid' => $locConfig['serverlocationid'],
+ 'openingtimes' => $this->compressTimes($openingtimes),
);
- $data['expertMode'] = !$this->isSimpleMode($openingtimes);
- // !! isSimpleMode might have changed $openingtimes, so order is important here...
- $data['schedule_data'] = json_encode($openingtimes);
echo Render::parse('ajax-config-location', $data);
}
@@ -833,71 +775,85 @@ class Page_LocationInfo extends Page
* Checks if simple mode or expert mode is active.
* Tries to merge/compact the opening times schedule, and
* will actually modify the passed array iff it can be
- * transformed into easy opening times.
+ * transformed into simple opening times.
*
- * @param array $array of the saved openingtimes.
- * @return bool True if simple mode, false if expert mode
+ * @return array new optimized openingtimes
*/
- private function isSimpleMode(&$array)
+ private function compressTimes(array $array): array
{
if (empty($array))
- return true;
+ return [];
// Decompose by day
- $new = array();
+ $DAYLIST = array_flip(['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']);
+ $new = [];
foreach ($array as $row) {
- $s = $this->getTime($row['openingtime']);
- $e = $this->getTime($row['closingtime']);
- if ($s === false || $e === false || $e <= $s)
+ $s = Page_LocationInfo::getTime($row['openingtime']);
+ $e = Page_LocationInfo::getTime($row['closingtime']);
+ if ($s === null || $e === null || $e <= $s)
continue;
foreach ($row['days'] as $day) {
+ $day = $DAYLIST[$day] ?? -1;
+ if ($day === -1)
+ continue;
$this->addDay($new, $day, $s, $e);
}
}
- // Merge by timespan, but always keep saturday and sunday separate
- $merged = array();
+ // Merge by timespan
+ $merged = [];
foreach ($new as $day => $ranges) {
foreach ($ranges as $range) {
- if ($day === 'Saturday' || $day === 'Sunday') {
- $add = $day;
- } else {
- $add = '';
+ $range = $range[0] . '#' . $range[1];
+ if (!isset($merged[$range])) {
+ $merged[$range] = [];
}
- $key = '#' . $range[0] . '#' . $range[1] . '#' . $add;
- if (!isset($merged[$key])) {
- $merged[$key] = array();
- }
- $merged[$key][$day] = true;
+ $merged[$range][$day] = true;
}
}
- // Check if it passes as simple mode
- if (count($merged) > 3)
- return false;
- foreach ($merged as $days) {
- if (count($days) === 5) {
- $res = array_keys($days);
- $res = array_intersect($res, array("Monday", "Tuesday", "Wednesday", "Thursday", "Friday"));
- if (count($res) !== 5)
- return false;
- } elseif (count($days) === 1) {
- if (!isset($days['Saturday']) && !isset($days['Sunday'])) {
- return false;
- }
+ // Finally transform to display struct
+ $new = [];
+ foreach ($merged as $span => $days) {
+ $out = explode('#', $span);
+ $new[] = [
+ 'days' => $this->buildDaysString(array_keys($days)),
+ 'open' => sprintf('%02d:%02d', ($out[0] / 60), ($out[0] % 60)),
+ 'close' => sprintf('%02d:%02d', ($out[1] / 60), ($out[1] % 60)),
+ ];
+ }
+ return $new;
+ }
+
+ /**
+ * @param array $daysArray List of days, "Monday", "Tuesday" etc. Must not contain duplicates.
+ * @return string Human-readable representation of list of days
+ */
+ private function buildDaysString(array $daysArray): string
+ {
+ /* Dictionary::translate('monday') Dictionary::translate('tuesday') Dictionary::translate('wednesday')
+ * Dictionary::translate('thursday') Dictionary::translate('friday') Dictionary::translate('saturday')
+ * Dictionary::translate('sunday')
+ */
+ $DAYLIST = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
+ $output = [];
+ $first = $last = -1;
+ sort($daysArray);
+ $daysArray[] = -1; // One trailing element to enforce a flush
+ foreach ($daysArray as $day) {
+ if ($first === -1) {
+ $first = $last = $day;
+ } elseif ($last + 1 === $day) {
+ // Chain
+ $last++;
} else {
- return false;
+ $string = Dictionary::translate($DAYLIST[$first]);
+ if ($first !== $last) {
+ $string .= ($first + 1 === $last ? ",\xe2\x80\x89" : "\xe2\x80\x89-\xe2\x80\x89")
+ . Dictionary::translate($DAYLIST[$last]);
+ }
+ $output[] = $string;
+ $first = $last = $day;
}
}
- // Valid simple mode, finally transform back to what we know
- $new = array();
- foreach ($merged as $span => $days) {
- preg_match('/^#(\d+)#(\d+)#/', $span, $out);
- $new[] = array(
- 'days' => array_keys($days),
- 'openingtime' => floor($out[1] / 60) . ':' . ($out[1] % 60),
- 'closingtime' => floor($out[2] / 60) . ':' . ($out[2] % 60),
- );
- }
- $array = $new;
- return true;
+ return implode(', ', $output);
}
private function addDay(&$array, $day, $s, $e)
@@ -927,7 +883,7 @@ class Page_LocationInfo extends Page
// $start must lie before range start, otherwise we'd have hit the case above
$e = $current[1];
unset($array[$day][$key]);
- continue;
+ //continue;
}
}
$array[$day][] = array($s, $e);
@@ -935,10 +891,8 @@ class Page_LocationInfo extends Page
/**
* Ajax the config of a panel.
- *
- * @param $id Location ID
*/
- private function showPanelConfig()
+ private function showPanelConfig(): void
{
$id = Request::get('uuid', false, 'string');
if ($id === false) {
@@ -1044,19 +998,29 @@ class Page_LocationInfo extends Page
}
}
+ if (empty($config['blacklist']) && $config['whitelist'] === '*' && !empty($config['urllist'])) {
+ if ($config['iswhitelist']) {
+ $config['whitelist'] = str_replace(' ', "\n", $config['urllist']);
+ } else {
+ $config['blacklist'] = str_replace(' ', "\n", $config['urllist']);
+ }
+ }
+
Render::addTemplate('page-config-panel-url', array(
'new' => $id === 'new',
'uuid' => $id,
'panelname' => $panel['panelname'],
'url' => $config['url'],
+ 'zoom-factor' => $config['zoom-factor'],
'ssl_checked' => $config['insecure-ssl'] ? 'checked' : '',
'reloadminutes' => (int)$config['reload-minutes'],
- 'iswhitelist_' . $config['iswhitelist'] . '_checked' => 'checked',
- 'urllist' => str_replace(' ', "\r\n", $config['urllist']),
+ 'whitelist' => str_replace("\n", "\r\n", $config['whitelist']),
+ 'blacklist' => str_replace("\n", "\r\n", $config['blacklist']),
'split-login_checked' => $config['split-login'] ? 'checked' : '',
'browser' => $config['browser'],
'interactive_checked' => $config['interactive'] ? 'checked' : '',
'bookmarks' => $bookmarksArray,
+ 'allow-tty_' . $config['allow-tty'] . '_checked' => 'checked',
));
} else {
Render::addTemplate('page-config-panel-summary', array(
@@ -1073,7 +1037,7 @@ class Page_LocationInfo extends Page
}
}
- private function showPanel()
+ private function showPanel(): void
{
$uuid = Request::get('uuid', false, 'string');
if ($uuid === false) {
@@ -1081,7 +1045,7 @@ class Page_LocationInfo extends Page
die('Missing parameter uuid');
}
$type = InfoPanel::getConfig($uuid, $config);
- if ($type === false) {
+ if ($type === null) {
http_response_code(404);
die('Panel with given uuid not found');
}
@@ -1091,10 +1055,14 @@ class Page_LocationInfo extends Page
}
$data = array();
- preg_match('#^(.*)/#', $_SERVER['PHP_SELF'], $script);
- preg_match('#^([^?]+)/#', $_SERVER['REQUEST_URI'], $request);
+ preg_match('#^/(.*)/#', $_SERVER['PHP_SELF'], $script);
+ preg_match('#^/([^?]+)/#', $_SERVER['REQUEST_URI'], $request);
if ($script[1] !== $request[1]) {
- $data['dirprefix'] = $script[1] . '/';
+ // Working with server-side redirects
+ $data['api'] = 'api/';
+ } else {
+ // 1:1
+ $data['api'] = 'api.php?do=locationinfo&';
}
if ($type === 'DEFAULT') {
@@ -1104,7 +1072,7 @@ class Page_LocationInfo extends Page
'language' => $config['language'],
);
- die(Render::parse('frontend-default', $data, $module = false, $lang = $config['language']));
+ die(Render::parse('frontend-default', $data, null, $config['language']));
}
if ($type === 'SUMMARY') {
@@ -1116,7 +1084,7 @@ class Page_LocationInfo extends Page
'language' => $config['language'],
);
- die(Render::parse('frontend-summary', $data, $module = false, $lang = $config['language']));
+ die(Render::parse('frontend-summary', $data, null, $config['language']));
}
http_response_code(500);
@@ -1125,10 +1093,9 @@ class Page_LocationInfo extends Page
/**
* @param string|array $panelOrUuid UUID of panel, or array with keys paneltype and locationds
- * @param string $permission
- * @param null|int[] $additionalLocations
+ * @param int[] $additionalLocations
*/
- private function assertPanelPermission($panelOrUuid, $permission, $additionalLocations = null)
+ private function assertPanelPermission($panelOrUuid, string $permission, array $additionalLocations = null): void
{
if (is_array($panelOrUuid)) {
$panel = $panelOrUuid;
diff --git a/modules-available/locationinfo/style.css b/modules-available/locationinfo/style.css
index b5fffe75..dce47c42 100644
--- a/modules-available/locationinfo/style.css
+++ b/modules-available/locationinfo/style.css
@@ -1,10 +1,4 @@
-.btn-static {
- background-color: white;
- border: 1px solid lightgrey;
- cursor: default;
-}
-.btn-static:active{
- -moz-box-shadow: inset 0 0 0px white;
- -webkit-box-shadow: inset 0 0 0px white;
- box-shadow: inset 0 0 0px white;
+.spacebottop {
+ margin-bottom: 0;
+ margin-top: 0.5em;
}
diff --git a/modules-available/locationinfo/templates/ajax-config-location.html b/modules-available/locationinfo/templates/ajax-config-location.html
index 47d4ba8a..a5e7e45e 100644
--- a/modules-available/locationinfo/templates/ajax-config-location.html
+++ b/modules-available/locationinfo/templates/ajax-config-location.html
@@ -1,192 +1,59 @@
<input type="hidden" name="locationid" value="{{id}}">
-<div id="settings-outer">
- <h3>{{lang_openingTime}}</h3>
- {{^expertMode}}
- <div id="simple-mode">
-
- <div align="right">
- <a href="#" class="btn btn-default btn-sm" id="btn-show-expert">{{lang_expertMode}}</a>
- </div>
- <div class="clearfix"></div>
-
- <table class="table table-condensed" style="margin-bottom:0">
- <tr>
- <th>{{lang_day}}</th>
- <th>{{lang_openingTime}}</th>
- <th>{{lang_closingTime}}</th>
- </tr>
-
- <tr class="tablerow">
- <td>{{lang_monTilFr}}</td>
- <td>
- <div class="input-group bootstrap-timepicker">
- <span class="input-group-addon">
- <span class="glyphicon glyphicon-time"></span>
- </span>
- <input type="text" class="form-control timepicker2" id="week-open" pattern="[0-9]{1,2}:[0-9]{2}">
- </div>
- </td>
- <td>
- <div class="input-group bootstrap-timepicker">
- <span class="input-group-addon">
- <span class="glyphicon glyphicon-time"></span>
- </span>
- <input type="text" class="form-control timepicker2" id="week-close" pattern="[0-9]{1,2}:[0-9]{2}">
- </div>
- </td>
- </tr>
- <tr class="tablerow">
- <td>{{lang_saturday}}</td>
- <td>
- <div class="input-group bootstrap-timepicker">
- <span class="input-group-addon">
- <span class="glyphicon glyphicon-time"></span>
- </span>
- <input type="text" class="form-control timepicker2" id="saturday-open" pattern="[0-9]{1,2}:[0-9]{2}">
- </div>
- </td>
- <td>
- <div class="input-group bootstrap-timepicker">
- <span class="input-group-addon">
- <span class="glyphicon glyphicon-time"></span>
- </span>
- <input type="text" class="form-control timepicker2" id="saturday-close" pattern="[0-9]{1,2}:[0-9]{2}">
- </div>
- </td>
- </tr>
- <tr class="tablerow">
- <td>{{lang_sunday}}</td>
- <td>
- <div class="input-group bootstrap-timepicker">
- <span class="input-group-addon">
- <span class="glyphicon glyphicon-time"></span>
- </span>
- <input type="text" class="form-control timepicker2" id="sunday-open" pattern="[0-9]{1,2}:[0-9]{2}">
- </div>
- </td>
- <td>
- <div class="input-group bootstrap-timepicker">
- <span class="input-group-addon">
- <span class="glyphicon glyphicon-time"></span>
- </span>
- <input type="text" class="form-control timepicker2" id="sunday-close" pattern="[0-9]{1,2}:[0-9]{2}">
- </div>
- </td>
- </tr>
- </table>
- </div>
- {{/expertMode}}
-
- <div id="expert-mode" style="{{^expertMode}}display:none{{/expertMode}}">
- <div class="pull-right">
- <a class="btn btn-success btn-sm" id="new-openingtime">
- <span class="glyphicon glyphicon-plus-sign"></span>
- {{lang_openingTime}}
- </a>
- </div>
- <div class="clearfix"></div>
- <div id="expert-table">
- <div class="row">
- <div class="col-xs-9">{{lang_day}}</div>
- <div class="col-xs-3 text-right">{{lang_delete}}</div>
- <div class="col-sm-6">{{lang_openingTime}}</div>
- <div class="col-sm-6">{{lang_closingTime}}</div>
- </div>
- </div>
- </div>
+<h3>{{lang_openingTime}}</h3>
+<table class="table">
+ {{#openingtimes}}
+ <tr>
+ <td>{{days}}</td>
+ <td class="text-right">{{open}}&thinsp;-&thinsp;{{close}}</td>
+ </tr>
+ {{/openingtimes}}
+</table>
+
+<div class="row" style="margin-top: 20px; margin-right: 15px;">
+ <a class="pull-right" title="{{lang_goToLocationWarning}}" href='?do=locations#{{id}}'>
+ {{lang_goToLocation}}
+ <span class="glyphicon glyphicon-arrow-right"></span>
+ </a>
</div>
+<hr>
<h3>{{lang_remoteSchedule}}</h3>
<div class="row">
- <div class="col-sm-3">
+ <div class="col-sm-4">
<label for="backend-select">{{lang_backend}}</label>
</div>
- <div class="col-sm-7">
+ <div class="col-sm-8">
<select id="backend-select" class="form-control" name="serverid">
<option value="0">{{lang_noServer}}</option>
{{#serverlist}}
- <option value="{{serverid}}" {{selected}}>{{servername}}</option>
+ <option value="{{serverid}}" {{selected}}>{{servername}}</option>
{{/serverlist}}
</select>
</div>
- <div class="col-sm-2">
- <p class="btn btn-static helptext" title="{{lang_serverTooltip}}">
- <span class="glyphicon glyphicon-question-sign"></span>
- </p>
+ <div class="col-sm-12 small text-muted slx-smallspace">
+ {{lang_serverTooltip}}
</div>
</div>
<div class="row">
- <div class="col-sm-3"></div>
- <div class="col-sm-7">
+ <div class="col-sm-4"></div>
+ <div class="col-sm-8">
<div class="checkbox">
<input type="checkbox" name="recursive" id="recursive-check">
<label for="recursive-check">{{lang_recursiveServerSet}}</label>
</div>
</div>
- <div class="col-sm-2">
- <p class="btn btn-static helptext" title="{{lang_recursiveSetTooltip}}">
- <span class="glyphicon glyphicon-question-sign"></span>
- </p>
+ <div class="col-sm-12 small text-muted slx-smallspace">
+ {{lang_recursiveSetTooltip}}
</div>
</div>
<div class="row">
- <div class="col-sm-3">
+ <div class="col-sm-4">
<label for="roomid-input">{{lang_roomId}}</label>
</div>
- <div class="col-sm-7">
+ <div class="col-sm-8">
<input id="roomid-input" class="form-control" name="serverlocationid" id="serverlocationid" value="{{serverlocationid}}">
</div>
- <div class="col-sm-2">
- <p class="btn btn-static helptext" title="{{lang_roomIdTooltip}}">
- <span class="glyphicon glyphicon-question-sign"></span>
- </p>
+ <div class="col-sm-12 small text-muted slx-smallspace">
+ {{lang_roomIdTooltip}}
</div>
</div>
-
-<script type="application/javascript"><!--
- (function() {
-
- var scheduleData = {{{schedule_data}}};
-
- {{#expertMode}}
- for (var i = 0; i < scheduleData.length; ++i) {
- newOpeningTime(scheduleData[i]);
- }
- {{/expertMode}}
- {{^expertMode}}
- for (var i = 0; i < scheduleData.length; ++i) {
- if (scheduleData[i].days.length === 5) {
- $('#week-open').val(scheduleData[i]['openingtime']);
- $('#week-close').val(scheduleData[i]['closingtime']);
- } else if (scheduleData[i].days.length === 1 && scheduleData[i].days[0] === 'Saturday') {
- $('#saturday-open').val(scheduleData[i]['openingtime']);
- $('#saturday-close').val(scheduleData[i]['closingtime']);
- } else if (scheduleData[i].days.length === 1 && scheduleData[i].days[0] === 'Sunday') {
- $('#sunday-open').val(scheduleData[i]['openingtime']);
- $('#sunday-close').val(scheduleData[i]['closingtime']);
- }
- }
- {{/expertMode}}
-
- setTimepicker($('#settings-outer').find('.timepicker2'));
-
- $('p.helptext').tooltip();
-
- $('#new-openingtime').click(function (e) {
- e.preventDefault();
- setTimepicker(newOpeningTime({}).find('.timepicker2'));
- })
-
- $('#btn-show-expert').click(function (e) {
- e.preventDefault();
- scheduleData = simpleToExpert();
- for (var i = 0; i < scheduleData.length; ++i) {
- setTimepicker(newOpeningTime(scheduleData[i]).find('.timepicker2'));
- }
- $('#simple-mode').remove();
- $('#expert-mode').show();
- });
-
- })();
-
-//--></script>
diff --git a/modules-available/locationinfo/templates/ajax-config-server.html b/modules-available/locationinfo/templates/ajax-config-server.html
index 8c2cb3ba..c61927c0 100644
--- a/modules-available/locationinfo/templates/ajax-config-server.html
+++ b/modules-available/locationinfo/templates/ajax-config-server.html
@@ -1,43 +1,37 @@
<div class="panel panel-default">
<div class="panel-heading">{{lang_general}}</div>
- <div class="panel-body">
- <div class="list-group">
- <div class="list-group-item">
- <div class="row">
- <div class="col-md-3">
- <label>{{lang_entryName}}</label>
- </div>
- <div class="col-md-7">
- <input required class="form-control" name="name" type="text" value="{{name}}" id="name-input"
- form="form-{{currentbackend}}">
- </div>
- <div class="col-md-2">
- <p class="btn btn-static" title="{{lang_nameTooltip}}">
- <span class="glyphicon glyphicon-question-sign"></span>
- </p>
- </div>
+ <div class="list-group">
+ <div class="list-group-item">
+ <div class="row">
+ <div class="col-md-4">
+ <label>{{lang_entryName}}</label>
+ </div>
+ <div class="col-md-8">
+ <input required class="form-control" name="name" type="text" value="{{name}}" id="name-input"
+ form="form-{{currentbackend}}">
+ </div>
+ <div class="col-sm-12 small text-muted spacebottop">
+ {{lang_nameTooltip}}
</div>
</div>
- <div class="list-group-item">
- <div class="row">
- <div class="col-md-3">
- <label>{{lang_serverType}}</label>
- </div>
- <div class="col-md-7">
- <select class="form-control" onchange="servertype_changed(this.value)">
- {{#defaultBlank}}
- <option value="" selected>{{lang_pleaseSelect}}</option>
- {{/defaultBlank}}
- {{#backendList}}
- <option value="{{backendtype}}" {{#active}}selected{{/active}}>{{display}}</option>
- {{/backendList}}
- </select>
- </div>
- <div class="col-md-2">
- <p class="btn btn-static" id="help-type" title="{{lang_typeTooltip}}">
- <span class="glyphicon glyphicon-question-sign"></span>
- </p>
- </div>
+ </div>
+ <div class="list-group-item">
+ <div class="row">
+ <div class="col-md-4">
+ <label>{{lang_serverType}}</label>
+ </div>
+ <div class="col-md-8">
+ <select class="form-control" onchange="servertype_changed(this.value)">
+ {{#defaultBlank}}
+ <option value="" selected>{{lang_pleaseSelect}}</option>
+ {{/defaultBlank}}
+ {{#backendList}}
+ <option value="{{backendtype}}" {{#active}}selected{{/active}}>{{display}}</option>
+ {{/backendList}}
+ </select>
+ </div>
+ <div class="col-sm-12 small text-muted spacebottop">
+ {{lang_typeTooltip}}
</div>
</div>
</div>
@@ -57,12 +51,10 @@
<div class="panel panel-default">
<div class="panel-heading">{{lang_credentials}}</div>
- <div class="panel-body">
- <div class="list-group">
- {{#credentials}}
- {{{credentialsHtml}}}
- {{/credentials}}
- </div>
+ <div class="list-group">
+ {{#credentials}}
+ {{{credentialsHtml}}}
+ {{/credentials}}
</div>
</div>
</form>
@@ -92,7 +84,6 @@
currentBackend = value;
}
- $('p.btn[title]').tooltip();
$('#myModalSubmitButton').attr('form', 'form-' + currentBackend);
$('.settings-bs-switch').bootstrapSwitch({size:'small'});
diff --git a/modules-available/locationinfo/templates/frontend-default.html b/modules-available/locationinfo/templates/frontend-default.html
index 324ee559..cc62075e 100755
--- a/modules-available/locationinfo/templates/frontend-default.html
+++ b/modules-available/locationinfo/templates/frontend-default.html
@@ -26,8 +26,8 @@ optional:
<meta name="viewport" content="width=device-width, initial-scale=1.0" charset="utf-8">
<head>
<title>DoorSign</title>
- <link rel='stylesheet' type='text/css' href='{{dirprefix}}modules/js_jqueryui/style.css'/>
- <link rel='stylesheet' type='text/css' href='{{dirprefix}}modules/js_weekcalendar/style.css'/>
+ <link rel='stylesheet' type='text/css' href='modules/js_jqueryui/style.css'/>
+ <link rel='stylesheet' type='text/css' href='modules/js_weekcalendar/style.css'/>
<style type="text/css">
@@ -51,12 +51,21 @@ optional:
box-shadow: 0 0.1875rem 0.375rem rgba(0, 0, 0, 0.25);
margin-bottom: 4px;
width: 100%;
- display: flex;
- flex-wrap: nowrap;
- align-items: center;
- justify-content: space-between;
+ font-size: 25pt;
+ font-size: 1.8vw;
+ font-weight: bold;
}
+ .count-3 {
+ font-size: 16pt;
+ font-size: 1.2vw;
+ }
+
+ .count-1 {
+ font-size: 30pt;
+ font-size: 2.25vw;
+ }
+
.pull-left {
float: left;
}
@@ -66,15 +75,11 @@ optional:
}
.col {
- padding: 0 4px;
+ padding: 3px 5px 0;
color: white;
overflow: hidden;
- flex: 1 1 auto;
text-overflow: ellipsis;
- position: relative;
- display: flex;
- flex-direction: column;
- justify-content: center;
+ line-height: 106%;
}
.col-square {
@@ -90,6 +95,9 @@ optional:
text-align: center;
padding: 0;
overflow: visible;
+ display: flex;
+ justify-content: center;
+ align-items: center;
}
.count-1 .col-square {
@@ -119,30 +127,19 @@ optional:
z-index: 100;
}
- .header-font {
- font-size: 25pt;
- font-size: 1.8vw;
- font-weight: bold;
- padding: 10px;
- }
-
.nowrap {
white-space: nowrap;
overflow: hidden;
}
- .timer {
- color: #ddd;
- }
-
- .count-3 .header-font {
- font-size: 16pt;
- font-size: 1.2vw;
+ .location-name {
+ font-size: 80%;
+ font-weight: normal;
}
- .count-1 .header-font {
- font-size: 30pt;
- font-size: 2.25vw;
+ .timer {
+ color: #ddd;
+ font-size: 80%;
}
.seats-counter {
@@ -256,6 +253,7 @@ optional:
overflow: visible;
height: 0;
font-size: 10pt;
+ text-shadow: #000 1px 1px;
}
.BROKEN .screen-inner {
@@ -363,10 +361,10 @@ optional:
</style>
- <script type='text/javascript' src='{{dirprefix}}script/jquery.js'></script>
- <script type='text/javascript' src='{{dirprefix}}modules/js_jqueryui/clientscript.js'></script>
- <script type='text/javascript' src="{{dirprefix}}modules/js_weekcalendar/clientscript.js"></script>
- <script type='text/javascript' src='{{dirprefix}}modules/locationinfo/frontend/frontendscript.js'></script>
+ <script type='text/javascript' src='script/jquery.js'></script>
+ <script type='text/javascript' src='modules/js_jqueryui/clientscript.js'></script>
+ <script type='text/javascript' src="modules/js_weekcalendar/clientscript.js"></script>
+ <script type='text/javascript' src='modules/locationinfo/frontend/frontendscript.js'></script>
</head>
<body>
@@ -566,6 +564,8 @@ optional:
putInRange(config, 'rotation', 0, 3, 0);
}
+ var updateTimer = null;
+
/**
* generates the Room divs and calls the needed functions depending on the rooms mode
*/
@@ -603,16 +603,16 @@ optional:
$("body").append($loc);
room.$.container = $loc;
- room.$.locationName = $('<div>').addClass('col').addClass('header-font').addClass('pull-left');
+ room.$.locationName = $('<div>').addClass('col location-name nowrap');
room.$.currentEvent = $("<span>").addClass('nowrap');
- room.$.currentRemain = $("<span>").addClass('nowrap').addClass('timer');
+ room.$.currentRemain = $("<span>").addClass('nowrap timer');
room.$.seatsCounter = $('<span>').addClass('seats-counter');
room.$.seatsBackground = $('<div>').addClass('col col-square').append(room.$.seatsCounter);
- var $header = $('<div>').addClass('row').addClass('count-' + columns);
- $header.append(room.$.locationName);
+ var $header = $('<div>').addClass('row count-' + columns);
$header.append(room.$.seatsBackground);
- $header.append($('<div>').addClass('col header-font center').append(room.$.currentEvent).append(' ').append(room.$.currentRemain));
+ $header.append(room.$.locationName);
+ $header.append($('<div>').addClass('col center').append(room.$.currentEvent).append(' ').append(room.$.currentRemain));
room.$.header = $header;
$loc.append($header);
$header.append('<div class="clearfix">');
@@ -653,7 +653,7 @@ optional:
}
mainUpdateLoop();
- setInterval(mainUpdateLoop, 10000);
+ updateTimer = setInterval(mainUpdateLoop, 10000);
setInterval(updateHeaders, globalConfig.eco ? 10000 : 1000);
}
@@ -681,7 +681,14 @@ optional:
var today = date.getDate();
if (lastDate !== false) {
if (lastDate !== today) {
- location.reload(true);
+ if (updateTimer !== null) {
+ clearInterval(updateTimer);
+ updateTimer = null;
+ }
+ // Delay by a minute, sometimes the calendar shows the previous day if we load too quickly.
+ setTimeout(function() {
+ location.reload(true);
+ }, 60000);
}
} else {
lastDate = today;
@@ -950,7 +957,7 @@ optional:
*/
function queryCalendars() {
if (!panelUuid) return;
- var url = "{{dirprefix}}api.php?do=locationinfo&get=calendar&uuid=" + panelUuid;
+ var url = "{{{api}}}get=calendar&uuid=" + panelUuid;
$.ajax({
url: url,
dataType: 'json',
@@ -1211,7 +1218,7 @@ optional:
var newText = false, newTime = false;
var seats = room.freePcs;
if (tmp.state === 'closed' || tmp.state === 'CalendarEvent' || tmp.state === 'Free') {
- newTime = GetTimeDiferenceAsString(tmp.end, MyDate(), globalConfig);
+ newTime = GetTimeDiferenceAsString(tmp.end, MyDate(), room.config);
}
if (tmp.state === "closed") {
if (!same) newText = t("closed");
@@ -1328,7 +1335,6 @@ optional:
/========================================== Room Layout =============================================
*/
-
const picSizeX = 3.8;
const picSizeY = 3;
@@ -1502,7 +1508,7 @@ optional:
*/
function queryPanelChange() {
$.ajax({
- url: "{{dirprefix}}api.php?do=locationinfo&get=timestamp&uuid=" + panelUuid,
+ url: "{{{api}}}get=timestamp&uuid=" + panelUuid,
dataType: 'json',
cache: false,
timeout: 5000,
@@ -1525,7 +1531,7 @@ optional:
*/
function queryRooms() {
$.ajax({
- url: "{{dirprefix}}api.php?do=locationinfo&get=machines&uuid=" + panelUuid,
+ url: "{{{api}}}get=machines&uuid=" + panelUuid,
dataType: 'json',
cache: false,
timeout: 30000,
diff --git a/modules-available/locationinfo/templates/frontend-summary.html b/modules-available/locationinfo/templates/frontend-summary.html
index 540d0af6..136ac3a5 100644
--- a/modules-available/locationinfo/templates/frontend-summary.html
+++ b/modules-available/locationinfo/templates/frontend-summary.html
@@ -2,8 +2,8 @@
<html lang="{{language}}">
<meta name="viewport" content="width=device-width, initial-scale=1.0" charset="utf-8">
<head>
- <script type='text/javascript' src='{{dirprefix}}script/jquery.js'></script>
- <script type='text/javascript' src='{{dirprefix}}modules/locationinfo/frontend/frontendscript.js'></script>
+ <script type='text/javascript' src='script/jquery.js'></script>
+ <script type='text/javascript' src='modules/locationinfo/frontend/frontendscript.js'></script>
<style type='text/css'>
@@ -249,7 +249,7 @@
*/
function queryRooms() {
$.ajax({
- url: "{{dirprefix}}api.php?do=locationinfo&get=pcstates&uuid={{uuid}}",
+ url: "{{{api}}}get=pcstates&uuid={{uuid}}",
dataType: 'json',
cache: false,
timeout: 30000,
@@ -696,7 +696,7 @@
* api.inc.php / page.inc.php is getting the ids with the panel uuid.
*/
function queryCalendars() {
- var url = "{{dirprefix}}api.php?do=locationinfo&get=calendar&uuid={{uuid}}";
+ var url = "{{{api}}}get=calendar&uuid={{uuid}}";
// Todo reimplement Frontend method if needed
/*
@@ -723,7 +723,7 @@
*/
function queryPanelChange() {
$.ajax({
- url: "{{dirprefix}}api.php?do=locationinfo&get=timestamp&uuid={{uuid}}",
+ url: "{{{api}}}get=timestamp&uuid={{uuid}}",
dataType: 'json',
cache: false,
timeout: 5000,
diff --git a/modules-available/locationinfo/templates/page-config-panel-default.html b/modules-available/locationinfo/templates/page-config-panel-default.html
index 86e2a520..a289d26a 100644
--- a/modules-available/locationinfo/templates/page-config-panel-default.html
+++ b/modules-available/locationinfo/templates/page-config-panel-default.html
@@ -20,108 +20,117 @@
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">{{lang_generalSettings}}</div>
- <div class="panel-body">
- <div class="list-group">
+ <div class="list-group">
- <div class="list-group-item">
- <div class="row">
- <div class="col-sm-3">
- <label for="panel-title">{{lang_displayName}}</label>
- </div>
- <div class="col-sm-7">
- <input class="form-control" name="name" id="panel-title" type="text" value="{{panelname}}">
- </div>
- <div class="col-sm-2">
- <p class="btn btn-static helptext" title="{{lang_displayNameTooltip}}">
- <span class="glyphicon glyphicon-question-sign"></span>
- </p>
- </div>
+ <div class="list-group-item">
+ <div class="row">
+ <div class="col-sm-4">
+ <label for="panel-title">{{lang_displayName}}</label>
+ </div>
+ <div class="col-sm-8">
+ <input class="form-control" name="name" id="panel-title" type="text" value="{{panelname}}">
+ </div>
+ <div class="col-sm-12 small text-muted spacebottop">
+ {{lang_displayNameTooltip}}
</div>
</div>
+ </div>
- <div class="list-group-item">
- <div class="row">
- <div class="col-sm-3">
- <label for="language">{{lang_language}}</label>
- </div>
- <div class="col-sm-7">
- <select class="form-control" name="language" id="language">
- {{#languages}}
- <option value="{{cc}}" id="lang-{{cc}}" {{selected}}>{{name}}</option>
- {{/languages}}
- </select>
- </div>
- <div class="col-sm-2">
- <p class="btn btn-static helptext" title="{{lang_languageTooltip}}">
- <span class="glyphicon glyphicon-question-sign"></span>
- </p>
- </div>
+ <div class="list-group-item">
+ <div class="row">
+ <div class="col-sm-4">
+ <label for="language">{{lang_language}}</label>
+ </div>
+ <div class="col-sm-8">
+ <select class="form-control" name="language" id="language">
+ {{#languages}}
+ <option value="{{cc}}" id="lang-{{cc}}" {{selected}}>{{name}}</option>
+ {{/languages}}
+ </select>
+ </div>
+ <div class="col-sm-12 small text-muted spacebottop">
+ {{lang_languageTooltip}}
</div>
</div>
+ </div>
- <div class="list-group-item">
- <div class="row">
- <div class="col-sm-3">
- <label for="input-eco">{{lang_ecoMode}}</label>
- </div>
- <div class="col-sm-7">
+ <div class="list-group-item">
+ <div class="row">
+ <div class="col-sm-4">
+ <label for="input-eco">{{lang_ecoMode}}</label>
+ </div>
+ <div class="col-sm-8">
+ <div class="checkbox">
<input id="input-eco" type="checkbox" name="eco" {{eco_checked}}>
- </div>
- <div class="col-sm-2">
- <p class="btn btn-static helptext" title="{{lang_ecoTooltip}}">
- <span class="glyphicon glyphicon-question-sign"></span>
- </p>
+ <label></label>
</div>
</div>
+ <div class="col-sm-12 small text-muted spacebottop">
+ {{lang_ecoTooltip}}
+ </div>
</div>
+ </div>
- <div class="list-group-item">
- <div class="row">
- <div class="col-sm-3">
- <label for="input-prettytime">{{lang_prettytime}}</label>
- </div>
- <div class="col-sm-7">
+ <div class="list-group-item">
+ <div class="row">
+ <div class="col-sm-4">
+ <label for="input-prettytime">{{lang_prettytime}}</label>
+ </div>
+ <div class="col-sm-8">
+ <div class="checkbox">
<input id="input-prettytime" type="checkbox" name="prettytime" {{prettytime_checked}}>
- </div>
- <div class="col-sm-2">
- <p class="btn btn-static helptext" title="{{lang_prettytimeTooltip}}">
- <span class="glyphicon glyphicon-question-sign"></span>
- </p>
+ <label></label>
</div>
</div>
+ <div class="col-sm-12 small text-muted spacebottop">
+ {{lang_prettytimeTooltip}}
+ </div>
</div>
+ </div>
- <div class="list-group-item">
- <div class="row">
- <div class="col-sm-3">
- <label for="input-calupdate">{{lang_calendarUpdate}}</label>
- </div>
- <div class="col-sm-7">
- <input class="form-control" name="calupdate" type="number" min="30" id="input-calupdate"
- max="1440" value="{{calupdate}}" required>
- </div>
- <div class="col-sm-2">
- <p class="btn btn-static helptext" title="{{lang_calupdateTooltip}}">
- <span class="glyphicon glyphicon-question-sign"></span>
- </p>
+ <div class="list-group-item m1-s m2-h m3-s m4-s">
+ <div class="row">
+ <div class="col-sm-4">
+ <label for="show-hostname">{{lang_showHostname}}</label>
+ </div>
+ <div class="col-sm-8">
+ <div class="checkbox">
+ <input id="show-hostname" class="btstrpCheckbox" type="checkbox" name="hostname" {{hostname_checked}}>
+ <label></label>
</div>
</div>
+ <div class="col-sm-12 small text-muted spacebottop">
+ {{lang_hostnameTooltip}}
+ </div>
</div>
+ </div>
- <div class="list-group-item">
- <div class="row">
- <div class="col-sm-3">
- <label for="form-roomupdate">{{lang_roomUpdate}}</label>
- </div>
- <div class="col-sm-7">
- <input class="form-control" name="roomupdate" type="number" min="15" id="form-roomupdate"
- max="86400" value="{{roomupdate}}" required>
- </div>
- <div class="col-sm-2">
- <p class="btn btn-static helptext" title="{{lang_roomupdateTooltip}}">
- <span class="glyphicon glyphicon-question-sign"></span>
- </p>
- </div>
+ <div class="list-group-item">
+ <div class="row">
+ <div class="col-sm-4">
+ <label for="input-calupdate">{{lang_calendarUpdate}}</label>
+ </div>
+ <div class="col-sm-8">
+ <input class="form-control" name="calupdate" type="number" min="30" id="input-calupdate"
+ max="1440" value="{{calupdate}}" required>
+ </div>
+ <div class="col-sm-12 small text-muted spacebottop">
+ {{lang_calupdateTooltip}}
+ </div>
+ </div>
+ </div>
+
+ <div class="list-group-item">
+ <div class="row">
+ <div class="col-sm-4">
+ <label for="form-roomupdate">{{lang_roomUpdate}}</label>
+ </div>
+ <div class="col-sm-8">
+ <input class="form-control" name="roomupdate" type="number" min="15" id="form-roomupdate"
+ max="86400" value="{{roomupdate}}" required>
+ </div>
+ <div class="col-sm-12 small text-muted spacebottop">
+ {{lang_roomupdateTooltip}}
</div>
</div>
</div>
@@ -132,197 +141,164 @@
<div class="col-md-6">
<div class="panel panel-default" id="extra-div">
<div class="panel-heading">{{lang_mode}}</div>
- <div class="panel-body">
- <div class="list-group" id="overridableConfigs">
- <div class="list-group-item">
- <div class="row">
- <div class="col-sm-3">
- <label for="mode">{{lang_mode}}</label>
- </div>
- <div class="col-sm-7">
- <select class="form-control" name="mode" id="mode" onchange="modeChange(id)">
- <option value="1" id="mode1">{{lang_mode1}}</option>
- <option value="2" id="mode2">{{lang_mode2}}</option>
- <option value="3" id="mode3">{{lang_mode3}}</option>
- <option value="4" id="mode4">{{lang_mode4}}</option>
- </select>
- </div>
- <div class="col-sm-2">
- <p class="btn btn-static helptext" title="{{lang_modeTooltip}}">
- <span class="glyphicon glyphicon-question-sign"></span>
- </p>
- </div>
+ <div class="list-group" id="overridableConfigs">
+ <div class="list-group-item">
+ <div class="row">
+ <div class="col-sm-4">
+ <label for="mode">{{lang_mode}}</label>
+ </div>
+ <div class="col-sm-8">
+ <select class="form-control" name="mode" id="mode" onchange="modeChange(id)">
+ <option value="1" id="mode1">{{lang_mode1}}</option>
+ <option value="2" id="mode2">{{lang_mode2}}</option>
+ <option value="3" id="mode3">{{lang_mode3}}</option>
+ <option value="4" id="mode4">{{lang_mode4}}</option>
+ </select>
+ </div>
+ <div class="col-sm-12 small text-muted spacebottop">
+ {{lang_modeTooltip}}
</div>
</div>
+ </div>
- <div class="list-group-item">
- <div class="row">
- <div class="col-sm-3">
- <label for="input-roomplanner">{{lang_useRoomplanner}}</label>
- </div>
- <div class="col-sm-7">
- <select class="form-control" name="roomplanner" id="input-roomplanner">
- <option value="0" id="roomplanner0">{{lang_countIp}}</option>
- <option value="1" id="roomplanner1">{{lang_countRoomplan}}</option>
- </select>
- </div>
- <div class="col-sm-2">
- <p class="btn btn-static helptext" title="{{lang_roomplannerTooltip}}">
- <span class="glyphicon glyphicon-question-sign"></span>
- </p>
- </div>
+ <div class="list-group-item">
+ <div class="row">
+ <div class="col-sm-4">
+ <label for="input-roomplanner">{{lang_useRoomplanner}}</label>
+ </div>
+ <div class="col-sm-8">
+ <select class="form-control" name="roomplanner" id="input-roomplanner">
+ <option value="0" id="roomplanner0">{{lang_countIp}}</option>
+ <option value="1" id="roomplanner1">{{lang_countRoomplan}}</option>
+ </select>
+ </div>
+ <div class="col-sm-12 small text-muted spacebottop">
+ {{lang_roomplannerTooltip}}
</div>
</div>
+ </div>
- <div class="list-group-item m1-s m2-h m3-h m4-h">
- <div class="row">
- <div class="col-sm-3">
- <label for="input-vertical">{{lang_vertical}}</label>
- </div>
- <div class="col-sm-7">
- <input id="input-vertical" class="btstrpCheckbox" type="checkbox" name="vertical" {{vertical_checked}}>
- </div>
- <div class="col-sm-2">
- <p class="btn btn-static helptext" title="{{lang_verticalTooltip}}">
- <span class="glyphicon glyphicon-question-sign"></span>
- </p>
- </div>
+ <div class="list-group-item m1-s m2-h m3-h m4-h">
+ <div class="row">
+ <div class="col-sm-4">
+ <label for="input-vertical">{{lang_vertical}}</label>
+ </div>
+ <div class="col-sm-8">
+ <input id="input-vertical" class="btstrpCheckbox" type="checkbox" name="vertical" {{vertical_checked}}>
+ </div>
+ <div class="col-sm-12 small text-muted spacebottop">
+ {{lang_verticalTooltip}}
</div>
</div>
+ </div>
- <div class="list-group-item m1-s m2-s m3-h m4-s">
- <div class="row">
- <div class="col-sm-3">
- <label for="startday">{{lang_startDay}}</label>
- </div>
- <div class="col-sm-7">
- <select class="form-control" id="startday" name="startday">
- <option value="0">{{lang_currentDay}}</option>
- <option value="1">{{lang_monday}}</option>
- <option value="2">{{lang_thuesday}}</option>
- <option value="3">{{lang_wednesday}}</option>
- <option value="4">{{lang_thursday}}</option>
- <option value="5">{{lang_friday}}</option>
- <option value="6">{{lang_saturday}}</option>
- <option value="7">{{lang_sunday}}</option>
- </select>
- </div>
- <div class="col-sm-2">
- <p class="btn btn-static helptext" title="{{lang_startDayTooltip}}">
- <span class="glyphicon glyphicon-question-sign"></span>
- </p>
- </div>
+ <div class="list-group-item m1-s m2-s m3-h m4-s">
+ <div class="row">
+ <div class="col-sm-4">
+ <label for="startday">{{lang_startDay}}</label>
+ </div>
+ <div class="col-sm-8">
+ <select class="form-control" id="startday" name="startday">
+ <option value="0">{{lang_currentDay}}</option>
+ <option value="1">{{lang_monday}}</option>
+ <option value="2">{{lang_thuesday}}</option>
+ <option value="3">{{lang_wednesday}}</option>
+ <option value="4">{{lang_thursday}}</option>
+ <option value="5">{{lang_friday}}</option>
+ <option value="6">{{lang_saturday}}</option>
+ <option value="7">{{lang_sunday}}</option>
+ </select>
+ </div>
+ <div class="col-sm-12 small text-muted spacebottop">
+ {{lang_startDayTooltip}}
</div>
</div>
+ </div>
- <div class="list-group-item m1-s m2-s m3-h m4-s">
- <div class="row">
- <div class="col-sm-3">
- <label for="scaledaysauto">{{lang_autoScale}}</label>
- </div>
- <div class="col-sm-7">
+ <div class="list-group-item m1-s m2-s m3-h m4-s">
+ <div class="row">
+ <div class="col-sm-4">
+ <label for="scaledaysauto">{{lang_autoScale}}</label>
+ </div>
+ <div class="col-sm-8">
+ <div class="checkbox">
<input id="scaledaysauto" class="btstrpCheckbox" type="checkbox" name="scaledaysauto" {{scaledaysauto_checked}}>
- </div>
- <div class="col-sm-2">
- <p class="btn btn-static helptext" title="{{lang_autoscaleTooltip}}">
- <span class="glyphicon glyphicon-question-sign"></span>
- </p>
+ <label></label>
</div>
</div>
- </div>
-
- <div class="list-group-item m1-s m2-s m3-h m4-s">
- <div class="row">
- <div class="col-sm-3">
- <label for="daystoshow">{{lang_daysToShow}}</label>
- </div>
- <div class="col-sm-7">
- <select class="form-control" id="daystoshow" name="daystoshow">
- <option value="1">1</option>
- <option value="2">2</option>
- <option value="3">3</option>
- <option value="4">4</option>
- <option value="5">5</option>
- <option value="6">6</option>
- <option value="7">7</option>
- </select>
- </div>
- <div class="col-sm-2">
- <p class="btn btn-static helptext" title="{{lang_daysToShowTooltip}}">
- <span class="glyphicon glyphicon-question-sign"></span>
- </p>
- </div>
+ <div class="col-sm-12 small text-muted spacebottop">
+ {{lang_autoscaleTooltip}}
</div>
</div>
+ </div>
- <div class="list-group-item m1-s m2-h m3-s m4-s">
- <div class="row">
- <div class="col-sm-3">
- <label for="rotation">{{lang_rotation}}</label>
- </div>
- <div class="col-sm-7">
- <select class="form-control" id="rotation" name="rotation">
- <option value="0">{{lang_rotation0}}</option>
- <option value="3">{{lang_rotation3}}</option>
- <option value="2">{{lang_rotation2}}</option>
- <option value="1">{{lang_rotation1}}</option>
- </select>
- </div>
- <div class="col-sm-2">
- <p class="btn btn-static helptext" title="{{lang_rotationTooltip}}">
- <span class="glyphicon glyphicon-question-sign"></span>
- </p>
- </div>
+ <div class="list-group-item m1-s m2-s m3-h m4-s">
+ <div class="row">
+ <div class="col-sm-4">
+ <label for="daystoshow">{{lang_daysToShow}}</label>
+ </div>
+ <div class="col-sm-8">
+ <select class="form-control" id="daystoshow" name="daystoshow">
+ <option value="1">1</option>
+ <option value="2">2</option>
+ <option value="3">3</option>
+ <option value="4">4</option>
+ <option value="5">5</option>
+ <option value="6">6</option>
+ <option value="7">7</option>
+ </select>
+ </div>
+ <div class="col-sm-12 small text-muted spacebottop">
+ {{lang_daysToShowTooltip}}
</div>
</div>
+ </div>
- <div class="list-group-item m1-s m2-h m3-s m4-s">
- <div class="row">
- <div class="col-sm-3">
- <label for="show-hostname">{{lang_showHostname}}</label>
- </div>
- <div class="col-sm-7">
- <input id="show-hostname" class="btstrpCheckbox" type="checkbox" name="hostname" {{hostname_checked}}>
- </div>
- <div class="col-sm-2">
- <p class="btn btn-static helptext" title="{{lang_hostnameTooltip}}">
- <span class="glyphicon glyphicon-question-sign"></span>
- </p>
- </div>
+ <div class="list-group-item m1-s m2-h m3-s m4-s">
+ <div class="row">
+ <div class="col-sm-4">
+ <label for="rotation">{{lang_rotation}}</label>
+ </div>
+ <div class="col-sm-8">
+ <select class="form-control" id="rotation" name="rotation">
+ <option value="0">{{lang_rotation0}}</option>
+ <option value="3">{{lang_rotation3}}</option>
+ <option value="2">{{lang_rotation2}}</option>
+ <option value="1">{{lang_rotation1}}</option>
+ </select>
+ </div>
+ <div class="col-sm-12 small text-muted spacebottop">
+ {{lang_rotationTooltip}}
</div>
</div>
+ </div>
- <div class="list-group-item m1-s m2-h m3-h m4-h">
- <div class="row">
- <div class="col-sm-3">
- <label for="input-scale">{{lang_scale}}</label>
- </div>
- <div class="col-sm-7">
- <span><span class="range-display"></span>&thinsp;%</span>
- <input id="input-scale" name="scale" type="range" step="1" min="10" max="90" value="{{scale}}">
- </div>
- <div class="col-sm-2">
- <p class="btn btn-static helptext" title="{{lang_scaleTooltip}}">
- <span class="glyphicon glyphicon-question-sign"></span>
- </p>
- </div>
+ <div class="list-group-item m1-s m2-h m3-h m4-h">
+ <div class="row">
+ <div class="col-sm-4">
+ <label for="input-scale">{{lang_scale}}</label>
+ </div>
+ <div class="col-sm-8">
+ <span><span class="range-display"></span>&thinsp;%</span>
+ <input id="input-scale" name="scale" type="range" step="1" min="10" max="90" value="{{scale}}">
+ </div>
+ <div class="col-sm-12 small text-muted spacebottop">
+ {{lang_scaleTooltip}}
</div>
</div>
+ </div>
- <div class="list-group-item m1-h m2-h m3-h m4-s">
- <div class="row">
- <div class="col-sm-3">
- <label for="input-switchtime">{{lang_switchTime}}</label>
- </div>
- <div class="col-sm-7">
- <span><span class="range-display"></span>&thinsp;{{lang_sec}}</span>
- <input id="input-switchtime" name="switchtime" type="range" step="1" min="1" max="120" value="{{switchtime}}">
- </div>
- <div class="col-sm-2">
- <p class="btn btn-static helptext" title="{{lang_switchTimeTooltip}}">
- <span class="glyphicon glyphicon-question-sign"></span>
- </p>
- </div>
+ <div class="list-group-item m1-h m2-h m3-h m4-s">
+ <div class="row">
+ <div class="col-sm-4">
+ <label for="input-switchtime">{{lang_switchTime}}</label>
+ </div>
+ <div class="col-sm-8">
+ <span><span class="range-display"></span>&thinsp;{{lang_sec}}</span>
+ <input id="input-switchtime" name="switchtime" type="range" step="1" min="1" max="120" value="{{switchtime}}">
+ </div>
+ <div class="col-sm-12 small text-muted spacebottop">
+ {{lang_switchTimeTooltip}}
</div>
</div>
</div>
@@ -400,9 +376,6 @@ document.addEventListener("DOMContentLoaded", function () {
var $owPanels = $('#overrideRoomConfigs');
var overrides = {{{overrides}}}
- // Initialize fancy tooltips
- $('p.helptext').tooltip();
-
// Add listener to range sliders so their label can be updated
var $range = $('input[type="range"]');
$range.change(function () {
@@ -410,8 +383,7 @@ document.addEventListener("DOMContentLoaded", function () {
});
// Set state of input controls that aren't statically initialized server side
$('.modify-inputs input[type="checkbox"]')
- .bootstrapSwitch({size: 'small'})
- .on('switchChange.bootstrapSwitch', function () {
+ .on('click', function () {
if (this.name === 'scaledaysauto') {
$('#daystoshow').prop('disabled', this.checked);
}
@@ -471,12 +443,12 @@ document.addEventListener("DOMContentLoaded", function () {
// Add Panel for overwriting room specific config
$owPanels.find('#overrideRoom' + id).remove();
$owPanels.append('<div class="col-md-6" id="overrideRoom' + id + '">' +
- '<div class="panel panel-default">' +
+ '<div class="panel panel-default body-dest">' +
'<div class="panel-heading clearfix">' +
name + ' {{lang_configOverride}}' +
'<div class="checkbox-inline pull-right">' +
'<input type="checkbox" id="override' + id + '" name="override' + id + '"/>' +
- '</div></div><div class="panel-body"></div></div></div>');
+ '</div></div></div></div>');
// Load content into panel
@@ -501,10 +473,6 @@ document.addEventListener("DOMContentLoaded", function () {
var overVal = false;
if (overrides !== null && overrides[locId] !== undefined) overVal = true;
- // Make Bootstrap switches to normal checkboxes to be able to clone them correctly
- var btstrpCheckboxes = $('.modify-inputs input[class="btstrpCheckbox"]');
- btstrpCheckboxes.bootstrapSwitch('destroy');
-
// Clone needed content
var $contentCopy = $('#overridableConfigs').clone(true);
$contentCopy.closest('#overridableConfigs').prop('id', '');
@@ -542,20 +510,19 @@ document.addEventListener("DOMContentLoaded", function () {
if ($( this ).attr('type') === 'range') $( this ).val(overVal ? overrides[locId][oldName] : $('#' + oldId).val());
else if ($( this ).attr('type') === 'checkbox') {
$( this )
- .bootstrapSwitch({size: 'small'})
- .on('switchChange.bootstrapSwitch', function () {
+ .on('click', function () {
var regex = RegExp('[a-b0-9]*scaledaysauto');
var substr = this.name.substring(0, this.name.length - 13);
if (regex.test(this.name)) {
$('#' + substr + 'daystoshow').prop('disabled', this.checked);
}
});
- $( this ).bootstrapSwitch('state', overVal ? overrides[locId][oldName] : $('#' + oldId).val());
+ $( this ).prop('checked', overVal ? overrides[locId][oldName] : $('#' + oldId).prop('checked'));
}
});
// Append copied content to location specific <div>
- var $panelBody = $('#overrideRoom' + locId).find('div.panel-body');
+ var $panelBody = $('#overrideRoom' + locId).find('div.body-dest');
$panelBody.append($contentCopy);
// Specific extra stuff needed
@@ -566,17 +533,6 @@ document.addEventListener("DOMContentLoaded", function () {
// Call modeChange once to correctly show/hide fields
modeChange('override'+ locId + 'mode');
- // Recreate Bootstrap switches from checkboxes
- btstrpCheckboxes
- .bootstrapSwitch({size: 'small'})
- .on('switchChange.bootstrapSwitch', function () {
- var regex = RegExp('[a-b0-9]*scaledaysauto');
- var substr = this.name.substring(0, this.name.length - 13);
- if (regex.test(this.name)) {
- $('#' + substr + 'daystoshow').prop('disabled', this.checked);
- }
- });
-
// Add listener to range inputs for updating value text
var range = $('input[type="range"]');
range.change(function () {
@@ -586,7 +542,7 @@ document.addEventListener("DOMContentLoaded", function () {
}
function unloadOverrideContent(id) {
- var $panelBody = $('#overrideRoom' + id).find('div.panel-body');
+ var $panelBody = $('#overrideRoom' + id).find('div.body-dest');
$panelBody.find('div.list-group').remove();
if (overrides !== null && overrides[id] !== undefined) delete overrides[id]
}
diff --git a/modules-available/locationinfo/templates/page-config-panel-summary.html b/modules-available/locationinfo/templates/page-config-panel-summary.html
index 2dc556ce..a7a34217 100644
--- a/modules-available/locationinfo/templates/page-config-panel-summary.html
+++ b/modules-available/locationinfo/templates/page-config-panel-summary.html
@@ -16,97 +16,88 @@
<div class="col-md-6">
<div class="modify-inputs panel panel-default">
<div class="panel-heading">{{lang_display}}</div>
- <div class="panel-body">
- <div class="list-group">
+ <div class="list-group">
- <div class="list-group-item">
- <div class="row">
- <div class="col-sm-3">
- <label for="panel-title">{{lang_displayName}}</label>
- </div>
- <div class="col-sm-7">
- <input class="form-control" name="name" id="panel-title" type="text" value="{{panelname}}">
- </div>
- <div class="col-sm-2">
- <p class="btn btn-static helptext" title="{{lang_displayNameTooltip}}">
- <span class="glyphicon glyphicon-question-sign"></span>
- </p>
- </div>
+ <div class="list-group-item">
+ <div class="row">
+ <div class="col-sm-4">
+ <label for="panel-title">{{lang_displayName}}</label>
+ </div>
+ <div class="col-sm-8">
+ <input class="form-control" name="name" id="panel-title" type="text" value="{{panelname}}">
+ </div>
+ <div class="col-sm-12 small text-muted spacebottop">
+ {{lang_displayNameTooltip}}
</div>
</div>
+ </div>
- <div class="list-group-item">
- <div class="row">
- <div class="col-sm-3">
- <label for="language">{{lang_language}}</label>
- </div>
- <div class="col-sm-7">
- <select class="form-control" name="language" id="language">
- {{#languages}}
- <option value="{{cc}}" id="lang-{{cc}}" {{selected}}>{{name}}</option>
- {{/languages}}
- </select>
- </div>
- <div class="col-sm-2">
- <p class="btn btn-static helptext" title="{{lang_languageTooltip}}">
- <span class="glyphicon glyphicon-question-sign"></span>
- </p>
- </div>
+ <div class="list-group-item">
+ <div class="row">
+ <div class="col-sm-4">
+ <label for="language">{{lang_language}}</label>
+ </div>
+ <div class="col-sm-8">
+ <select class="form-control" name="language" id="language">
+ {{#languages}}
+ <option value="{{cc}}" id="lang-{{cc}}" {{selected}}>{{name}}</option>
+ {{/languages}}
+ </select>
+ </div>
+ <div class="col-sm-12 small text-muted spacebottop">
+ {{lang_languageTooltip}}
</div>
</div>
+ </div>
- <div class="list-group-item">
- <div class="row">
- <div class="col-sm-3">
- <label for="input-eco">{{lang_ecoMode}}</label>
- </div>
- <div class="col-sm-7">
+ <div class="list-group-item">
+ <div class="row">
+ <div class="col-sm-4">
+ <label for="input-eco">{{lang_ecoMode}}</label>
+ </div>
+ <div class="col-sm-8">
+ <div class="checkbox">
<input id="input-eco" type="checkbox" name="eco" {{eco_checked}}>
- </div>
- <div class="col-sm-2">
- <p class="btn btn-static helptext" title="{{lang_ecoTooltip}}">
- <span class="glyphicon glyphicon-question-sign"></span>
- </p>
+ <label></label>
</div>
</div>
+ <div class="col-sm-12 small text-muted spacebottop">
+ {{lang_ecoTooltip}}
+ </div>
</div>
+ </div>
- <div class="list-group-item">
- <div class="row">
- <div class="col-sm-3">
- <label for="input-roomplanner">{{lang_useRoomplanner}}</label>
- </div>
- <div class="col-sm-7">
- <select class="form-control" name="roomplanner" id="input-roomplanner">
- <option value="0" id="roomplanner0">{{lang_countIp}}</option>
- <option value="1" id="roomplanner1">{{lang_countRoomplan}}</option>
- </select>
- </div>
- <div class="col-sm-2">
- <p class="btn btn-static helptext" title="{{lang_roomplannerTooltip}}">
- <span class="glyphicon glyphicon-question-sign"></span>
- </p>
- </div>
+ <div class="list-group-item">
+ <div class="row">
+ <div class="col-sm-4">
+ <label for="input-roomplanner">{{lang_useRoomplanner}}</label>
+ </div>
+ <div class="col-sm-8">
+ <select class="form-control" name="roomplanner" id="input-roomplanner">
+ <option value="0" id="roomplanner0">{{lang_countIp}}</option>
+ <option value="1" id="roomplanner1">{{lang_countRoomplan}}</option>
+ </select>
+ </div>
+ <div class="col-sm-12 small text-muted spacebottop">
+ {{lang_roomplannerTooltip}}
</div>
</div>
+ </div>
<!--
- <div class="list-group-item">
- <div class="row">
- <div class="col-sm-3">
- <label for="input-prettytime">{{lang_prettytime}}</label>
- </div>
- <div class="col-sm-7">
- <input id="input-prettytime" type="checkbox" name="prettytime" {{prettytime_checked}}>
- </div>
- <div class="col-sm-2">
- <a class="btn btn-default helptext" title="{{lang_prettytimeTooltip}}">
- <span class="glyphicon glyphicon-question-sign"></span>
- </a>
- </div>
+ <div class="list-group-item">
+ <div class="row">
+ <div class="col-sm-4">
+ <label for="input-prettytime">{{lang_prettytime}}</label>
+ </div>
+ <div class="col-sm-8">
+ <input id="input-prettytime" type="checkbox" name="prettytime" {{prettytime_checked}}>
+ </div>
+ <div class="col-sm-12 small text-muted spacebottop">
+ {{lang_prettytimeTooltip}}
</div>
</div>
--->
</div>
+-->
</div>
</div>
</div>
@@ -114,27 +105,23 @@
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">{{lang_updateRates}}</div>
- <div class="panel-body">
- <div class="list-group">
+ <div class="list-group">
- <div class="list-group-item">
- <div class="row">
- <div class="col-sm-3">
- <label for="form-panelupdate">{{lang_panel}}</label>
- </div>
- <div class="col-sm-7">
- <input class="form-control" name="panelupdate" type="number" min="15" id="form-panelupdate"
- max="86400" value="{{panelupdate}}" required>
- </div>
- <div class="col-sm-2">
- <p class="btn btn-static helptext" title="{{lang_summaryUpdateIntervalTooltip}}">
- <span class="glyphicon glyphicon-question-sign"></span>
- </p>
- </div>
+ <div class="list-group-item">
+ <div class="row">
+ <div class="col-sm-4">
+ <label for="form-panelupdate">{{lang_panel}}</label>
+ </div>
+ <div class="col-sm-8">
+ <input class="form-control" name="panelupdate" type="number" min="15" id="form-panelupdate"
+ max="86400" value="{{panelupdate}}" required>
+ </div>
+ <div class="col-sm-12 small text-muted spacebottop">
+ {{lang_summaryUpdateIntervalTooltip}}
</div>
</div>
-
</div>
+
</div>
</div>
</div>
@@ -202,16 +189,11 @@ document.addEventListener("DOMContentLoaded", function () {
var $locList = $('#location-list');
var $locInput = $('#locationids');
- // Initialize fancy tooltips
- $('p.helptext').tooltip();
// Add listener to range sliders so their label can be updated
$('input[type="range"]').change(function () {
$(this).siblings().find('.range-display').text($(this).val());
});
// Set state of input controls that aren't statically initialized server side
- $('.modify-inputs input[type="checkbox"]')
- .bootstrapSwitch({size: 'small'});
-
var lids = $locInput.val().split(',');
$selLocs.empty();
for (var i = 0; i < lids.length; ++i) {
diff --git a/modules-available/locationinfo/templates/page-config-panel-url.html b/modules-available/locationinfo/templates/page-config-panel-url.html
index 86915729..3aaf8620 100644
--- a/modules-available/locationinfo/templates/page-config-panel-url.html
+++ b/modules-available/locationinfo/templates/page-config-panel-url.html
@@ -13,210 +13,227 @@
<div class="panel panel-default">
<div class="panel-heading">{{lang_display}}</div>
- <div class="panel-body">
- <div class="list-group">
+ <div class="list-group">
- <div class="list-group-item">
- <div class="row">
- <div class="col-sm-3">
- <label for="panel-title">{{lang_displayName}}</label>
- </div>
- <div class="col-sm-7">
- <input class="form-control" name="name" id="panel-title" type="text" value="{{panelname}}">
- </div>
- <div class="col-sm-2">
- <p class="btn btn-static helptext" title="{{lang_displayNameTooltip}}">
- <span class="glyphicon glyphicon-question-sign"></span>
- </p>
- </div>
+ <div class="list-group-item">
+ <div class="row">
+ <div class="col-sm-4">
+ <label for="panel-title">{{lang_displayName}}</label>
+ </div>
+ <div class="col-sm-8">
+ <input class="form-control" name="name" id="panel-title" type="text" value="{{panelname}}">
+ </div>
+ <div class="col-sm-12 small text-muted spacebottop">
+ {{lang_displayNameTooltip}}
</div>
</div>
+ </div>
- <div class="list-group-item">
- <div class="row">
- <div class="col-sm-3">
- <label for="panel-url">{{lang_url}}</label>
- </div>
- <div class="col-sm-7">
- <input class="form-control" name="url" id="panel-url" type="text" value="{{url}}"
- placeholder="http://www.bwlehrpool.de/" pattern=".*://.*" required>
- </div>
- <div class="col-sm-2">
- <p class="btn btn-static helptext" title="{{lang_urlTooltip}}">
- <span class="glyphicon glyphicon-question-sign"></span>
- </p>
- </div>
+ <div class="list-group-item">
+ <div class="row">
+ <div class="col-sm-4">
+ <label for="panel-url">{{lang_url}}</label>
+ </div>
+ <div class="col-sm-8">
+ <input class="form-control" name="url" id="panel-url" type="text" value="{{url}}"
+ placeholder="http://www.bwlehrpool.de/" pattern=".*://.*" required>
+ </div>
+ <div class="col-sm-12 small text-muted spacebottop">
+ {{lang_urlTooltip}}
</div>
</div>
+ </div>
- <div class="list-group-item">
- <div class="row">
- <div class="col-sm-3">
- <label for="input-ssl">{{lang_insecureSsl}}</label>
- </div>
- <div class="col-sm-7">
- <div class="checkbox">
- <input id="input-ssl" type="checkbox" name="insecure-ssl" {{ssl_checked}} value="1">
- <label></label>
- </div>
- </div>
- <div class="col-sm-2">
- <p class="btn btn-static helptext" title="{{lang_ignoreSslTooltip}}">
- <span class="glyphicon glyphicon-question-sign"></span>
- </p>
+ <div class="list-group-item">
+ <div class="row">
+ <div class="col-sm-4">
+ <label for="input-ssl">{{lang_insecureSsl}}</label>
+ </div>
+ <div class="col-sm-8">
+ <div class="checkbox">
+ <input id="input-ssl" type="checkbox" name="insecure-ssl" {{ssl_checked}} value="1">
+ <label></label>
</div>
</div>
+ <div class="col-sm-12 small text-muted spacebottop">
+ {{lang_ignoreSslTooltip}}
+ </div>
</div>
+ </div>
- <div class="list-group-item">
- <div class="row">
- <div class="col-sm-3">
- <label for="input-reload">{{lang_reloadIntervalMins}}</label>
+ <div class="list-group-item">
+ <div class="row">
+ <div class="col-sm-4">
+ <label>{{lang_allowTtySwitch}}</label>
+ </div>
+ <div class="col-sm-8">
+ <div class="radio">
+ <input id="tty1" type="radio" name="allow-tty" {{allow-tty__checked}} value="">
+ <label for="tty1">{{lang_useDefault}}</label>
</div>
- <div class="col-sm-7">
- <input class="form-control" id="input-reload" type="number" min="0" max="999" name="reloadminutes" pattern="\d*" value="{{reloadminutes}}">
+ <div class="radio">
+ <input id="tty2" type="radio" name="allow-tty" {{allow-tty_yes_checked}} value="yes">
+ <label for="tty2">{{lang_yes}}</label>
</div>
- <div class="col-sm-2">
- <p class="btn btn-static helptext" title="{{lang_reloadIntervalTooltip}}">
- <span class="glyphicon glyphicon-question-sign"></span>
- </p>
+ <div class="radio">
+ <input id="tty3" type="radio" name="allow-tty" {{allow-tty_no_checked}} value="no">
+ <label for="tty3">{{lang_no}}</label>
</div>
</div>
+ <div class="col-sm-12 small text-muted spacebottop">
+ {{lang_allowTtySwitchTooltip}}
+ </div>
</div>
+ </div>
- <div class="list-group-item">
- <div class="row">
- <div class="col-sm-3">
- <div class="radio">
- <input type="radio" name="iswhitelist" value="1" {{iswhitelist_1_checked}} id="iswhitelist1" class="form-control">
- <label for="iswhitelist1">
- {{lang_urlWhitelist}}
- </label>
- </div>
- <div class="radio">
- <input type="radio" name="iswhitelist" value="0" {{iswhitelist_0_checked}} id="iswhitelist0" class="form-control">
- <label for="iswhitelist0">
- {{lang_urlBlacklist}}
- </label>
- </div>
- </div>
- <div class="col-sm-7">
- <textarea name="urllist" rows="10" class="form-control">{{urllist}}</textarea>
- <p>{{lang_urlListHelp}}</p>
- </div>
- <div class="col-sm-2"></div>
+ <div class="list-group-item">
+ <div class="row">
+ <div class="col-sm-4">
+ <label for="input-reload">{{lang_reloadIntervalMins}}</label>
+ </div>
+ <div class="col-sm-8">
+ <input class="form-control" id="input-reload" type="number" min="0" max="999" name="reloadminutes" pattern="\d*" value="{{reloadminutes}}">
+ </div>
+ <div class="col-sm-12 small text-muted spacebottop">
+ {{lang_reloadIntervalTooltip}}
</div>
</div>
+ </div>
- <div class="list-group-item">
- <div class="row">
- <div class="col-sm-3">
- <label for="split-login">{{lang_splitlogin}}</label>
- </div>
- <div class="col-sm-7">
- <div class="checkbox">
- <input id="split-login" type="checkbox" name="split-login" {{split-login_checked}} value="1">
- <label></label>
- </div>
- </div>
- <div class="col-sm-2">
- <p class="btn btn-static helptext" title="{{lang_splitloginTooltip}}">
- <span class="glyphicon glyphicon-question-sign"></span>
- </p>
- </div>
+ <div class="list-group-item">
+ <div class="row">
+ <div class="col-sm-6">
+ <label for="whitelist">{{lang_whitelist}}</label>
+ <textarea id="whitelist" name="whitelist" rows="10" class="form-control">{{whitelist}}</textarea>
+ </div>
+ <div class="col-sm-6">
+ <label for="blacklist">{{lang_blacklist}}</label>
+ <textarea id="blacklist" name="blacklist" rows="10" class="form-control">{{blacklist}}</textarea>
+ </div>
+ <div class="col-sm-12 slx-smallspace">
+ {{lang_urlListHelp}}
</div>
</div>
+ </div>
- <div class="list-group-item">
- <div class="row">
- <div class="col-sm-3">
- <label for="browser">{{lang_browser}}</label>
- </div>
- <div class="col-sm-7">
- <select class="form-control" name="browser" id="browser" onchange="browserChange()">
- <option value="slx-browser" id="slx">{{lang_slxbrowser}}</option>
- <option value="chromium" id="chrome">{{lang_chromium}}</option>
- </select>
- </div>
- <div class="col-sm-2">
- <p class="btn btn-static helptext" title="{{lang_browserTooltip}}">
- <span class="glyphicon glyphicon-question-sign"></span>
- </p>
+ <div class="list-group-item">
+ <div class="row">
+ <div class="col-sm-4">
+ <label for="split-login">{{lang_splitlogin}}</label>
+ </div>
+ <div class="col-sm-8">
+ <div class="checkbox">
+ <input id="split-login" type="checkbox" name="split-login" {{split-login_checked}} value="1">
+ <label></label>
</div>
</div>
+ <div class="col-sm-12 small text-muted spacebottop">
+ {{lang_splitloginTooltip}}
+ </div>
+ </div>
+ </div>
+
+ <div class="list-group-item">
+ <div class="row">
+ <div class="col-sm-4">
+ <label for="browser">{{lang_browser}}</label>
+ </div>
+ <div class="col-sm-8">
+ <select class="form-control" name="browser" id="browser" onchange="browserChange()">
+ <option value="slx-browser" id="slx">{{lang_slxbrowser}}</option>
+ <option value="chromium" id="chrome">{{lang_chromium}}</option>
+ </select>
+ </div>
+ <div class="col-sm-12 small text-muted spacebottop">
+ {{lang_browserTooltip}}
+ </div>
</div>
+ </div>
- <div class="list-group-item b0-h">
- <div class="row">
- <div class="col-sm-3">
- <label for="interactive">{{lang_interactive}}</label>
- </div>
- <div class="col-sm-7">
- <div class="checkbox">
- <input id="interactive" type="checkbox" name="interactive" {{interactive_checked}} value="1">
- <label></label>
- </div>
- </div>
- <div class="col-sm-2">
- <p class="btn btn-static helptext" title="{{lang_interactiveTooltip}}">
- <span class="glyphicon glyphicon-question-sign"></span>
- </p>
+ <div class="list-group-item b0-h">
+ <div class="row">
+ <div class="col-sm-4">
+ <label for="interactive">{{lang_interactive}}</label>
+ </div>
+ <div class="col-sm-8">
+ <div class="checkbox">
+ <input id="interactive" type="checkbox" name="interactive" {{interactive_checked}} value="1">
+ <label></label>
</div>
</div>
+ <div class="col-sm-12 small text-muted spacebottop">
+ {{lang_interactiveTooltip}}
+ </div>
</div>
+ </div>
- <div class="list-group-item b0-h" id="bookmarks">
- <div class="row">
- <div class="col-sm-3">
- <label for="bookmarks">{{lang_bookmarks}}</label>
- </div>
- <div class="col-sm-7">
- <button type="button" class="btn btn-success" onclick="addBookmark()">
- <span class="glyphicon glyphicon-plus"></span>
- </button>
- </div>
- <div class="col-sm-2">
- <p class="btn btn-static helptext" title="{{lang_bookmarksTooltip}}">
- <span class="glyphicon glyphicon-question-sign"></span>
- </p>
- </div>
+ <div class="list-group-item b0-h" id="bookmarks">
+ <div class="row">
+ <div class="col-sm-4">
+ <label for="bookmarks">{{lang_bookmarks}}</label>
</div>
- <div class="row" style="margin-top: 1em;" id="bookmarkRow" hidden>
- <div class="col-sm-3 col-sm-offset-3">
- <input class="form-control" name="bookmarkNames[]" type="text" value=""
- placeholder="bwLehrpool">
- </div>
- <div class="col-sm-3">
- <input class="form-control" name="bookmarkUrls[]" type="text" value=""
- placeholder="http://www.bwlehrpool.de/" pattern=".*://.*">
- </div>
- <div class="col-sm-1">
- <button type="button" class="btn btn-danger" onclick="this.closest('.row').remove()">
- <span class="glyphicon glyphicon-minus"></span>
- </button>
- </div>
+ <div class="col-sm-8">
+ <button type="button" class="btn btn-success" onclick="addBookmark()">
+ <span class="glyphicon glyphicon-plus"></span>
+ </button>
+ </div>
+ <div class="col-sm-12 small text-muted spacebottop">
+ {{lang_bookmarksTooltip}}
</div>
+ </div>
+ <div class="row" style="margin-top: 1em;" id="bookmarkRow" hidden>
+ <div class="col-sm-3 col-sm-offset-3">
+ <input class="form-control" name="bookmarkNames[]" type="text" value=""
+ placeholder="bwLehrpool">
+ </div>
+ <div class="col-sm-3">
+ <input class="form-control" name="bookmarkUrls[]" type="text" value=""
+ placeholder="https://www.bwlehrpool.de/" pattern=".*://.*">
+ </div>
+ <div class="col-sm-1">
+ <button type="button" class="btn btn-danger" onclick="$(this).closest('.row').remove()">
+ <span class="glyphicon glyphicon-minus"></span>
+ </button>
+ </div>
+ </div>
- {{#bookmarks}}
- <div class="row" style="margin-top: 1em;">
- <div class="col-sm-3 col-sm-offset-3">
- <input class="form-control" name="bookmarkNames[]" type="text" value="{{name}}"
- placeholder="bwLehrpool" required>
- </div>
- <div class="col-sm-3">
- <input class="form-control" name="bookmarkUrls[]" type="text" value="{{url}}"
- placeholder="http://www.bwlehrpool.de/" pattern=".*://.*" required>
- </div>
- <div class="col-sm-1">
- <button type="button" class="btn btn-danger" onclick="this.closest('.row').remove()">
- <span class="glyphicon glyphicon-minus"></span>
- </button>
- </div>
+ {{#bookmarks}}
+ <div class="row" style="margin-top: 1em;">
+ <div class="col-sm-3 col-sm-offset-3">
+ <input class="form-control" name="bookmarkNames[]" type="text" value="{{name}}"
+ placeholder="bwLehrpool" required>
+ </div>
+ <div class="col-sm-3">
+ <input class="form-control" name="bookmarkUrls[]" type="text" value="{{url}}"
+ placeholder="http://www.bwlehrpool.de/" pattern=".*://.*" required>
+ </div>
+ <div class="col-sm-1">
+ <button type="button" class="btn btn-danger" onclick="$(this).closest('.row').remove()">
+ <span class="glyphicon glyphicon-minus"></span>
+ </button>
</div>
- {{/bookmarks}}
</div>
+ {{/bookmarks}}
+ </div>
+
+ <div class="list-group-item">
+ <div class="row">
+ <div class="col-sm-4">
+ <label for="zoom-factor">{{lang_zoomFactor}}</label>
+ </div>
+ <div class="col-sm-7 col-xs-10">
+ <input class="form-control" id="zoom-factor" type="range" min="50" max="300" step="5"
+ name="zoom-factor" value="{{zoom-factor}}">
+ </div>
+ <div class="col-sm-1 col-xs-2" id="zoom-value">
+ </div>
+ <div class="col-sm-12 small text-muted spacebottop">
+ {{lang_zoomFactorTooltip}}
+ </div>
+ </div>
</div>
+
</div>
</div>
<div class="text-right">
@@ -228,14 +245,19 @@
</div>
</form>
-<script type="text/javascript"><!--
+<script>
document.addEventListener("DOMContentLoaded", function () {
- // Initialize fancy tooltips
- $('p.helptext').tooltip();
// load value to dropdown menus
- $('#browser option[value="{{browser}}"]').attr("selected", "selected");
+ $('#browser option[value="{{browser}}"]').prop("selected", true);
browserChange();
+ var $zv = $('#zoom-value');
+ var $zf = $('#zoom-factor');
+ var sliderUpdate = function() {
+ $zv.text($zf.val() + '%');
+ };
+ $zf.on('input', sliderUpdate);
+ sliderUpdate();
});
// Hide interactive-input if slx-browser is selected
@@ -260,4 +282,4 @@ function addBookmark() {
$('#bookmarks').append(rowCopy);
}
-//--></script>
+</script>
diff --git a/modules-available/locationinfo/templates/page-locations.html b/modules-available/locationinfo/templates/page-locations.html
index f90a0f35..c09b5336 100644
--- a/modules-available/locationinfo/templates/page-locations.html
+++ b/modules-available/locationinfo/templates/page-locations.html
@@ -35,7 +35,7 @@
{{/backend}}
</td>
<td class="text-center">
- <span class="glyphicon glyphicon-{{openingGlyph}}"></span>
+ <span class="glyphicon glyphicon-{{openingGlyph}} {{^strong}}text-muted{{/strong}}"></span>
</td>
</tr>
{{/list}}
@@ -50,7 +50,7 @@
<input type="hidden" name="token" value="{{token}}">
<input type="hidden" name="action" value="writeLocationConfig">
<input type="hidden" name="openingtimes" id="json-openingtimes" value="">
- <div class="modal-header"><h2 id="location-modal-header"></h2></div>
+ <div class="modal-header"><h3 id="location-modal-header"></h3></div>
<div class="modal-body"></div>
<div class="modal-footer">
<a class="btn btn-default" data-dismiss="modal">{{lang_close}}</a>
@@ -65,35 +65,7 @@
</div>
</div>
-<div class="hidden" id="expert-template">
- <div class="row expert-row" style="margin-top:1em;border-top:1px solid #ddd">
- <div class="col-xs-12 days-box">
- <div class="pull-right checkbox checkbox-inline"><input type="checkbox" class="i-delete"><label><span class="glyphicon glyphicon-trash"></span></label></div>
- <div class="checkbox checkbox-inline"><input type="checkbox" class="i-Monday"><label>{{lang_shortMonday}}</label></div>
- <div class="checkbox checkbox-inline"><input type="checkbox" class="i-Tuesday"><label>{{lang_shortTuesday}}</label></div>
- <div class="checkbox checkbox-inline"><input type="checkbox" class="i-Wednesday"><label>{{lang_shortWednesday}}</label></div>
- <div class="checkbox checkbox-inline"><input type="checkbox" class="i-Thursday"><label>{{lang_shortThursday}}</label></div>
- <div class="checkbox checkbox-inline"><input type="checkbox" class="i-Friday"><label>{{lang_shortFriday}}</label></div>
- <div class="checkbox checkbox-inline"><input type="checkbox" class="i-Saturday"><label>{{lang_shortSaturday}}</label></div>
- <div class="checkbox checkbox-inline"><input type="checkbox" class="i-Sunday"><label>{{lang_shortSunday}}</label></div>
- </div>
- <div class="col-sm-6">
- <div class="input-group bootstrap-timepicker">
- <span class="input-group-addon"><span class="glyphicon glyphicon-time"></span></span>
- <input type="text" class="form-control timepicker2 i-openingtime" pattern="[0-9]{1,2}:[0-9]{2}">
- </div>
- </div>
- <div class="col-sm-6">
- <div class="input-group bootstrap-timepicker">
- <span class="input-group-addon"><span class="glyphicon glyphicon-time"></span></span>
- <input type="text" class="form-control timepicker2 i-closingtime" pattern="[0-9]{1,2}:[0-9]{2}">
- </div>
- </div>
- </div>
-</div>
-
-<script type="text/javascript"><!--
-
+<script>
document.addEventListener("DOMContentLoaded", function () {
/**
* Load a opening time modal of a location.
@@ -103,9 +75,7 @@ document.addEventListener("DOMContentLoaded", function () {
var locationId = $(this).data('locationid');
var locationName = $(this).text();
$('#location-modal-header').text("[" + locationId + "] " + locationName);
- $('#location-modal').modal('show').find('.modal-body').load("?do=locationinfo&action=config-location&id=" + locationId);
+ $('#location-modal').modal('show').find('.modal-body').text('...').load("?do=locationinfo&action=config-location&id=" + locationId);
});
- $('#settings-form').submit(submitLocationSettings);
});
-
-//--></script>
+</script>
diff --git a/modules-available/locationinfo/templates/page-servers.html b/modules-available/locationinfo/templates/page-servers.html
index 2f692078..86adecca 100644
--- a/modules-available/locationinfo/templates/page-servers.html
+++ b/modules-available/locationinfo/templates/page-servers.html
@@ -66,7 +66,7 @@
<div class="modal fade" id="myModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
- <div class="modal-header" id="myModalHeader"></div>
+ <div class="modal-header" id="myModalHeader"><h3>{{lang_backendSettings}}</h3></div>
<div class="modal-body" id="myModalBody"></div>
<div class="modal-footer">
<a class="btn btn-default" data-dismiss="modal">{{lang_close}}</a>
@@ -102,10 +102,9 @@
* @param serverid The id of the server.
*/
function loadServerSettingsModal(serverid) {
- $('#myModalHeader').text("{{lang_locationSettings}}").css("font-weight", "Bold");
$('#myModal .modal-dialog').css('width', '');
$('#myModal').modal('show');
- $('#myModalBody').load("?do=locationinfo&action=serverSettings&id=" + serverid);
+ $('#myModalBody').text('...').load("?do=locationinfo&action=serverSettings&id=" + serverid);
}
// ########### Server Table ###########
diff --git a/modules-available/locationinfo/templates/server-prop-bool.html b/modules-available/locationinfo/templates/server-prop-bool.html
index bd9dcc64..ee2b8121 100644
--- a/modules-available/locationinfo/templates/server-prop-bool.html
+++ b/modules-available/locationinfo/templates/server-prop-bool.html
@@ -1,16 +1,12 @@
<div class="list-group-item">
<div class="row">
- <div class="col-md-3"><label for="prop-{{property}}">{{title}}</label></div>
- <div class="col-md-7">
+ <div class="col-md-4"><label for="prop-{{property}}">{{title}}</label></div>
+ <div class="col-md-8">
<input class="settings-bs-switch" id="prop-{{property}}" type="checkbox" name="prop-{{property}}" value="1"
{{#currentvalue}}checked{{/currentvalue}}>
</div>
- <div class="col-md-2">
- {{#helptext}}
- <p class="btn btn-static" title="{{helptext}}">
- <span class="glyphicon glyphicon-question-sign"></span>
- </p>
- {{/helptext}}
+ <div class="col-sm-12 small text-muted spacebottop">
+ {{helptext}}
</div>
</div>
</div>
diff --git a/modules-available/locationinfo/templates/server-prop-dropdown.html b/modules-available/locationinfo/templates/server-prop-dropdown.html
index d1351551..bcb0cd5a 100644
--- a/modules-available/locationinfo/templates/server-prop-dropdown.html
+++ b/modules-available/locationinfo/templates/server-prop-dropdown.html
@@ -1,19 +1,15 @@
<div class="list-group-item">
<div class="row">
- <div class="col-md-3"><label for="prop-{{property}}">{{title}}</label></div>
- <div class="col-md-7">
+ <div class="col-md-4"><label for="prop-{{property}}">{{title}}</label></div>
+ <div class="col-md-8">
<select class="form-control" id="prop-{{property}}" name="prop-{{property}}">
{{#select_list}}
<option {{#active}}selected{{/active}}>{{option}}</option>
{{/select_list}}
</select>
</div>
- <div class="col-md-2">
- {{#helptext}}
- <p class="btn btn-static" title="{{helptext}}">
- <span class="glyphicon glyphicon-question-sign"></span>
- </p>
- {{/helptext}}
+ <div class="col-sm-12 small text-muted spacebottop">
+ {{helptext}}
</div>
</div>
</div>
diff --git a/modules-available/locationinfo/templates/server-prop-generic.html b/modules-available/locationinfo/templates/server-prop-generic.html
index 23ff1e4e..ca8234fe 100644
--- a/modules-available/locationinfo/templates/server-prop-generic.html
+++ b/modules-available/locationinfo/templates/server-prop-generic.html
@@ -1,16 +1,12 @@
<div class="list-group-item">
<div class="row">
- <div class="col-md-3"><label for="prop-{{property}}">{{title}}</label></div>
- <div class="col-md-7">
+ <div class="col-md-4"><label for="prop-{{property}}">{{title}}</label></div>
+ <div class="col-md-8">
<input class="form-control" id="prop-{{property}}" type="{{inputtype}}" name="prop-{{property}}"
value="{{currentvalue}}">
</div>
- <div class="col-md-2">
- {{#helptext}}
- <p class="btn btn-static" title="{{helptext}}">
- <span class="glyphicon glyphicon-question-sign"></span>
- </p>
- {{/helptext}}
+ <div class="col-sm-12 small text-muted spacebottop">
+ {{helptext}}
</div>
</div>
</div>