summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.idea/codeStyleSettings.xml1
-rw-r--r--.idea/php.xml4
-rw-r--r--api.php7
-rw-r--r--apis/cron.inc.php44
-rw-r--r--config.php.example13
-rw-r--r--doc/locationinfo114
-rw-r--r--inc/database.inc.php116
-rw-r--r--inc/dictionary.inc.php4
-rw-r--r--inc/download.inc.php1
-rw-r--r--inc/event.inc.php30
-rw-r--r--inc/module.inc.php36
-rw-r--r--inc/property.inc.php11
-rw-r--r--inc/render.inc.php52
-rw-r--r--inc/session.inc.php9
-rw-r--r--inc/util.inc.php30
-rw-r--r--index.php6
-rwxr-xr-xinstall-all2
-rw-r--r--install.php43
-rw-r--r--modules-available/baseconfig/api.inc.php107
-rw-r--r--modules-available/baseconfig/inc/validator.inc.php2
-rw-r--r--modules-available/baseconfig/lang/de/template-tags.json4
-rw-r--r--modules-available/baseconfig/lang/en/template-tags.json4
-rw-r--r--modules-available/baseconfig/page.inc.php65
-rw-r--r--modules-available/baseconfig/templates/_page.html16
-rw-r--r--modules-available/baseconfig_bwlp/baseconfig/settings.json12
-rw-r--r--modules-available/baseconfig_bwlp/lang/de/config-variables.json4
-rw-r--r--modules-available/baseconfig_bwlp/lang/en/config-variables.json4
-rw-r--r--modules-available/baseconfig_partitions_cdn/baseconfig/getconfig.inc.php28
-rw-r--r--modules-available/bootstrap_datepicker/config.json8
-rw-r--r--modules-available/bootstrap_dialog/config.json8
-rw-r--r--modules-available/bootstrap_multiselect/config.json8
-rw-r--r--modules-available/bootstrap_switch/config.json9
-rw-r--r--modules-available/bootstrap_timepicker/config.json8
-rw-r--r--modules-available/dozmod/inc/pagemailtemplates.inc.php42
-rw-r--r--modules-available/dozmod/lang/de/messages.json3
-rw-r--r--modules-available/dozmod/lang/de/module.json1
-rw-r--r--modules-available/dozmod/lang/de/template-tags.json6
-rw-r--r--modules-available/dozmod/lang/en/messages.json3
-rw-r--r--modules-available/dozmod/lang/en/module.json1
-rw-r--r--modules-available/dozmod/lang/en/template-tags.json6
-rw-r--r--modules-available/dozmod/page.inc.php37
-rw-r--r--modules-available/dozmod/templates/templates.html32
-rw-r--r--modules-available/exams/baseconfig/getconfig.inc.php13
-rw-r--r--modules-available/exams/lang/de/template-tags.json4
-rw-r--r--modules-available/exams/lang/en/template-tags.json4
-rw-r--r--modules-available/exams/page.inc.php12
-rw-r--r--modules-available/exams/templates/page-add-edit-exam.html10
-rw-r--r--modules-available/exams/templates/page-upcoming-lectures.html5
-rw-r--r--modules-available/js_chart/config.json9
-rw-r--r--modules-available/js_circles/config.json9
-rw-r--r--modules-available/js_jqueryui/config.json8
-rwxr-xr-xmodules-available/js_jqueryui/style.css30
-rw-r--r--modules-available/js_moment/config.json8
-rw-r--r--modules-available/js_selectize/config.json8
-rw-r--r--modules-available/js_stupidtable/config.json9
-rw-r--r--modules-available/js_vis/config.json8
-rwxr-xr-xmodules-available/js_weekcalendar/clientscript.js2968
-rwxr-xr-xmodules-available/js_weekcalendar/style.css284
-rw-r--r--modules-available/locationinfo/api.inc.php252
-rw-r--r--modules-available/locationinfo/clientscript.js144
-rw-r--r--modules-available/locationinfo/composer.json7
-rw-r--r--modules-available/locationinfo/config.json3
-rwxr-xr-xmodules-available/locationinfo/frontend/img/overlay/rollstuhl.svg104
-rw-r--r--modules-available/locationinfo/hooks/runmode/config.json7
-rw-r--r--modules-available/locationinfo/hooks/translation.inc.php24
-rw-r--r--modules-available/locationinfo/inc/coursebackend.inc.php365
-rw-r--r--modules-available/locationinfo/inc/coursebackend/coursebackend_davinci.inc.php142
-rw-r--r--modules-available/locationinfo/inc/coursebackend/coursebackend_dummy.inc.php128
-rwxr-xr-xmodules-available/locationinfo/inc/coursebackend/coursebackend_exchange.inc.php203
-rw-r--r--modules-available/locationinfo/inc/coursebackend/coursebackend_hisinone.inc.php368
-rw-r--r--modules-available/locationinfo/inc/infopanel.inc.php219
-rw-r--r--modules-available/locationinfo/inc/locationinfo.inc.php109
-rw-r--r--modules-available/locationinfo/install.inc.php88
-rw-r--r--modules-available/locationinfo/lang/de/backend-davinci.json8
-rw-r--r--modules-available/locationinfo/lang/de/backend-dummy.json14
-rw-r--r--modules-available/locationinfo/lang/de/backend-hisinone.json16
-rw-r--r--modules-available/locationinfo/lang/de/messages.json10
-rw-r--r--modules-available/locationinfo/lang/de/module.json3
-rw-r--r--modules-available/locationinfo/lang/de/template-tags.json111
-rw-r--r--modules-available/locationinfo/lang/en/backend-davinci.json8
-rw-r--r--modules-available/locationinfo/lang/en/backend-dummy.json14
-rw-r--r--modules-available/locationinfo/lang/en/backend-hisinone.json16
-rw-r--r--modules-available/locationinfo/lang/en/messages.json10
-rw-r--r--modules-available/locationinfo/lang/en/module.json5
-rw-r--r--modules-available/locationinfo/lang/en/template-tags.json107
-rw-r--r--modules-available/locationinfo/lang/pt/template-tags.json20
-rw-r--r--modules-available/locationinfo/page.inc.php858
-rw-r--r--modules-available/locationinfo/templates/_page.html4
-rw-r--r--modules-available/locationinfo/templates/ajax-config-location.html192
-rw-r--r--modules-available/locationinfo/templates/ajax-config-server.html99
-rwxr-xr-xmodules-available/locationinfo/templates/frontend-default.html1763
-rw-r--r--modules-available/locationinfo/templates/frontend-summary.html700
-rw-r--r--modules-available/locationinfo/templates/page-config-panel-default.html418
-rw-r--r--modules-available/locationinfo/templates/page-config-panel-url.html81
-rw-r--r--modules-available/locationinfo/templates/page-locations.html100
-rw-r--r--modules-available/locationinfo/templates/page-panels.html74
-rw-r--r--modules-available/locationinfo/templates/page-servers.html116
-rw-r--r--modules-available/locationinfo/templates/page-tabs.html6
-rw-r--r--modules-available/locationinfo/templates/server-prop-bool.html16
-rw-r--r--modules-available/locationinfo/templates/server-prop-dropdown.html19
-rw-r--r--modules-available/locationinfo/templates/server-prop-generic.html16
-rw-r--r--modules-available/locations/baseconfig/getconfig.inc.php9
-rw-r--r--modules-available/locations/baseconfig/hook.json3
-rw-r--r--modules-available/locations/inc/location.inc.php82
-rw-r--r--modules-available/locations/templates/location-subnets.html5
-rw-r--r--modules-available/main/category-icons.json3
-rw-r--r--modules-available/main/lang/de/categories.json1
-rw-r--r--modules-available/main/lang/de/global-tags.json2
-rw-r--r--modules-available/main/lang/en/categories.json1
-rw-r--r--modules-available/main/lang/en/global-tags.json2
-rw-r--r--modules-available/main/templates/main-menu.html1
-rw-r--r--modules-available/news/lang/de/template-tags.json2
-rw-r--r--modules-available/news/lang/en/module.json2
-rw-r--r--modules-available/news/lang/en/template-tags.json2
-rw-r--r--modules-available/rebootcontrol/api.inc.php12
-rw-r--r--modules-available/rebootcontrol/config.json2
-rw-r--r--modules-available/rebootcontrol/inc/rebootqueries.inc.php25
-rw-r--r--modules-available/rebootcontrol/lang/de/template-tags.json6
-rw-r--r--modules-available/rebootcontrol/templates/_page.html4
-rw-r--r--modules-available/roomplanner/baseconfig/getconfig.inc.php6
-rw-r--r--modules-available/roomplanner/clientscript.js10
-rw-r--r--modules-available/roomplanner/page.inc.php9
-rw-r--r--modules-available/runmode/baseconfig/getconfig.inc.php29
-rw-r--r--modules-available/runmode/config.json4
-rw-r--r--modules-available/runmode/inc/runmode.inc.php208
-rw-r--r--modules-available/runmode/install.inc.php46
-rw-r--r--modules-available/runmode/page.inc.php176
-rw-r--r--modules-available/runmode/style.css36
-rw-r--r--modules-available/runmode/templates/machine-selector.html128
-rw-r--r--modules-available/runmode/templates/module-machine-list.html44
-rw-r--r--modules-available/statistics/hooks/config-tgz.inc.php40
-rw-r--r--modules-available/statistics/inc/filter.inc.php1
-rw-r--r--modules-available/statistics/inc/machine.inc.php73
-rw-r--r--modules-available/statistics/inc/statistics.inc.php40
-rw-r--r--modules-available/statistics/install.inc.php2
-rw-r--r--modules-available/statistics/lang/de/template-tags.json1
-rw-r--r--modules-available/statistics/lang/en/template-tags.json1
-rw-r--r--modules-available/statistics/page.inc.php4
-rw-r--r--modules-available/statistics/templates/machine-main.html6
-rw-r--r--modules-available/statistics_reporting/config.json2
-rw-r--r--modules-available/statistics_reporting/hooks/cron.inc.php3
-rw-r--r--modules-available/statistics_reporting/inc/getdata.inc.php1
-rw-r--r--modules-available/statistics_reporting/inc/queries.inc.php63
-rw-r--r--modules-available/statistics_reporting/inc/remotereport.inc.php75
-rw-r--r--modules-available/statistics_reporting/lang/de/template-tags.json3
-rw-r--r--modules-available/statistics_reporting/lang/en/template-tags.json1
-rw-r--r--modules-available/statistics_reporting/templates/columnChooser.html1
-rw-r--r--modules-available/summernote/config.json8
-rw-r--r--modules-available/sysconfig/addmodule_adauth.inc.php12
-rw-r--r--modules-available/sysconfig/addmodule_ldapauth.inc.php10
-rw-r--r--modules-available/sysconfig/api.inc.php7
-rw-r--r--modules-available/sysconfig/inc/configmodulebaseldap.inc.php5
-rw-r--r--modules-available/sysconfig/lang/de/template-tags.json2
-rw-r--r--modules-available/sysconfig/lang/en/template-tags.json2
-rw-r--r--modules-available/sysconfig/templates/ad-selfsearch.html2
-rw-r--r--modules-available/sysconfig/templates/ad-start.html13
-rw-r--r--modules-available/sysconfig/templates/ad_ldap-checkconnection.html2
-rw-r--r--modules-available/sysconfig/templates/ad_ldap-checkcredentials.html2
-rw-r--r--modules-available/sysconfig/templates/ad_ldap-homedir.html5
-rw-r--r--modules-available/sysconfig/templates/ldap-start.html9
-rw-r--r--modules-available/syslog/baseconfig/getconfig.inc.php2
-rw-r--r--modules-available/syslog/clientscript.js8
-rw-r--r--modules-available/syslog/config.json3
-rw-r--r--modules-available/syslog/page.inc.php12
-rw-r--r--modules-available/syslog/style.css45
-rw-r--r--modules-available/syslog/templates/page-syslog.html101
-rw-r--r--modules-available/systemstatus/hooks/main-warning.inc.php7
-rw-r--r--modules-available/systemstatus/lang/de/messages.json3
-rw-r--r--modules-available/systemstatus/lang/de/module.json7
-rw-r--r--modules-available/systemstatus/lang/de/template-tags.json9
-rw-r--r--modules-available/systemstatus/lang/en/messages.json3
-rw-r--r--modules-available/systemstatus/lang/en/module.json7
-rw-r--r--modules-available/systemstatus/lang/en/template-tags.json9
-rw-r--r--modules-available/systemstatus/page.inc.php29
-rw-r--r--modules-available/systemstatus/templates/_page.html199
-rw-r--r--modules-available/systemstatus/templates/ajax-reboot.html14
-rw-r--r--modules-available/translation/page.inc.php4
-rw-r--r--modules-available/vmstore/baseconfig/getconfig.inc.php12
-rw-r--r--modules-available/vmstore/templates/page-vmstore.html6
-rw-r--r--modules-available/webinterface/lang/de/template-tags.json5
-rw-r--r--modules-available/webinterface/lang/en/template-tags.json5
-rw-r--r--modules-available/webinterface/page.inc.php48
-rw-r--r--modules-available/webinterface/templates/customization.html30
-rw-r--r--modules-available/webinterface/templates/https.html6
-rw-r--r--style/default.css19
185 files changed, 12600 insertions, 546 deletions
diff --git a/.idea/codeStyleSettings.xml b/.idea/codeStyleSettings.xml
index 9237b675..851e676d 100644
--- a/.idea/codeStyleSettings.xml
+++ b/.idea/codeStyleSettings.xml
@@ -47,6 +47,7 @@
<option name="METHOD_PARAMETERS_LPAREN_ON_NEXT_LINE" value="true" />
<option name="METHOD_PARAMETERS_RPAREN_ON_NEXT_LINE" value="true" />
<option name="KEEP_SIMPLE_METHODS_IN_ONE_LINE" value="true" />
+ <option name="ARRAY_INITIALIZER_WRAP" value="5" />
<option name="DOWHILE_BRACE_FORCE" value="3" />
<option name="WHILE_BRACE_FORCE" value="3" />
<option name="FOR_BRACE_FORCE" value="3" />
diff --git a/.idea/php.xml b/.idea/php.xml
deleted file mode 100644
index cd67c36e..00000000
--- a/.idea/php.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project version="4">
- <component name="PhpProjectSharedConfiguration" php_language_level="5.4.0" />
-</project> \ No newline at end of file
diff --git a/api.php b/api.php
index a9eec1e7..c7508fbd 100644
--- a/api.php
+++ b/api.php
@@ -28,6 +28,13 @@ if (!empty($_REQUEST['do'])) {
$module = preg_replace('/[^a-z]/', '', $_REQUEST['do']);
} elseif (!empty($argv[1])) {
$module = preg_replace('/[^a-z]/', '', $argv[1]);
+ $argc = count($argv) - 1;
+ for ($i = 2; $i < $argc; ++$i) {
+ if (substr($argv[$i], 0, 2) === '--') {
+ $_GET[substr($argv[$i], 2)] = $argv[$i+1];
+ ++$i;
+ }
+ }
} else {
exit(1);
}
diff --git a/apis/cron.inc.php b/apis/cron.inc.php
index cf96ac29..4a980fed 100644
--- a/apis/cron.inc.php
+++ b/apis/cron.inc.php
@@ -11,6 +11,41 @@ if (!isLocalExecution())
exit(0);
define('CRON_KEY_STATUS', 'cron.key.status');
+define('CRON_KEY_BLOCKED', 'cron.key.blocked');
+
+// Crash report mode - used by system crontab entry
+if (($report = Request::get('crashreport', false, 'string'))) {
+ $list = Property::getList(CRON_KEY_STATUS);
+ if (empty($list)) {
+ error_log('Cron crash report triggered but no cronjob marked active.');
+ exit(0);
+ }
+ $str = array();
+ foreach ($list as $item) {
+ Property::removeFromList(CRON_KEY_STATUS, $item);
+ $entry = explode('|', $item, 2);
+ if (count($entry) !== 2)
+ continue;
+ $time = time() - $entry[1];
+ if ($time > 3600) // Sanity check
+ continue;
+ $str[] = $entry[0] . ' (started ' . $time . 's ago)';
+ Property::addToList(CRON_KEY_BLOCKED, $entry[0], 30);
+ }
+ if (empty($str)) {
+ $str = 'an unknown module';
+ }
+ $message = 'Conjob failed. No reply by ' . implode(', ', $str);
+ $details = '';
+ if (is_readable($report)) {
+ $details = file_get_contents($report);
+ if (!empty($details)) {
+ $message .=', click "details" for log';
+ }
+ }
+ EventLog::failure($message, $details);
+ exit(0);
+}
function getJobStatus($id)
{
@@ -32,6 +67,7 @@ function handleModule($file)
include_once $file;
}
+$blocked = Property::getList(CRON_KEY_BLOCKED);
foreach (Hook::load('cron') as $hook) {
// Check if job is still running, or should be considered crashed
$status = getJobStatus($hook->moduleId);
@@ -46,12 +82,16 @@ foreach (Hook::load('cron') as $hook) {
} else {
// Consider job crashed
Property::removeFromList(CRON_KEY_STATUS, $status['string']);
- EventLog::failure('Cronjob for module ' . $hook->moduleId . ' seems to be stuck or has crashed. Check the php or web server error log.');
+ EventLog::failure('Cronjob for module ' . $hook->moduleId . ' seems to be stuck or has crashed.');
continue;
}
}
+ // Are we blocked
+ if (in_array($hook->moduleId, $blocked))
+ continue;
+ // Fire away
$value = $hook->moduleId . '|' . time();
- Property::addToList(CRON_KEY_STATUS, $value, 1800);
+ Property::addToList(CRON_KEY_STATUS, $value, 30);
try {
handleModule($hook->file);
} catch (Exception $e) {
diff --git a/config.php.example b/config.php.example
index 41949fc8..074bba5a 100644
--- a/config.php.example
+++ b/config.php.example
@@ -15,7 +15,7 @@ define('CONFIG_SQL_FORCE_UTF8', true);
define('CONFIG_TGZ_LIST_DIR', '/opt/openslx/configs');
-define('CONFIG_REMOTE_ML', 'https://bwlp-masterserver.ruf.uni-freiburg.de/minilinux/sat_02');
+define('CONFIG_REMOTE_ML', 'https://bwlp-masterserver.ruf.uni-freiburg.de/minilinux/sat_03');
define('CONFIG_TFTP_DIR', '/srv/openslx/tftp');
define('CONFIG_HTTP_DIR', '/srv/openslx/www/boot');
@@ -40,13 +40,16 @@ $MENU_CAT_OVERRIDE = array(
'main.content' => array(
'news', 'locations', 'exams', 'dozmod', 'translation'
),
- 'main.settings-server' => array(
- 'serversetup', 'vmstore', 'webinterface', 'backup'
- ),
'main.settings-client' => array(
'sysconfig', 'baseconfig', 'minilinux'
),
+ 'main.settings-server' => array(
+ 'serversetup', 'vmstore', 'webinterface', 'backup'
+ ),
'main.status' => array(
- 'systemstatus', 'eventlog', 'syslog', 'statistics'
+ 'systemstatus', 'eventlog', 'syslog', 'statistics', 'statistics_reporting'
+ ),
+ 'main.beta' => array(
+ 'locationinfo', 'rebootcontrol'
)
); \ No newline at end of file
diff --git a/doc/locationinfo b/doc/locationinfo
new file mode 100644
index 00000000..e8f4ea50
--- /dev/null
+++ b/doc/locationinfo
@@ -0,0 +1,114 @@
+########## API ##########
+
+/slx-admin/api.php?do=locationinfo
+
+&action=roominfo
+&id= (1 or 1,2,3…)
+[optional] &coords= (true/false)
+Returns an array with the information of the room(s). (JSON) pcState => (IDLE, OCCUPIED, OFF, BROKEN))
+e.g.:
+[{"id":"11","computer":[{"id":"6BF41E7F-C663-E211-9BAC-C5625F50F9E8","pcState":"OFF"}]}]
+
+&action=openingtime
+&id= (1 or 1,2,3...)
+Return an array with the openingtime of the room(s). (JSON)
+e.g.:
+[{"id":"11","openingtime":{"Monday":[{"HourOpen":"7","MinutesOpen":"00","HourClose":"20","MinutesClose":"00"}],"Tuesday":[{"HourOpen":"7","MinutesOpen":"00","HourClose":"20","MinutesClose":"00"}],"Wednesday":[{"HourOpen":"7","MinutesOpen":"00","HourClose":"20","MinutesClose":"00"}],"Thursday":[{"HourOpen":"7","MinutesOpen":"00","HourClose":"20","MinutesClose":"00"}],"Friday":[{"HourOpen":"7","MinutesOpen":"00","HourClose":"20","MinutesClose":"00"}],"Saturday":[{"HourOpen":"9","MinutesOpen":"00","HourClose":"13","MinutesClose":"00"}]}}]
+
+&action=roomtree
+&id= (1 or 1,2,3...)
+Returns the roomtree. (JSON)
+e.g.:
+[{"id":"8","name":"4er-Netz","childs":[{"id":"1","name":"4er-Netz(1-10)","childs":[{"id":"5","name":"bwLehrstuhl 4.4","childs":[]}]},{"id":"11","name":"Raum -113","childs":[]}]}]
+
+&action=config
+&id= (1)
+Return the config of a room. (JSON)
+e.g.:
+{"language":"en","mode":1,"vertical":false,"eco":false,"scaledaysauto":false,"daystoshow":7,"rotation":0,"scale":50,"switchtime":20,"calupdate":30,"roomupdate":30,"configupdate":180,"room":"Raum -113","time":"2017-3-27 2:36:40"}
+
+&action=pcstates
+&id= (1 or 1,2,3...)
+Returns an array of the state stats of the room(s). (JSON)
+e.g.:
+[{"id":"11","idle":0,"occupied":0,"off":1,"broken":0}]
+
+&action=calendar
+&id= (1 or 1,2,3...)
+Returns an array with the calendar of the room(s). (JSON)
+e.g.:
+[{"id":7,"calendar":[{"title":"test exam","start":"2017-3-08 13:00:00","end":"2017-3-08 16:00:00"}]}]
+
+##############################
+########## Frontend ##########
+##############################
+
+Doorsign
+========
+
+1.Usage
+=======
+parameter
+
+required:
+ id: [integer] room id, see in admin panel. For e.g.: id=5 or multiple, up to 4 e.g.: id=5,6,7,8
+
+optional:
+
+ lang:[en,de] set the language
+ mode:[1,2,3,4] sets the displaying
+ 1: Calendar & Room
+ 2: only Calendar
+ 3: only Room
+ 4: Calendar & Room alternately
+ daystoshow:[1,2,3,4,5,6,7] sets how many days the calendar shows
+ scale:[10-90] scales the calendar and Roomplan in mode 1
+ switchtime:[1-120] sets the time between switchen in mode 4 (in seconds)
+ calupdate: Time the calender querys for updates,in minutes.
+ roomupdate: Time the PCs in the room gets updated,in seconds.
+ rotation:[0-3] rotation of the roomplan
+ vertical:[true] only mode 1, sets the calendar above the roomplan
+ configupdate: Time interval the config gets updated (in minutes)
+ scaledaysauto: [true] if true it finds automatically the daystoshow parameter depending on display size
+
+
+All the optional parameters will overwrite the config settings in the admin panel. If more then one room is shown,then it will
+overwrite it for all rooms.
+
+2. Add an overlay
+=================
+First you need an Image(svg,png,jpg), add it to ./locationinfo/frontend/img/overlay.
+You can add your own css class if you want. To do so create an css calss named .overlay-YOUR_IMAGE_NAME in the doorsign.html.
+You can find an example in the doorsign.html called ".overlay-rollstuhl".
+The backend functionaltiy is right now not implemented since it relays on the roominfo module.
+But you can add it manually.
+You need to add the image name (without ending) in the machine database on the position column with the key overlays in an array.
+
+For example (the name of the images could be overlay1.jpg, overlay1.svg):
+
+{"gridRow":"41","gridCol":"48","itemlook":"pc-south", "overlays":["overlay1","overlay2"]}
+
+
+3.Tipps & Tricks
+==================
+
+-if you show 2-3 rooms in mode 1, it's useful to use vertical mode.
+-if calendar items don't fit, show less days or if in mode 1 give the calendar more space(calendar width);
+-it is possible to use different modes if you show more then one room
+
+4.CourseBackend
+===============
+fetchSchedule returns an array containing an array as value and the local room ID as key.
+The contained array contains arrays that have this form ["start"=>'JJJJ-MM-DD HH:MM:SS',"end"=>'JJJJ-MM-DD HH:MM:SS',"title"=>string].
+getError returns the last errormessage.
+checkConection uses a hardcoded room for test purposes.
+If you want to write a new Class you can look at the Dummy Class to learn the structure.
+You also should write the language files for your options into the lang directory.
+
+Panel
+=====
+
+parameter
+
+required:
+ id: [integer] room id, see in admin panel. For e.g.: id=5 or multiple, up to 4 e.g.: id=5,6,7,8
diff --git a/inc/database.inc.php b/inc/database.inc.php
index ff98f5ee..150f828a 100644
--- a/inc/database.inc.php
+++ b/inc/database.inc.php
@@ -45,7 +45,7 @@ class Database
*
* @return array|boolean Associative array representing row, or false if no row matches the query
*/
- public static function queryFirst($query, $args = array(), $ignoreError = false)
+ public static function queryFirst($query, $args = array(), $ignoreError = null)
{
$res = self::simpleQuery($query, $args, $ignoreError);
if ($res === false)
@@ -53,6 +53,20 @@ class Database
return $res->fetch(PDO::FETCH_ASSOC);
}
+ /**
+ * If you need all rows for a query as plain array you can use this.
+ * Don't use this if you want to do further processing of the data, to save some
+ * memory.
+ *
+ * @return array|bool List of associative arrays representing rows, or false on error
+ */
+ public static function queryAll($query, $args = array(), $ignoreError = null)
+ {
+ $res = self::simpleQuery($query, $args, $ignoreError);
+ if ($res === false)
+ return false;
+ return $res->fetchAll(PDO::FETCH_ASSOC);
+ }
/**
* Execute the given query and return the number of rows affected.
@@ -63,7 +77,7 @@ class Database
* @param boolean $ignoreError Ignore query errors and just return false
* @return int|boolean Number of rows affected, or false on error
*/
- public static function exec($query, $args = array(), $ignoreError = false)
+ public static function exec($query, $args = array(), $ignoreError = null)
{
$res = self::simpleQuery($query, $args, $ignoreError);
if ($res === false)
@@ -95,11 +109,16 @@ class Database
* query again with different params, do not rely on the first PDOStatement
* still being valid. If you need to do something fancy, use Database::prepare
*
- * @return \PDOStatement The query result object
+ * @return \PDOStatement|false The query result object
*/
- public static function simpleQuery($query, $args = array(), $ignoreError = false)
+ public static function simpleQuery($query, $args = array(), $ignoreError = null)
{
self::init();
+ if (CONFIG_DEBUG && preg_match('/^\s*SELECT/is', $query)) {
+ self::explainQuery($query, $args);
+ }
+ // Support passing nested arrays for IN statements, automagically refactor
+ self::handleArrayArgument($query, $args);
try {
if (!isset(self::$statements[$query])) {
self::$statements[$query] = self::$dbh->prepare($query);
@@ -108,20 +127,105 @@ class Database
}
if (self::$statements[$query]->execute($args) === false) {
self::$lastError = implode("\n", self::$statements[$query]->errorInfo());
- if ($ignoreError || self::$returnErrors)
+ if ($ignoreError === true || ($ignoreError === null && self::$returnErrors))
return false;
Util::traceError("Database Error: \n" . self::$lastError);
}
return self::$statements[$query];
} catch (Exception $e) {
self::$lastError = '(' . $e->getCode() . ') ' . $e->getMessage();
- if ($ignoreError || self::$returnErrors)
+ if ($ignoreError === true || ($ignoreError === null && self::$returnErrors))
return false;
Util::traceError("Database Error: \n" . self::$lastError);
}
return false;
}
+ private static function explainQuery($query, $args)
+ {
+ $res = self::simpleQuery('EXPLAIN ' . $query, $args, true);
+ if ($res === false)
+ return;
+ $rows = $res->fetchAll(PDO::FETCH_ASSOC);
+ if (empty($rows))
+ return;
+ $log = false;
+ $lens = array();
+ foreach (array_keys($rows[0]) as $key) {
+ $lens[$key] = strlen($key);
+ }
+ foreach ($rows as $row) {
+ if (!$log && preg_match('/filesort|temporary/i', $row['Extra'])) {
+ $log = true;
+ }
+ foreach ($row as $key => $col) {
+ $l = strlen($col);
+ if ($l > $lens[$key]) {
+ $lens[$key] = $l;
+ }
+ }
+ }
+ if (!$log)
+ return;
+ error_log('Possible slow query: ' . $query);
+ $border = $head = '';
+ foreach ($lens as $key => $len) {
+ $border .= '+' . str_repeat('-', $len + 2);
+ $head .= '| ' . str_pad($key, $len) . ' ';
+ }
+ $border .= '+';
+ $head .= '|';
+ error_log("\n" . $border . "\n" . $head . "\n" . $border);
+ foreach ($rows as $row) {
+ $line = '';
+ foreach ($lens as $key => $len) {
+ $line .= '| '. str_pad($row[$key], $len) . ' ';
+ }
+ error_log($line . "|");
+ }
+ error_log($border);
+ }
+
+ /**
+ * Convert nested array argument to multiple arguments.
+ * If you have:
+ * $query = 'SELECT * FROM tbl WHERE bcol = :bool AND col IN (:list)
+ * $args = ( 'bool' => 1, 'list' => ('foo', 'bar') )
+ * it results in:
+ * $query = '...WHERE bcol = :bool AND col IN (:list_0, :list_1)
+ * $args = ( 'bool' => 1, 'list_0' => 'foo', 'list_1' => 'bar' )
+ *
+ * @param string $query sql query string
+ * @param array $args query arguments
+ */
+ private static function handleArrayArgument(&$query, &$args)
+ {
+ foreach (array_keys($args) as $key) {
+ if (is_numeric($key) || $key === '?')
+ continue;
+ if (is_array($args[$key])) {
+ if (empty($args[$key])) {
+ // Empty list - what to do? We try to generate a query string that will not yield any result
+ $args[$key] = 'asdf' . mt_rand(0,PHP_INT_MAX) . mt_rand(0,PHP_INT_MAX)
+ . mt_rand(0,PHP_INT_MAX) . '@' . microtime(true);
+ continue;
+ }
+ $newkey = $key;
+ if ($newkey{0} !== ':') {
+ $newkey = ":$newkey";
+ }
+ $new = array();
+ foreach ($args[$key] as $subIndex => $sub) {
+ $new[] = $newkey . '_' . $subIndex;
+ $args[$newkey . '_' . $subIndex] = $sub;
+ }
+ unset($args[$key]);
+ $new = implode(',', $new);
+ $query = preg_replace('/' . $newkey . '\b/', $new, $query);
+ }
+ }
+ }
+
/**
* Simply calls PDO::prepare and returns the PDOStatement.
* You must call PDOStatement::execute manually on it.
diff --git a/inc/dictionary.inc.php b/inc/dictionary.inc.php
index 634b1c3c..ca2811ff 100644
--- a/inc/dictionary.inc.php
+++ b/inc/dictionary.inc.php
@@ -81,11 +81,11 @@ class Dictionary
return $strings[$tag];
}
- public static function translateFile($path, $tag)
+ public static function translateFile($path, $tag, $returnTagOnMissing = false)
{
if (!class_exists('Page') || Page::getModule() === false)
return false; // We have no page - return false for now, as we're most likely running in api or install mode
- return self::translateFileModule(Page::getModule()->getIdentifier(), $path, $tag);
+ return self::translateFileModule(Page::getModule()->getIdentifier(), $path, $tag, $returnTagOnMissing);
}
public static function translate($tag, $returnTagOnMissing = false)
diff --git a/inc/download.inc.php b/inc/download.inc.php
index a2054f78..b3496e85 100644
--- a/inc/download.inc.php
+++ b/inc/download.inc.php
@@ -21,6 +21,7 @@ class Download
curl_setopt($ch, CURLOPT_MAXREDIRS, 6);
$tmpfile = tempnam('/tmp/', 'bwlp-');
$head = fopen($tmpfile, 'w+b');
+ unlink($tmpfile);
if ($head === false)
Util::traceError("Could not open temporary head file $tmpfile for writing.");
curl_setopt($ch, CURLOPT_WRITEHEADER, $head);
diff --git a/inc/event.inc.php b/inc/event.inc.php
index 7a7c48b0..a16be97b 100644
--- a/inc/event.inc.php
+++ b/inc/event.inc.php
@@ -20,12 +20,17 @@ class Event
EventLog::info('System boot...');
$everythingFine = true;
+ // Delete job entries that might have been running when system rebooted
+ Property::clearList('cron.key.status');
+ Property::clearList('cron.key.blocked');
+
// Tasks: fire away
+ $mountStatus = false;
$mountId = Trigger::mount();
$autoIp = Trigger::autoUpdateServerIp();
$ldadpId = Trigger::ldadp();
$ipxeId = Trigger::ipxe();
-
+
Taskmanager::submit('DozmodLauncher', array(
'operation' => 'start'
));
@@ -36,11 +41,8 @@ class Event
EventLog::info('No VM store type defined.');
$everythingFine = false;
} else {
- $res = Taskmanager::waitComplete($mountId, 5000);
- if (Taskmanager::isFailed($res)) {
- EventLog::failure('Mounting VM store failed', $res['data']['messages']);
- $everythingFine = false;
- }
+ $mountStatus = Taskmanager::waitComplete($mountId, 5000);
+
}
// LDAP AD Proxy
if ($ldadpId === false) {
@@ -70,6 +72,22 @@ class Event
}
}
+ if ($mountStatus !== false && !Taskmanager::isFinished($mountStatus)) {
+ $mountStatus = Taskmanager::waitComplete($mountStatus, 5000);
+ }
+ if (Taskmanager::isFailed($mountStatus)) {
+ // One more time, network could've been down before
+ sleep(10);
+ $mountId = Trigger::mount();
+ $mountStatus = Taskmanager::waitComplete($mountId, 10000);
+ }
+ if ($mountId !== false && Taskmanager::isFailed($mountStatus)) {
+ EventLog::failure('Mounting VM store failed', $mountStatus['data']['messages']);
+ $everythingFine = false;
+ } elseif ($mountId !== false && !Taskmanager::isFinished($mountStatus)) {
+ // TODO: Still running - create callback
+ }
+
// Just so we know booting is done (and we don't expect any more errors from booting up)
if ($everythingFine) {
EventLog::info('Bootup finished without errors.');
diff --git a/inc/module.inc.php b/inc/module.inc.php
index 597cfb57..7211c68c 100644
--- a/inc/module.inc.php
+++ b/inc/module.inc.php
@@ -142,13 +142,23 @@ class Module
private $activated = false;
private $dependencies = array();
private $name;
+ /**
+ * @var array assoc list of 'filename.css' => true|false (true = always load, false = only if module is main module)
+ */
+ private $css = array();
+ /**
+ * @var array assoc list of 'filename.js' => true|false (true = always load, false = only if module is main module)
+ */
+ private $scripts = array();
private function __construct($name)
{
$file = 'modules/' . $name . '/config.json';
$json = @json_decode(@file_get_contents($file), true);
- if (isset($json['dependencies']) && is_array($json['dependencies'])) {
- $this->dependencies = $json['dependencies'];
+ foreach (['dependencies', 'css', 'scripts'] as $key) {
+ if (isset($json[$key]) && is_array($json[$key])) {
+ $this->$key = $json[$key];
+ }
}
if (isset($json['category']) && is_string($json['category'])) {
$this->category = $json['category'];
@@ -251,4 +261,26 @@ class Module
return 'modules/' . $this->name;
}
+ public function getScripts($externalOnly)
+ {
+ if (!$externalOnly) {
+ if (!isset($this->scripts['clientscript.js']) && file_exists($this->getDir() . '/clientscript.js')) {
+ $this->scripts['clientscript.js'] = false;
+ }
+ return array_keys($this->scripts);
+ }
+ return array_keys(array_filter($this->scripts));
+ }
+
+ public function getCss($externalOnly)
+ {
+ if (!$externalOnly) {
+ if (!isset($this->css['style.css']) && file_exists($this->getDir() . '/style.css')) {
+ $this->css['style.css'] = false;
+ }
+ return array_keys($this->css);
+ }
+ return array_keys(array_filter($this->css));
+ }
+
}
diff --git a/inc/property.inc.php b/inc/property.inc.php
index b33e1bff..0b4ea7b3 100644
--- a/inc/property.inc.php
+++ b/inc/property.inc.php
@@ -105,6 +105,17 @@ class Property
));
}
+ /**
+ * Delete entire list with given key.
+ *
+ * @param string $key Key of list
+ * @return int number of items removed
+ */
+ public static function clearList($key)
+ {
+ return Database::exec("DELETE FROM property_list WHERE name = :key", compact('key'));
+ }
+
/*
* Legacy getters/setters
*/
diff --git a/inc/render.inc.php b/inc/render.inc.php
index 5515c659..53e2f314 100644
--- a/inc/render.inc.php
+++ b/inc/render.inc.php
@@ -40,19 +40,39 @@ class Render
self::$mustache = new Mustache_Engine($options);
}
+ private static function cssEsc($str)
+ {
+ return str_replace(array('"', '&', '<', '>'), array('\\000022', '\\000026', '\\00003c', '\\00003e'), $str);
+ }
+
/**
* Output the buffered, generated page
*/
public static function output()
{
Header('Content-Type: text/html; charset=utf-8');
+ /* @var $modules Module[] */
$modules = array_reverse(Module::getActivated());
+ $pageModule = Page::getModule();
+ $title = Property::get('page-title-prefix', '');
+ $bgcolor = Property::get('logo-background', '');
+ if (!empty($bgcolor) || !empty($title)) {
+ self::$header .= '<style type="text/css">' . "\n";
+ if (!empty($bgcolor)) {
+ $fgcolor = self::readableColor($bgcolor);
+ self::$header .= ".navbar-header{background-color:$bgcolor}a.navbar-brand{color:$fgcolor!important}";
+ }
+ if (!empty($title)) {
+ self::$header .= '#navbar-sub:after{content:"' . self::cssEsc($title) . '";margin:0}';
+ }
+ self::$header .= "\n</style>";
+ }
ob_start('ob_gzhandler');
echo
'<!DOCTYPE html>
<html>
<head>
- <title>', self::$title, RENDER_DEFAULT_TITLE, '</title>
+ <title>', $title, self::$title, RENDER_DEFAULT_TITLE, '</title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -61,9 +81,9 @@ class Render
';
// Include any module specific styles
foreach ($modules as $module) {
- $file = $module->getDir() . '/style.css';
- if (file_exists($file)) {
- echo '<link href="', $file, '" rel="stylesheet" media="screen">';
+ $files = $module->getCss($module != $pageModule);
+ foreach ($files as $file) {
+ echo '<link href="', $module->getDir(), '/', $file, '" rel="stylesheet" media="screen">';
}
}
echo '
@@ -91,9 +111,9 @@ class Render
<script src="script/collapse.js"></script>
';
foreach ($modules as $module) {
- $file = $module->getDir() . '/clientscript.js';
- if (file_exists($file)) {
- echo '<script src="', $file, '"></script>';
+ $files = $module->getScripts($module != $pageModule);
+ foreach ($files as $file) {
+ echo '<script src="', $module->getDir(), '/', $file, '"></script>';
}
}
echo
@@ -285,4 +305,22 @@ class Render
self::$dashboard = $params;
}
+ public static function readableColor($hex) {
+ if (strlen($hex) <= 4) {
+ $cnt = 1;
+ } else {
+ $cnt = 2;
+ }
+ if (preg_match('/^#?([a-f0-9]{'.$cnt.'})([a-f0-9]{'.$cnt.'})([a-f0-9]{'.$cnt.'})$/i', $hex, $out) != 1)
+ return '#000';
+ $chans = array();
+ $f = ($cnt === 1 ? 17 : 1);
+ for ($i = 1; $i <= 3; ++$i) {
+ $out[$i] = (hexdec($out[$i]) * $f);
+ $chans[] = $out[$i] ^ 0x80;
+ }
+ $b = (255 - (0.299 * $out[1] + 0.587 * $out[2] + 0.114 * $out[3])) * 2;
+ return sprintf("#%02x%02x%02x", ($chans[0] + $b) / 3, ($chans[1] + $b) / 3, ($chans[2] + $b) / 3);
+ }
+
}
diff --git a/inc/session.inc.php b/inc/session.inc.php
index 26effa3f..24bf6ac0 100644
--- a/inc/session.inc.php
+++ b/inc/session.inc.php
@@ -74,10 +74,15 @@ class Session
{
if (self::$sid === false) return;
@unlink(self::getSessionFile());
- @setcookie('sid', '', time() - 8640000, null, null, !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off', true);
+ self::deleteCookie();
self::$sid = false;
self::$data = false;
}
+
+ public static function deleteCookie()
+ {
+ setcookie('sid', '', time() - 8640000, null, null, !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off', true);
+ }
private static function getSessionFile()
{
@@ -104,7 +109,7 @@ class Session
$sessionfile = self::getSessionFile();
$ret = @file_put_contents($sessionfile, @serialize(self::$data));
if (!$ret) Util::traceError('Storing session data in ' . $sessionfile . ' failed.');
- $ret = @setcookie('sid', self::$sid, time() + CONFIG_SESSION_TIMEOUT, null, null, !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off', true);
+ $ret = setcookie('sid', self::$sid, time() + CONFIG_SESSION_TIMEOUT, null, null, !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off', true);
if (!$ret) Util::traceError('Error: Could not set Cookie for Client (headers already sent)');
}
}
diff --git a/inc/util.inc.php b/inc/util.inc.php
index 5d1a4563..9bcfdf13 100644
--- a/inc/util.inc.php
+++ b/inc/util.inc.php
@@ -12,7 +12,7 @@ class Util
*/
public static function traceError($message)
{
- if ((defined('API') && API) || (defined('AJAX') && AJAX)) {
+ if ((defined('API') && API) || (defined('AJAX') && AJAX) || php_sapi_name() === 'cli') {
error_log('API ERROR: ' . $message);
error_log(self::formatBacktracePlain(debug_backtrace()));
}
@@ -434,4 +434,32 @@ SADFACE;
return $bytes;
}
+ /**
+ * @return string a random UUID, v4.
+ */
+ public static function randomUuid()
+ {
+ $b = unpack('h8a/h4b/h12c', self::randomBytes(12));
+ return sprintf('%08s-%04s-%04x-%04x-%012s',
+
+ // 32 bits for "time_low"
+ $b['a'],
+
+ // 16 bits for "time_mid"
+ $b['b'],
+
+ // 16 bits for "time_hi_and_version",
+ // four most significant bits holds version number 4
+ mt_rand(0, 0x0fff) | 0x4000,
+
+ // 16 bits, 8 bits for "clk_seq_hi_res",
+ // 8 bits for "clk_seq_low",
+ // two most significant bits holds zero and one for variant DCE1.1
+ mt_rand(0, 0x3fff) | 0x8000,
+
+ // 48 bits for "node"
+ $b['c']
+ );
+ }
+
}
diff --git a/index.php b/index.php
index 20049335..a3f45ff3 100644
--- a/index.php
+++ b/index.php
@@ -116,16 +116,14 @@ if (defined('CONFIG_DEBUG') && CONFIG_DEBUG) {
// Set HSTS Header if client is using HTTPS
if(!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
- if (Request::any('hsts') === 'off') {
+ if (Property::get('webinterface.https-hsts', 'False') !== 'True') {
Header('Strict-Transport-Security: max-age=0', true);
} else {
Header('Strict-Transport-Security: max-age=15768000', true);
}
}
Header('Expires: Wed, 29 Mar 2007 09:56:28 GMT');
-Header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
-Header("Cache-Control: post-check=0, pre-check=0", false);
-Header("Pragma: no-cache");
+Header("Cache-Control: max-age=0");
// Now determine which module to run
Page::init();
diff --git a/install-all b/install-all
index 42f8e5e4..50efd708 100755
--- a/install-all
+++ b/install-all
@@ -38,6 +38,8 @@ while true; do
fi
STATUS=$(grep -m1 '^STATUS=' "$FILE" | cut -d= -f2-)
echo "Result: $STATUS"
+ MESSAGE=$(grep -m1 '^MESSAGE=' "$FILE" | cut -d= -f2-)
+ echo "($MESSAGE)"
if [ -z "$STATUS" ] || [ "$STATUS" = "UPDATE_RETRY" ]; then
RETRY=1
fi
diff --git a/install.php b/install.php
index d09cbff2..7937ec38 100644
--- a/install.php
+++ b/install.php
@@ -24,7 +24,7 @@
* This has to be called by an update module at some point to signal the result
* of its execution.
*
- * @param $status one of the UPDATE_* status codes
+ * @param string $status one of the UPDATE_* status codes
* @param string $message Human readable description of the status (optional)
*/
function finalResponse($status, $message = '')
@@ -90,6 +90,47 @@ function tableRename($old, $new) {
}
+/**
+ * Get all constraints from given table+column to another table+column.
+ *
+ * @param string $table source table, being constrained
+ * @param string $column source column
+ * @param string $refTable referenced table, dictating the constraints
+ * @param string $refColumn referenced column
+ * @return false|string[] list of constraints matching the request, false on error
+ */
+function tableGetContraints($table, $column, $refTable, $refColumn)
+{
+ $db = 'openslx';
+ if (defined('CONFIG_SQL_DB')) {
+ $db = CONFIG_SQL_DB;
+ } elseif (defined('CONFIG_SQL_DSN')) {
+ if (preg_match('/dbname\s*=\s*([^;\s]+)\s*(;|$)/i', CONFIG_SQL_DSN, $out)) {
+ $db = $out[1];
+ define('CONFIG_SQL_DB', $db);
+ }
+ }
+ $res = Database::simpleQuery('SELECT `CONSTRAINT_NAME` FROM information_schema.KEY_COLUMN_USAGE'
+ . ' WHERE `TABLE_SCHEMA` = :db AND `TABLE_NAME` = :table AND `COLUMN_NAME` = :column'
+ . ' AND `REFERENCED_TABLE_NAME` = :refTable AND `REFERENCED_COLUMN_NAME` = :refColumn',
+ compact('db', 'table', 'column', 'refTable', 'refColumn'));
+ if ($res === false)
+ return false;
+ return $res->fetchAll(PDO::FETCH_COLUMN, 0);
+}
+
+/**
+ * Drop constraint from a table.
+ *
+ * @param string $table table name
+ * @param string $constraint constraint name
+ * @return bool success indicator
+ */
+function tableDeleteConstraint($table, $constraint)
+{
+ return Database::exec("ALTER TABLE `$table` DROP FOREIGN KEY `$constraint`") !== false;
+}
+
function tableCreate($table, $structure, $fatalOnError = true)
{
if (tableExists($table)) {
diff --git a/modules-available/baseconfig/api.inc.php b/modules-available/baseconfig/api.inc.php
index af780d99..6e985050 100644
--- a/modules-available/baseconfig/api.inc.php
+++ b/modules-available/baseconfig/api.inc.php
@@ -10,6 +10,82 @@ if ($uuid !== false && strlen($uuid) !== 36) {
$uuid = false;
}
+class ConfigHolder
+{
+ private static $config = [];
+
+ private static $context = '';
+
+ public static function setContext($name)
+ {
+ self::$context = $name;
+ }
+
+ public static function addArray($array, $prio = 0)
+ {
+ foreach ($array as $key => $value) {
+ self::add($key, $value, $prio);
+ }
+ }
+
+ public static function add($key, $value, $prio = 0)
+ {
+ if (!isset(self::$config[$key])) {
+ self::$config[$key] = [];
+ }
+ $new = [
+ 'prio' => $prio,
+ 'value' => $value,
+ 'context' => self::$context,
+ ];
+ if (empty(self::$config[$key]) || self::$config[$key][0]['prio'] > $prio) {
+ // Existing is higher, append new one
+ array_push(self::$config[$key], $new);
+ } else {
+ // New one has highest prio or matches existing, put in front
+ array_unshift(self::$config[$key], $new);
+ }
+ }
+
+ public static function get($key)
+ {
+ if (!isset(self::$config[$key]))
+ return false;
+ return self::$config[$key][0]['value'];
+ }
+
+ public static function getConfig()
+ {
+ $ret = [];
+ foreach (self::$config as $key => $list) {
+ if ($list[0]['value'] === false)
+ continue;
+ $ret[$key] = $list[0]['value'];
+ }
+ return $ret;
+ }
+
+ public static function outputConfig()
+ {
+ foreach (self::$config as $key => $list) {
+ echo '##', $key, "\n";
+ foreach ($list as $pos => $item) {
+ echo '# (', $item['context'], ':', $item['prio'], ')';
+ if ($pos != 0 || $item['value'] === false) {
+ if ($pos == 0) {
+ echo " <disabled>\n";
+ } else {
+ echo ': ', str_replace(array("\r", "\n"), array('\r', '\n'), $item['value']), "\n";
+ }
+ continue;
+ }
+ echo "⤵\n", $key, "='", escape($item['value']), "'\n";
+ }
+ }
+ }
+
+}
+
/**
* Escape given string so it is a valid string in sh that can be surrounded
* by single quotes ('). This basically turns _'_ into _'"'"'_
@@ -29,11 +105,11 @@ function escape($string)
* global setting.
*/
-$configVars = array();
function handleModule($file, $ip, $uuid) // Pass ip and uuid instead of global to make them read only
{
- global $configVars;
+ $configVars = [];
include_once $file;
+ ConfigHolder::addArray($configVars, 0);
}
// Handle any hooks by other modules first
@@ -47,9 +123,11 @@ foreach (glob('modules/*/baseconfig/getconfig.inc.php') as $file) {
foreach ($mod->getDependencies() as $dep) {
$depFile = 'modules/' . $dep . '/baseconfig/getconfig.inc.php';
if (file_exists($depFile) && Module::isAvailable($dep)) {
+ ConfigHolder::setContext($dep);
handleModule($depFile, $ip, $uuid);
}
}
+ ConfigHolder::setContext($out[1]);
handleModule($file, $ip, $uuid);
}
@@ -57,28 +135,23 @@ foreach (glob('modules/*/baseconfig/getconfig.inc.php') as $file) {
$defaults = BaseConfigUtil::getVariables();
// Dump global config from DB
+ConfigHolder::setContext('<global>');
$res = Database::simpleQuery('SELECT setting, value, enabled FROM setting_global');
while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
- if (isset($configVars[$row['setting']]) // Already set by a hook above, ignore
- || !isset($defaults[$row['setting']])) // Setting is not defined in any <module>/baseconfig/settings.json
- continue;
+ if (!isset($defaults[$row['setting']]))
+ continue; // Setting is not defined in any <module>/baseconfig/settings.json
if ($row['enabled'] != 1) {
// Setting is disabled
- $configVars[$row['setting']] = false;
+ ConfigHolder::add($row['setting'], false, -1);
} else {
- $configVars[$row['setting']] = $row['value'];
+ ConfigHolder::add($row['setting'], $row['value'], -1);
}
}
// Fallback to default values from json files
+ConfigHolder::setContext('<default>');
foreach ($defaults as $setting => $value) {
- if (isset($configVars[$setting])) {
- if ($configVars[$setting] === false) {
- unset($configVars[$setting]);
- }
- } else {
- $configVars[$setting] = $value['defaultvalue'];
- }
+ ConfigHolder::add($setting, $value['defaultvalue'], -1000);
}
// All done, now output
@@ -86,7 +159,7 @@ foreach ($defaults as $setting => $value) {
if (Request::any('save') === 'true') {
// output AND save to disk: Generate contents
$lines = '';
- foreach ($configVars as $setting => $value) {
+ foreach (ConfigHolder::getConfig() as $setting => $value) {
$lines .= $setting . "='" . escape($value) . "'\n";
}
// Save to all the locations
@@ -105,9 +178,7 @@ if (Request::any('save') === 'true') {
echo $lines;
} else {
// Only output to client
- foreach ($configVars as $setting => $value) {
- echo $setting, "='", escape($value), "'\n";
- }
+ ConfigHolder::outputConfig();
}
// For quick testing or custom extensions: Include external file that should do nothing
diff --git a/modules-available/baseconfig/inc/validator.inc.php b/modules-available/baseconfig/inc/validator.inc.php
index ec7b95aa..d65b592c 100644
--- a/modules-available/baseconfig/inc/validator.inc.php
+++ b/modules-available/baseconfig/inc/validator.inc.php
@@ -28,7 +28,7 @@ class Validator
case 'list':
return self::validateList($data[1], $displayValue);
case 'function':
- return self::$data[1]($displayValue);
+ return call_user_func(array('self', $data[1]), $displayValue);
case 'multilist':
return self::validateMultiList($data[1], $displayValue);
case 'multiinput':
diff --git a/modules-available/baseconfig/lang/de/template-tags.json b/modules-available/baseconfig/lang/de/template-tags.json
index cdd54f6a..7d6da790 100644
--- a/modules-available/baseconfig/lang/de/template-tags.json
+++ b/modules-available/baseconfig/lang/de/template-tags.json
@@ -1,7 +1,9 @@
{
"lang_basicConfiguration": "Basiskonfiguration",
"lang_clientRelatedConfig": "Die Optionen auf dieser Seite beziehen sich auf das Verhalten der bwLehrpool-Clients.",
+ "lang_defaultValue": "Standard",
"lang_editOverrideNotice": "Sie bearbeiten die Einstellungen f\u00fcr einen Unterbereich",
"lang_enableOverride": "\u00dcberschreiben",
- "lang_settingActive": "Einstellung aktiv"
+ "lang_inheritSource": "Geerbt von",
+ "lang_settingActive": "Einstellung aktivieren"
} \ No newline at end of file
diff --git a/modules-available/baseconfig/lang/en/template-tags.json b/modules-available/baseconfig/lang/en/template-tags.json
index 8e75e5ac..1a32cb0d 100644
--- a/modules-available/baseconfig/lang/en/template-tags.json
+++ b/modules-available/baseconfig/lang/en/template-tags.json
@@ -1,7 +1,9 @@
{
"lang_basicConfiguration": "Basic Configuration",
"lang_clientRelatedConfig": "The options on this page are related to the bwLehrpool client machines.",
+ "lang_defaultValue": "Default",
"lang_editOverrideNotice": "You're editing the settings of a sub-section",
"lang_enableOverride": "Override",
- "lang_settingActive": "Setting active"
+ "lang_inheritSource": "Inherited from",
+ "lang_settingActive": "Enable setting"
} \ No newline at end of file
diff --git a/modules-available/baseconfig/page.inc.php b/modules-available/baseconfig/page.inc.php
index 5e99f2a0..bd9d6683 100644
--- a/modules-available/baseconfig/page.inc.php
+++ b/modules-available/baseconfig/page.inc.php
@@ -124,9 +124,6 @@ class Page_BaseConfig extends Page
Util::redirect('?do=BaseConfig');
}
}
- // List config options
- $settings = array();
- $vars = BaseConfigUtil::getVariables();
// Get stuff that's set in DB already
if ($this->targetModule === false) {
$fields = ', enabled';
@@ -141,19 +138,28 @@ class Page_BaseConfig extends Page
$where = '';
$params = array();
}
+ // List config options
+ $settings = array();
+ $vars = BaseConfigUtil::getVariables();
+ // Remember missing variables
+ $missing = $vars;
// Populate structure with existing config from db
- $res = Database::simpleQuery("SELECT setting, value, displayvalue $fields FROM {$this->qry_extra['table']} "
- . " {$where} ORDER BY setting ASC", $params);
- while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
- if (!isset($vars[$row['setting']]) || !is_array($vars[$row['setting']])) {
- $unknown[] = $row['setting'];
- continue;
+ $this->fillSettings($vars, $settings, $missing, $this->qry_extra['table'], $fields, $where, $params, false);
+ if (isset($this->qry_extra['getfallback']) && !empty($missing)) {
+ $method = explode('::', $this->qry_extra['getfallback']);
+ $fieldValue = $this->qry_extra['field_value'];
+ $tries = 0;
+ while (++$tries < 100 && !empty($missing)) {
+ $ret = call_user_func($method, $fieldValue);
+ if ($ret === false)
+ break;
+ $fieldValue = $ret['value'];
+ $params = array('field_value' => $fieldValue);
+ $this->fillSettings($vars, $settings, $missing, $this->qry_extra['table'], $fields, $where, $params, $ret['display']);
}
- $row += $vars[$row['setting']];
- if (!isset($row['catid'])) {
- $row['catid'] = 'unknown';
- }
- $settings[$row['catid']]['settings'][$row['setting']] = $row;
+ }
+ if ($this->targetModule !== false && !empty($missing)) {
+ $this->fillSettings($vars, $settings, $missing, 'setting_global', '', '', array(), 'Global');
}
// Add entries that weren't in the db (global), setup override checkbox (module specific)
foreach ($vars as $key => $var) {
@@ -162,10 +168,7 @@ class Page_BaseConfig extends Page
if (!isset($settings[$var['catid']]['settings'][$key]['enabled']) || $settings[$var['catid']]['settings'][$key]['enabled'] == 1) {
$settings[$var['catid']]['settings'][$key]['checked'] = 'checked';
}
- } elseif (isset($settings[$var['catid']]['settings'][$key])) {
- // Module specific - value is set in DB
- $settings[$var['catid']]['settings'][$key]['checked'] = 'checked';
- } else {
+ } elseif (!isset($settings[$var['catid']]['settings'][$key])) {
// Module specific - value is not set in DB
$settings[$var['catid']]['settings'][$key] = $var + array(
'setting' => $key
@@ -216,6 +219,32 @@ class Page_BaseConfig extends Page
Module::isAvailable('bootstrap_switch');
}
+ private function fillSettings($vars, &$settings, &$missing, $table, $fields, $where, $params, $sourceName)
+ {
+ $res = Database::simpleQuery("SELECT setting, value, displayvalue $fields FROM $table "
+ . " {$where} ORDER BY setting ASC", $params);
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ if (!isset($missing[$row['setting']]))
+ continue;
+ if (!isset($vars[$row['setting']]) || !is_array($vars[$row['setting']])) {
+ $unknown[] = $row['setting'];
+ continue;
+ }
+ unset($missing[$row['setting']]);
+ if ($sourceName !== false) {
+ $row['defaultvalue'] = '';
+ $row['defaultsource'] = $sourceName;
+ } elseif ($this->targetModule !== false) {
+ $row['checked'] = 'checked';
+ }
+ $row += $vars[$row['setting']];
+ if (!isset($row['catid'])) {
+ $row['catid'] = 'unknown';
+ }
+ $settings[$row['catid']]['settings'][$row['setting']] = $row;
+ }
+ }
+
private function getCurrentModuleName()
{
if (isset($this->qry_extra['tostring'])) {
diff --git a/modules-available/baseconfig/templates/_page.html b/modules-available/baseconfig/templates/_page.html
index 7f380495..e0be35bc 100644
--- a/modules-available/baseconfig/templates/_page.html
+++ b/modules-available/baseconfig/templates/_page.html
@@ -25,18 +25,22 @@
<div class="row">
<div class="col-md-5 slx-cfg-toggle">
<div>{{setting}}</div>
- {{^override}}
<div class="slx-default">
+ {{#defaultvalue}}{{lang_defaultValue}}:{{/defaultvalue}}
{{defaultvalue}}
</div>
- <input class="bs-switch" name="override[{{setting}}]" id="CB_{{setting}}" type="checkbox" {{checked}}> <label for="CB_{{setting}}">{{lang_settingActive}}</label>
- {{/override}}
- {{#override}}
- <input class="bs-switch" name="override[{{setting}}]" id="CB_{{setting}}" type="checkbox" {{checked}}> <label for="CB_{{setting}}">{{lang_enableOverride}}</label>
- {{/override}}
+ <input class="bs-switch" name="override[{{setting}}]" id="CB_{{setting}}" type="checkbox" {{checked}}>
+ <label for="CB_{{setting}}">
+ {{#override}}{{lang_enableOverride}}{{/override}}
+ {{^override}}{{lang_settingActive}}{{/override}}
+ </label>
</div>
<div class="col-md-5">
{{{item}}}
+ <div class="slx-default">
+ {{#defaultsource}}{{lang_inheritSource}}:{{/defaultsource}}
+ {{defaultsource}}
+ </div>
</div>
<div class="col-md-2">
<a class="btn btn-default" data-toggle="modal" data-target="#help-{{setting}}"><span class="glyphicon glyphicon-question-sign"></span></a>
diff --git a/modules-available/baseconfig_bwlp/baseconfig/settings.json b/modules-available/baseconfig_bwlp/baseconfig/settings.json
index e9a0a894..79127ccc 100644
--- a/modules-available/baseconfig_bwlp/baseconfig/settings.json
+++ b/modules-available/baseconfig_bwlp/baseconfig/settings.json
@@ -132,5 +132,17 @@
"defaultvalue": "",
"permissions": "2",
"validator": ""
+ },
+ "SLX_AUTOSTART_UUID": {
+ "catid": "vmchooser",
+ "defaultvalue": "",
+ "permissions": "2",
+ "validator": "regex:\/^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|)$\/i"
+ },
+ "SLX_PVS_DEFAULT": {
+ "catid": "vmchooser",
+ "defaultvalue": "OFF",
+ "permissions": "2",
+ "validator": "list:ON|OFF"
}
}
diff --git a/modules-available/baseconfig_bwlp/lang/de/config-variables.json b/modules-available/baseconfig_bwlp/lang/de/config-variables.json
index 63ddf280..06681a58 100644
--- a/modules-available/baseconfig_bwlp/lang/de/config-variables.json
+++ b/modules-available/baseconfig_bwlp/lang/de/config-variables.json
@@ -1,5 +1,6 @@
{
"SLX_ADDONS": "Zu ladende Addons. Zur Zeit steht nur *vmware* zur Verf\u00fcgung.",
+ "SLX_AUTOSTART_UUID": "ID einer Veranstaltung die automatisch gestartet werden soll. Die Veranstaltungs-ID finden Sie im Detailfenster innerhalb der bwLehrpool-Suite.\r\n\r\n*Hinweis: Diese Option ist eine tempor\u00e4re \u00dcbergangsl\u00f6sung. In sp\u00e4teren Versionen wird die Funktionalit\u00e4t einfacher erreichbar sein.*",
"SLX_BIOS_CLOCK": "Legt fest, ob und wie die interne Uhr des Rechners im Bezug auf die Systemzeit des \/MiniLinux\/ gesetzt werden soll.\r\n*off* = Die interne Uhr des Rechners wird nicht ver\u00e4ndert.\r\n*local* = Die interne Uhr wird auf die Lokalzeit gesetzt. Bevorzugt wenn z.B. noch eine native Windows-Installation auf dem PC vorhanden ist.\r\n*utc* = Die interne Uhr wird auf die \/Koordinierte Weltzeit\/ gesetzt. Dies ist die g\u00e4ngige Einstellung in einem reinen Linux-Umfeld.",
"SLX_DEMO_PASS": "Passwort f\u00fcr den eingebauten *demo*-Account. Leer lassen, um das Einloggen zu verbieten.\r\nDas Passwort wird wie das root-Passwort nur gehasht an den Client \u00fcbertragen.",
"SLX_LOGOUT_TIMEOUT": "Zeit \/in Sekunden\/, die eine Benutzersitzung ohne Aktion sein darf, bevor sie beendet wird.Feld leer lassen, um die Funktion zu deaktivieren.",
@@ -11,6 +12,7 @@
"SLX_PROXY_MODE": "Legt fest, ob zum Zugriff aufs Internet ein Proxy-Server ben\u00f6tigt wird.\r\n*off* = keinen Proxy benutzen.\r\n*on* = Proxy immer benutzen.\r\n*auto* = Proxy nur benutzen, wenn sich der Client-PC in einem privaten Adressbereich befindet.",
"SLX_PROXY_PORT": "Der Port des zu verwendenden Proxy Servers.",
"SLX_PROXY_TYPE": "Art des Proxys: *socks4*, *socks5*, *http-connect* (HTTP Proxy mit Unterst\u00fctzung der CONNECT-Methode), *http-relay* (Klassischer HTTP Proxy)",
+ "SLX_PVS_DEFAULT": "Legt fest, ob der Haken zur PVS-Teilnahme im vmChooser standardm\u00e4\u00dfig gesetzt ist oder nicht.",
"SLX_REBOOT_SCHEDULE": "Feste Uhrzeit, zu der sich die Rechner neustarten, auch wenn noch ein Benutzer aktiv ist.\r\nMehrere Zeitpunkte k\u00f6nnen durch Leerzeichen getrennt angegeben werden.",
"SLX_REMOTE_LOG_SESSIONS": "Legt fest, ob Logins und Logouts der Benutzer an den Satelliten gemeldet werden sollen.\r\n*yes* = Mit Benutzerkennung loggen\r\n*anonymous* = Anonym loggen\r\n*no* = Nicht loggen",
"SLX_ROOT_PASS": "Das root-Passwort des Grundsystems. Wird nur f\u00fcr Diagnosezwecke am Client ben\u00f6tigt.\r\nFeld leer lassen, um root-Logins zu verbieten.\r\n\/Hinweis\/: Das Passwort wird im Klartext in der lokalen Datenbank hinterlegt, jedoch immer gehasht an die Clients \u00fcbermittelt (SHA-512 mit Salt). Wenn Sie das Passwort auch im Satelliten nicht im Klartext speichern wollen, k\u00f6nnen Sie hier auch ein vorgehashtes Passwort eintragen (im *$6$....*-Format).",
@@ -18,6 +20,6 @@
"SLX_SHUTDOWN_SCHEDULE": "Feste Uhrzeit, zu der sich die Rechner ausschalten, auch wenn noch ein Benutzer aktiv ist.\r\nMehrere Zeitpunkte k\u00f6nnen durch Leerzeichen getrennt angegeben werden.",
"SLX_SHUTDOWN_TIMEOUT": "Zeit in Sekunden, nach dem ein Rechner abgeschaltet wird, sofern kein Benutzer angemeldet ist.\r\nFeld leer lassen, um die Funktion zu deaktivieren.",
"SLX_VMCHOOSER_FORLOCATION": "Legt das Verhalten fest, wenn es Veranstaltungen gibt, die an einen bestimmten Ort\/Raum gebunden sind.\r\n*IGNORE*: Mit den restlichen, globalen Veranstaltungen alphabetisch sortiert auflisten.\r\n*BUMP*: Die spezifischen Veranstaltungen oben auflisten, die globalen darunter.\r\n*EXCLUSIVE*: Spezifische Veranstaltungen oben auflisten, globale Veranstaltungen zun\u00e4chst ausblenden. Die globalen Veranstaltungen befinden sich unter einem eingeklappten Listenknoten.",
- "SLX_VMCHOOSER_TAB": "Bestimmt, welcher Karteireiter im vmchooser standardm\u00e4\u00dfig ausgew\u00e4hlt wird.\r\n*0*: Native Linux-Sessions\r\n*1*: Nutzerspezifische Kurse\r\n*2*: Alle Kurse\r\n*AUTO*: Hat der Rechner beschr\u00e4nkte Ressourcen, werden die Linux-Sitzungen angezeigt, sonst alle Kurse\r\n\r\nHat der Benutzer ein persistentes Home-Verzeichnis, wirkt sich diese Einstellung nur beim ersten Anmelden aus. Bei sp\u00e4teren Sitzungen markiert der vmchooser die zuletzt gestartete Sitzung und wechselt zum entsprechenden Karteireiter.",
+ "SLX_VMCHOOSER_TAB": "Bestimmt, welcher Karteireiter im vmChooser standardm\u00e4\u00dfig ausgew\u00e4hlt wird.\r\n*0*: Native Linux-Sessions\r\n*1*: Nutzerspezifische Kurse\r\n*2*: Alle Kurse\r\n*AUTO*: Hat der Rechner beschr\u00e4nkte Ressourcen, werden die Linux-Sitzungen angezeigt, sonst alle Kurse\r\n\r\nHat der Benutzer ein persistentes Home-Verzeichnis, wirkt sich diese Einstellung nur beim ersten Anmelden aus. Bei sp\u00e4teren Sitzungen markiert der vmChooser die zuletzt gestartete Sitzung und wechselt zum entsprechenden Karteireiter.",
"SLX_VMCHOOSER_TEMPLATES": "Legt fest, wie Veranstaltungen in der Sortierung behandelt werden, welche auf eine VM linken, die eine Vorlage ist.\r\n*IGNORE*: Wie regul\u00e4re Veranstaltungen behandeln\r\n*BUMP*: Weiter oben in der Liste einsortieren"
} \ No newline at end of file
diff --git a/modules-available/baseconfig_bwlp/lang/en/config-variables.json b/modules-available/baseconfig_bwlp/lang/en/config-variables.json
index ad1601c3..6aee946b 100644
--- a/modules-available/baseconfig_bwlp/lang/en/config-variables.json
+++ b/modules-available/baseconfig_bwlp/lang/en/config-variables.json
@@ -1,5 +1,6 @@
{
"SLX_ADDONS": "Addons to load. Currently, only *vmware* is available.",
+ "SLX_AUTOSTART_UUID": "ID of a lecture which is automatically started. The lecture-ID is found in the detail window of a lecture in the bwLehrpool-Suite. \r\n\r\n*This solution is only temporary. In later versions this feature will probably be moved to another section*",
"SLX_BIOS_CLOCK": "Specifies whether and how the internal clock of the computer should be set in relation to the system time of the \/MiniLinux\/.\r\n*off* = The internal clock of the computer is not changed.\r\n*local* = The internal clock is set to local time. Preferably if, for example, there is still a native Windows installation available on the PC.\r\n*utc* = The internal clock is set to the \/Coordinated Universal Time\/. This is the most common setup in a pure Linux environment.",
"SLX_DEMO_PASS": "Password for the *demo* account. Leave empty to disallow logging in as the demo user.\r\nLike the root password, the demo user's password will be sent to the client in its hashed form.",
"SLX_LOGOUT_TIMEOUT": "Time \/in seconds\/, in which a user session may remain without action before it is terminated.Leave field blank to disable the function.",
@@ -11,13 +12,14 @@
"SLX_PROXY_MODE": "Determines whether a proxy server is required to access the Internet.\r\n*off* = do not use a Proxy.\r\n*on* = Always use proxy.\r\n*auto* = Only use proxy when the client PC is in a private address space.",
"SLX_PROXY_PORT": "The port to use for the proxy server.",
"SLX_PROXY_TYPE": "Type of the proxy.*socks4*, *socks5*, *http-connect* (HTTP proxy with support from the CONNECT method), *http-relay* (Classic HTTP proxy)",
+ "SLX_PVS_DEFAULT": "Set whether the \"Join PVS\" checkbox in vmChooser is checked by default.",
"SLX_REBOOT_SCHEDULE": "Fixed time to reboot the computer, even if there is a user active.\r\nSeveral times can be specified, separated by spaces.",
"SLX_REMOTE_LOG_SESSIONS": "Determines whether logins and logouts of the users should be reported to the satellite.\r\n*yes* = log with user ID\r\n*anonymous* = anonymous logging\r\n*no* = no logging",
"SLX_ROOT_PASS": "The root password of the client system. Only required for diagnostic purposes on the client.Leave field blank to disallow root logins.\r\n\/Hint\/: The password SHA-512-with-salt hashed before it's being sent to the client. It's only stored in clear text on the Satellite Server. If you want to have it hashed on the server too, you can supply a pre-hashed passoword in \/$6$...$...\/-format.",
"SLX_SCREEN_STANDBY_TIMEOUT": "Time in seconds after which the screen will enter power saving mode, if the client is not in use.",
"SLX_SHUTDOWN_SCHEDULE": "Fixed time to turn off the computer, even if there is a user active.\r\nSeveral times can be specified, separated by spaces.",
"SLX_SHUTDOWN_TIMEOUT": "Time in seconds after which a computer is switched off, if no user is logged on.\r\nLeave blank to disable the function.",
- "SLX_VMCHOOSER_FORLOCATION": "Defines how lectures special to the user's location are handled in the vmchooser.\r\n*IGNORE*: Sort them alphabetically among the global lectures.\r\n*BUMP*: Put them atop the global lectures.\r\n*EXCLUSIVE*: Put them atop the global lectures and aditionally collapse the node which contains the global lectures.",
+ "SLX_VMCHOOSER_FORLOCATION": "Defines how lectures special to the user's location are handled in the vmChooser.\r\n*IGNORE*: Sort them alphabetically among the global lectures.\r\n*BUMP*: Put them atop the global lectures.\r\n*EXCLUSIVE*: Put them atop the global lectures and aditionally collapse the node which contains the global lectures.",
"SLX_VMCHOOSER_TAB": "Defines which tab is show by default, if the user doesn't have stored a last used session on his persistent home directory.\r\n*0*: Native Linux sessions\r\n*1*: User specific lectures\r\n*2*: All lectures\r\n*AUTO*: If the computer has low system specs, show the Linux sessions, otherwise, show all lectures",
"SLX_VMCHOOSER_TEMPLATES": "Defines how lectures that link to template VMs are treated wrt sorting.\r\n*IGNORE*: Sort among regular lectures\r\n*BUMP*: Move to top of list"
} \ No newline at end of file
diff --git a/modules-available/baseconfig_partitions_cdn/baseconfig/getconfig.inc.php b/modules-available/baseconfig_partitions_cdn/baseconfig/getconfig.inc.php
index 6348fe71..20efdcb1 100644
--- a/modules-available/baseconfig_partitions_cdn/baseconfig/getconfig.inc.php
+++ b/modules-available/baseconfig_partitions_cdn/baseconfig/getconfig.inc.php
@@ -1,17 +1,19 @@
<?php
-$config = '';
-$res = Database::simpleQuery('SELECT partition_id, size, mount_point, options FROM setting_partition WHERE user = :user',
- array('user'=>$_GET['user']));
-while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
- $config .= "\n" . $row['partition_id'] . "," . $row['size'];
- if(strlen($row['mount_point']) > 0)
- $config .= "," . $row['mount_point'];
- if(strlen($row['options']) > 0)
- $config .= "," . $row['options'];
-}
-$config .= "\n";
+if (isset($_GET['user'])) {
+ $config = '';
+ $res = Database::simpleQuery('SELECT partition_id, size, mount_point, options FROM setting_partition WHERE user = :user',
+ array('user' => $_GET['user']));
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ $config .= "\n" . $row['partition_id'] . "," . $row['size'];
+ if (strlen($row['mount_point']) > 0)
+ $config .= "," . $row['mount_point'];
+ if (strlen($row['options']) > 0)
+ $config .= "," . $row['options'];
+ }
+ $config .= "\n";
// vm list url. doesn't really fit anywhere, seems to be a tie between here and dozmod
-$configVars["SLX_PARTITION_TABLE"] = $config;
-unset($config); \ No newline at end of file
+ $configVars["SLX_PARTITION_TABLE"] = $config;
+ unset($config);
+}
diff --git a/modules-available/bootstrap_datepicker/config.json b/modules-available/bootstrap_datepicker/config.json
index d1261653..5a0c7960 100644
--- a/modules-available/bootstrap_datepicker/config.json
+++ b/modules-available/bootstrap_datepicker/config.json
@@ -1,3 +1,9 @@
{
- "dependencies" : []
+ "dependencies" : [],
+ "css": {
+ "style.css": true
+ },
+ "scripts": {
+ "clientscript.js": true
+ }
}
diff --git a/modules-available/bootstrap_dialog/config.json b/modules-available/bootstrap_dialog/config.json
index d1261653..5a0c7960 100644
--- a/modules-available/bootstrap_dialog/config.json
+++ b/modules-available/bootstrap_dialog/config.json
@@ -1,3 +1,9 @@
{
- "dependencies" : []
+ "dependencies" : [],
+ "css": {
+ "style.css": true
+ },
+ "scripts": {
+ "clientscript.js": true
+ }
}
diff --git a/modules-available/bootstrap_multiselect/config.json b/modules-available/bootstrap_multiselect/config.json
index d1261653..5a0c7960 100644
--- a/modules-available/bootstrap_multiselect/config.json
+++ b/modules-available/bootstrap_multiselect/config.json
@@ -1,3 +1,9 @@
{
- "dependencies" : []
+ "dependencies" : [],
+ "css": {
+ "style.css": true
+ },
+ "scripts": {
+ "clientscript.js": true
+ }
}
diff --git a/modules-available/bootstrap_switch/config.json b/modules-available/bootstrap_switch/config.json
index 9e26dfee..de4d37b4 100644
--- a/modules-available/bootstrap_switch/config.json
+++ b/modules-available/bootstrap_switch/config.json
@@ -1 +1,8 @@
-{} \ No newline at end of file
+{
+ "css": {
+ "style.css": true
+ },
+ "scripts": {
+ "clientscript.js": true
+ }
+} \ No newline at end of file
diff --git a/modules-available/bootstrap_timepicker/config.json b/modules-available/bootstrap_timepicker/config.json
index d1261653..5a0c7960 100644
--- a/modules-available/bootstrap_timepicker/config.json
+++ b/modules-available/bootstrap_timepicker/config.json
@@ -1,3 +1,9 @@
{
- "dependencies" : []
+ "dependencies" : [],
+ "css": {
+ "style.css": true
+ },
+ "scripts": {
+ "clientscript.js": true
+ }
}
diff --git a/modules-available/dozmod/inc/pagemailtemplates.inc.php b/modules-available/dozmod/inc/pagemailtemplates.inc.php
index ef52ec12..dc41d8c6 100644
--- a/modules-available/dozmod/inc/pagemailtemplates.inc.php
+++ b/modules-available/dozmod/inc/pagemailtemplates.inc.php
@@ -22,7 +22,7 @@ class Page_mail_templates extends Page
private function enrichHtml() {
/* for each template */
- foreach ($this->templates as $k => $t) {
+ foreach ($this->templates as &$t) {
$lis = "";
$optManVars = "";
$optVars = "";
@@ -36,16 +36,22 @@ class Page_mail_templates extends Page
}
/* also options for hidden inputs */
- $this->templates[$k]['html_availableVariables'] = $lis;
- $this->templates[$k]['html_mandatoryVariables'] = $optManVars;
- $this->templates[$k]['html_optionalVariables'] = $optVars;
+ $t['html_availableVariables'] = $lis;
+ $t['html_mandatoryVariables'] = $optManVars;
+ $t['html_optionalVariables'] = $optVars;
/* also for javascript */
- $this->templates[$k]['list_mandatoryVariables'] =
- implode(',', $this->templates[$k]['mandatory_variables']);
+ $t['list_mandatoryVariables'] =
+ implode(',', $t['mandatory_variables']);
- $this->templates[$k]['list_optionalVariables'] =
- implode(',', $this->templates[$k]['optional_variables']);
+ $t['list_optionalVariables'] =
+ implode(',', $t['optional_variables']);
+
+ settype($t['original'], 'bool');
+ settype($t['edit_version'], 'int');
+ settype($t['version'], 'int');
+ $t['modified'] = !$t['original'];
+ $t['conflict'] = !$t['original'] && $t['edit_version'] < $t['version'];
}
}
@@ -55,12 +61,29 @@ class Page_mail_templates extends Page
Render::addTemplate('templates', ['templates' => $this->templates]);
}
- private function handleSave() {
+ private function forcmp($string)
+ {
+ return trim(str_replace("\r\n", "\n", $string));
+ }
+
+ private function handleSave()
+ {
$data = Request::post('templates');
if (is_array($data)) {
$this->fetchTemplates();
foreach ($this->templates as &$template) {
if (isset($data[$template['name']])) {
+ if ($this->forcmp($template['template']) !== $this->forcmp($data[$template['name']]['template'])) {
+ if (empty($template['original_template'])) {
+ $template['original_template'] = $template['template'];
+ }
+ $template['edit_version'] = $template['version'];
+ }
+ $template['original'] = (empty($template['original_template']) && $template['original'])
+ || $this->forcmp($template['original_template']) === $this->forcmp($data[$template['name']]['template']);
+ if ($template['original']) {
+ $template['original_template'] = '';
+ }
$template['template'] = $data[$template['name']]['template'];
}
}
@@ -68,7 +91,6 @@ class Page_mail_templates extends Page
$data = json_encode(array('templates' => $this->templates));
Database::exec("UPDATE sat.configuration SET value = :value WHERE parameter = 'templates'", array('value' => $data));
Message::addSuccess('templates-saved');
-
} else {
Message::addError('nothing-submitted');
}
diff --git a/modules-available/dozmod/lang/de/messages.json b/modules-available/dozmod/lang/de/messages.json
index e72fb1be..47580bcb 100644
--- a/modules-available/dozmod/lang/de/messages.json
+++ b/modules-available/dozmod/lang/de/messages.json
@@ -2,8 +2,9 @@
"all-templates-reset": "Alle Templates wurden zur\u00fcckgesetzt",
"delete-images": "L\u00f6schung: {{0}}",
"dozmod-error": "Fehler bei der Kommunikation mit dem bwLehrpool-Suite server: {{0}}",
- "images-pending-delete-exist": "Zur L\u00f6schung markierte Abbilder: {{0}}",
+ "images-pending-delete-exist": "Zur L\u00f6schung markierte VM-Versionen: {{0}}",
"mail-config-saved": "Mail-Konfiguration gespeichert",
+ "no-expired-images": "Keine Abgelaufenen VM-Versionen",
"nothing-submitted": "Es wurde nichts \u00fcbermittelt",
"runtimelimits-config-saved": "Einstellungen gespeichert",
"templates-saved": "Templates wurden gespeichert",
diff --git a/modules-available/dozmod/lang/de/module.json b/modules-available/dozmod/lang/de/module.json
index 5840d8af..02e8c84d 100644
--- a/modules-available/dozmod/lang/de/module.json
+++ b/modules-available/dozmod/lang/de/module.json
@@ -2,6 +2,7 @@
"module_name": "bwLehrpool-Suite",
"page_title": "Verwalten der bwLehrpool-Suite",
"submenu_actionlog": "Aktions-Log",
+ "submenu_expiredimages": "Abgelaufene VM-Versionen",
"submenu_mailconfig": "Email-Konfiguration",
"submenu_runtime": "Limits und Standardwerte",
"submenu_templates": "Textbausteine f\u00fcr E-Mails",
diff --git a/modules-available/dozmod/lang/de/template-tags.json b/modules-available/dozmod/lang/de/template-tags.json
index 141ac68d..97ab0a21 100644
--- a/modules-available/dozmod/lang/de/template-tags.json
+++ b/modules-available/dozmod/lang/de/template-tags.json
@@ -16,7 +16,6 @@
"lang_defaultLecturePermissions": "F\u00fcr Veranstaltungen",
"lang_defaultPermissions": "Standardberechtigungen",
"lang_delButton": "Gew\u00e4hlte VMs endg\u00fcltig l\u00f6schen",
- "lang_description": "Beschreibung",
"lang_descriptionPermissionConfig": "Dies sind die Berechtigungen, die ein Benutzer standardm\u00e4\u00dfig f\u00fcr fremde VMs\/Veranstaltungen hat. Sie werden angewandt, wenn der Besitzer keine anderweitigen Berechtigungen w\u00e4hlt.",
"lang_descriptionRuntimeLimits": "Hier k\u00f6nnen Sie verschiedene Limits festlegen, z.B. wie lange eine VM nach dem Hochladen g\u00fcltig ist. Nach Ablauf dieses Zeitraums ist der Verantwortliche gezwungen, eine neue Version der VM hochzuladen. Damit k\u00f6nnen Sie das Ansammeln nicht mehr ben\u00f6tigter VMs eind\u00e4mmen. Weiterhin k\u00f6nnen Sie die maximale Anzahl gleichzeitiger Transfers pro Benutzer einschr\u00e4nken.\r\n\r\nVer\u00e4nderte Einstellungen wirken sich nicht auf bereits bestehende VMs aus.",
"lang_description_delete_images": "Diese Liste zeigt VMs, die entweder abgelaufen sind, oder deren Datei besch\u00e4digt, verschoben oder gel\u00f6scht wurde. Diese Images sind zur Zeit im Lehrpool nicht verf\u00fcgbar, ihre endg\u00fcltige L\u00f6schung muss aber manuell best\u00e4tigt werden, um gr\u00f6\u00dfere Katastrophen durch Softwarefehler, verstellte Systemuhren etc. zu vermeiden.",
@@ -34,6 +33,7 @@
"lang_image": "VM",
"lang_lastEditor": "Zuletzt bearbeitet von",
"lang_lastLogin": "Letzte Anmeldung",
+ "lang_latestVersion": "Neuste Version",
"lang_lecture": "Veranstaltung",
"lang_lecturePermissionAdmin": "Administration",
"lang_lecturePermissionEdit": "Bearbeiten",
@@ -46,7 +46,7 @@
"lang_maxLectureVisibility": "Sp\u00e4testes Enddatum einer Veranstaltung (Tage in der Zukunft)",
"lang_maxTransfers": "Maximale Zahl gleichzeitiger Up-\/Downloads pro Benutzer",
"lang_miscOptions": "Verschiedene Einstellungen",
- "lang_name": "Name",
+ "lang_modified": "Modifiziert",
"lang_organization": "Einrichtung",
"lang_organizationList": "Liste der Einrichtungen",
"lang_organizationListHeader": "Nutzungsrechte f\u00fcr den Satelliten festlegen",
@@ -56,6 +56,7 @@
"lang_placeholders": "Platzhalter",
"lang_port": "Port",
"lang_reallyResetTemplates": "Sind Sie sicher, dass Sie alle Texte l\u00f6schen und auf die Standardwerte zur\u00fccksetzen wollen?",
+ "lang_replaceWithOriginal": "Originaltext in Textbox laden",
"lang_replyTo": "Reply-To Adresse (z.B. Helpdesk)",
"lang_runtimeConfig": "Laufzeit-Konfiguration",
"lang_runtimeConfigHeadline": "Laufzeit-Konfiguration",
@@ -76,6 +77,7 @@
"lang_test": "Test-Mail senden",
"lang_testConfiguration": "Um die Konfiguration zu testen, geben Sie hier eine Empf\u00e4ngeradresse ein",
"lang_testRecipient": "Empf\u00e4nger",
+ "lang_thisVersion": "Diese Version",
"lang_updateTime": "Letzte Bearbeitung",
"lang_user": "Benutzername",
"lang_userId": "Benutzer-ID",
diff --git a/modules-available/dozmod/lang/en/messages.json b/modules-available/dozmod/lang/en/messages.json
index e1b5eea6..a17eae37 100644
--- a/modules-available/dozmod/lang/en/messages.json
+++ b/modules-available/dozmod/lang/en/messages.json
@@ -2,8 +2,9 @@
"all-templates-reset": "All templates have been reset",
"delete-images": "Delete: {{0}}",
"dozmod-error": "Error communicating with the bwLehrpool-Suite server: {{0}}",
- "images-pending-delete-exist": "Images marked for deletion: {{0}}",
+ "images-pending-delete-exist": "VMs marked for deletion: {{0}}",
"mail-config-saved": "Mail config saved",
+ "no-expired-images": "No expired VMs",
"nothing-submitted": "There was nothing submitted",
"runtimelimits-config-saved": "Configuration saved successfully",
"templates-saved": "Templates saved successfully",
diff --git a/modules-available/dozmod/lang/en/module.json b/modules-available/dozmod/lang/en/module.json
index 742aa848..17a2b838 100644
--- a/modules-available/dozmod/lang/en/module.json
+++ b/modules-available/dozmod/lang/en/module.json
@@ -2,6 +2,7 @@
"module_name": "bwLehrpool-Suite",
"page_title": "Manage the bwLehrpool-Suite",
"submenu_actionlog": "action log",
+ "submenu_expiredimages": "Expired VM versions",
"submenu_mailconfig": "email configuration",
"submenu_runtime": "limits and defaults",
"submenu_templates": "templates",
diff --git a/modules-available/dozmod/lang/en/template-tags.json b/modules-available/dozmod/lang/en/template-tags.json
index 5532bdcb..e2e085b4 100644
--- a/modules-available/dozmod/lang/en/template-tags.json
+++ b/modules-available/dozmod/lang/en/template-tags.json
@@ -16,7 +16,6 @@
"lang_defaultLecturePermissions": "For lectures",
"lang_defaultPermissions": "Default permissions",
"lang_delButton": "Permanently delete selected images",
- "lang_description": "This list shows images that reached their expire date, or where the image file in the file system is damaged or missing. You need to manually confirm the deletion of these files for safety reasons (software bugs, wrong system time, etc.).",
"lang_descriptionPermissionConfig": "These are the default permissions being used for VMs and lectures if the owner does not specify any.",
"lang_descriptionRuntimeLimits": "Here you can define some limits, e.g. how long a newly uploaded VM will be valid. This should make sure that you don't end up with a lot of old, unused VMs over time.\r\n\r\nModified settings won't apply for already existing VMs.",
"lang_description_delete_images": "This is a list of VMs that either expired, or where the disk image is damaged or missing. These VMs are not available in bwLehrpool currently, but you have to manually confirm the deletion of the disk images for safety reasons (clock skew etc.)",
@@ -34,6 +33,7 @@
"lang_image": "VM",
"lang_lastEditor": "Edited by",
"lang_lastLogin": "Last login",
+ "lang_latestVersion": "latest version",
"lang_lecture": "Lecture",
"lang_lecturePermissionAdmin": "Administrate",
"lang_lecturePermissionEdit": "Edit",
@@ -46,7 +46,7 @@
"lang_maxLectureVisibility": "Max time lecture end date may lie in the future (days)",
"lang_maxTransfers": "Max concurrent transfers per user",
"lang_miscOptions": "Misc options",
- "lang_name": "Name",
+ "lang_modified": "modified",
"lang_organization": "Organization",
"lang_organizationList": "List of organizations",
"lang_organizationListHeader": "Set access permissions for organizations",
@@ -56,6 +56,7 @@
"lang_placeholders": "Placeholders",
"lang_port": "Port",
"lang_reallyResetTemplates": "Are you sure you want to reset all texts to their default values?",
+ "lang_replaceWithOriginal": "load original text into text box",
"lang_replyTo": "Reply-To address",
"lang_runtimeConfig": "Limits and Defaults",
"lang_runtimeConfigHeadline": "Configure limits and defaults for bwLehrpool-Suite",
@@ -76,6 +77,7 @@
"lang_test": "Send test mail",
"lang_testConfiguration": "To test the configuration, enter a recipient address here",
"lang_testRecipient": "Recipient",
+ "lang_thisVersion": "this version",
"lang_updateTime": "Last update",
"lang_user": "User name",
"lang_userId": "User id",
diff --git a/modules-available/dozmod/page.inc.php b/modules-available/dozmod/page.inc.php
index 7f1e6ee3..9c247770 100644
--- a/modules-available/dozmod/page.inc.php
+++ b/modules-available/dozmod/page.inc.php
@@ -33,6 +33,7 @@ class Page_DozMod extends Page
}
/* add sub-menus */
+ Dashboard::addSubmenu('?do=dozmod&section=expiredimages', Dictionary::translate('submenu_expiredimages', true));
Dashboard::addSubmenu('?do=dozmod&section=mailconfig', Dictionary::translate('submenu_mailconfig', true));
Dashboard::addSubmenu('?do=dozmod&section=templates', Dictionary::translate('submenu_templates', true));
Dashboard::addSubmenu('?do=dozmod&section=runtimeconfig', Dictionary::translate('submenu_runtime', true));
@@ -46,35 +47,49 @@ class Page_DozMod extends Page
}
/* execute actions */
- $action = Request::post('action');
+ $action = Request::post('action', false, 'string');
if ($action === 'mail') {
$this->mailHandler();
- }
- if ($action === 'runtime') {
+ } elseif ($action === 'runtime') {
$this->runtimeHandler();
- }
- if ($action === 'delimages') {
+ } elseif ($action === 'delimages') {
$result = $this->handleDeleteImages();
if (!empty($result)) {
Message::addInfo('delete-images', $result);
}
Util::redirect('?do=DozMod');
+ } elseif ($action !== false) {
+ Util::traceError('Invalid action: ' . $action);
}
}
protected function doRender()
{
- $this->listDeletePendingImages();
-
/* different pages for different sections */
if ($this->subPage !== false) {
$this->subPage->doRender();
return;
}
- $section = Request::get('section', 'mailconfig', 'string');
+ $section = Request::get('section', false, 'string');
+ if ($section === false || $section === 'expiredimages') {
+ $expiredImages = $this->loadExpiredImages();
+ if ($section === false && empty($expiredImages)) {
+ $section = 'mailconfig';
+ } else {
+ $section = 'expiredimages';
+ }
+ }
+
+ if ($section === 'expiredimages') {
+ if (empty($expiredImages)) {
+ Message::addSuccess('no-expired-images');
+ } else {
+ Render::addTemplate('images-delete', array('images' => $expiredImages));
+ }
+ }
if ($section === 'mailconfig') {
// Mail config
$mailConf = Database::queryFirst('SELECT value FROM sat.configuration WHERE parameter = :param', array('param' => 'mailconfig'));
@@ -140,7 +155,7 @@ class Page_DozMod extends Page
Render::addTemplate('blockstats', $data);
}
- private function listDeletePendingImages()
+ private function loadExpiredImages()
{
$res = Database::simpleQuery("SELECT b.displayname,"
. " own.firstname, own.lastname, own.email,"
@@ -168,9 +183,7 @@ class Page_DozMod extends Page
$row['filesize'] = Util::readableFileSize($row['filesize']);
$rows[] = $row;
}
- if (empty($rows))
- return;
- Render::addTemplate('images-delete', array('images' => $rows));
+ return $rows;
}
private function cleanMailArray()
diff --git a/modules-available/dozmod/templates/templates.html b/modules-available/dozmod/templates/templates.html
index 3b2a003f..8a3caf26 100644
--- a/modules-available/dozmod/templates/templates.html
+++ b/modules-available/dozmod/templates/templates.html
@@ -14,6 +14,12 @@
<div id="frame_{{name}}" class="panel panel-default">
<div class="panel-heading">
<div class="panel-title">
+ {{#conflict}}
+ <span class="glyphicon glyphicon-exclamation-sign pull-left text-danger"></span>
+ {{/conflict}}
+ {{#modified}}
+ <span class="glyphicon glyphicon-pencil pull-left"></span>
+ {{/modified}}
<h4>
<a class="collapsed" data-toggle="collapse" data-parent="#accordion" href="#panel_{{name}}">
{{name}}
@@ -27,17 +33,6 @@
<div class="panel-body">
<div id="msgbox_{{name}}">
</div>
- <!--
- <label for="n_{{name}}">{{lang_name}}</label>
- <div class="form-group">
- <input id="n_{{name}}" name="templates[{{name}}][name]" value="{{name}}" class="form-control"/>
- </div>
-
- <label for="d_{{name}}">{{lang_description}}</label>
- <div class="form-group">
- <input id="d_{{name}}" name="templates[{{name}}][description]" value="{{description}}" class="form-control"/>
- </div>
- -->
<label for="ta_{{name}}">{{lang_template}}</label>
<div class="form-group">
@@ -56,6 +51,21 @@
<ul>
{{{html_availableVariables}}}
</ul>
+ {{#original_template}}
+ <textarea class="hidden" id="orig_{{name}}">{{original_template}}</textarea>
+ <div class="pull-right">
+ <a href="#" class="btn btn-default" onclick="$('#ta_{{name}}').val($('#orig_{{name}}').val());return false">
+ <span class="glyphicon glyphicon-refresh"></span>
+ {{lang_replaceWithOriginal}}
+ </a>
+ </div>
+ {{/original_template}}
+ <div class="small">
+ {{lang_modified}}: {{#modified}}<b>{{lang_yes}}</b>{{/modified}}{{^modified}}{{lang_no}}{{/modified}},
+ {{lang_hasNewer}}: {{#conflict}}<b>{{lang_yes}}</b>{{/conflict}}{{^conflict}}{{lang_no}}{{/conflict}},
+ {{lang_thisVersion}}: {{edit_version}},
+ {{lang_latestVersion}}: {{version}}
+ </div>
</div>
</div>
</div>
diff --git a/modules-available/exams/baseconfig/getconfig.inc.php b/modules-available/exams/baseconfig/getconfig.inc.php
index 5e506755..325badb0 100644
--- a/modules-available/exams/baseconfig/getconfig.inc.php
+++ b/modules-available/exams/baseconfig/getconfig.inc.php
@@ -1,16 +1,17 @@
<?php
-if (isset($configVars["SLX_LOCATIONS"])) {
- $locationIds = explode(' ', $configVars["SLX_LOCATIONS"]);
+$locations = ConfigHolder::get('SLX_LOCATIONS');
+if ($locations === false) {
+ $locationIds = [];
} else {
- $locationIds = array();
+ $locationIds = explode(' ', $locations);
}
if (Exams::isInExamMode($locationIds, $lectureId, $autoLogin)) {
- $configVars['SLX_EXAM'] = 'yes';
+ ConfigHolder::add('SLX_EXAM', 'yes', 100000);
if (strlen($lectureId) > 0) {
- $configVars['SLX_EXAM_START'] = $lectureId;
+ ConfigHolder::add('SLX_EXAM_START', $lectureId, 100000);
}
if (strlen($autoLogin) > 0) {
- $configVars['SLX_AUTOLOGIN'] = $autoLogin;
+ ConfigHolder::add('SLX_AUTOLOGIN', $autoLogin, 100000);
}
}
diff --git a/modules-available/exams/lang/de/template-tags.json b/modules-available/exams/lang/de/template-tags.json
index 6f4dce62..e011ee20 100644
--- a/modules-available/exams/lang/de/template-tags.json
+++ b/modules-available/exams/lang/de/template-tags.json
@@ -11,6 +11,7 @@
"lang_begin": "Beginn",
"lang_begin_date": "Beginn Datum",
"lang_begin_time": "Uhrzeit",
+ "lang_comfirmGlobalExam": "Wollen Sie wirklich eine globale Pr\u00fcfung definieren? Im gew\u00e4hlten Zeitraum werden s\u00e4mtliche R\u00e4ume in den Pr\u00fcfungsmodus geschaltet.",
"lang_deleteConfirmation": "Wirklich l\u00f6schen?",
"lang_description": "Beschreibung",
"lang_duration": "Dauer",
@@ -20,7 +21,7 @@
"lang_end_time": "Uhrzeit",
"lang_examModeDescription": "Hier k\u00f6nnen Sie bwLehrpool-R\u00e4ume zeitgesteuert in den Pr\u00fcfungsmodus versetzen. Im Pr\u00fcfungsmodus ist das Client-System st\u00e4rker abgeriegelt, sodass es sich zum Schreiben von E-Pr\u00fcfungen eignet. Nach dem Ein- bzw. Ausschalten des Pr\u00fcfungsmodus ist es notwendig, die Rechner in den betroffenen R\u00e4umen neuzustarten.",
"lang_global": "Global",
- "lang_headingAllExamLectures": "Liste ausstehender Pr\u00fcfungsveranstaltungen",
+ "lang_headingAllExamLectures": "Ausstehende Pr\u00fcfungsveranstaltungen (30 Tage)",
"lang_headingGraphicalOverview": "Grafische Darstellung",
"lang_headingMain": "bwLehrpool Pr\u00fcfungsmodus",
"lang_id": "ID",
@@ -29,6 +30,7 @@
"lang_location": "Raum\/Ort",
"lang_locationInfo": "W\u00e4hlen Sie hier die R\u00e4ume und Orte aus, die w\u00e4hrend des unten ausgew\u00e4hlten Zeitraums in den Pr\u00fcfungsmodus versetzt werden. Wenn sie hier keine Auswahl treffen, werden alle R\u00e4ume in den Pr\u00fcfungsmodus versetzt.",
"lang_locations": "R\u00e4ume\/Orte",
+ "lang_moreThanOneDay": "Mehr als ein Tag",
"lang_noDescription": "Keine Beschreibung",
"lang_none": "(Keine)",
"lang_timeFrame": "Zeitraum"
diff --git a/modules-available/exams/lang/en/template-tags.json b/modules-available/exams/lang/en/template-tags.json
index f91329c9..a4ae9325 100644
--- a/modules-available/exams/lang/en/template-tags.json
+++ b/modules-available/exams/lang/en/template-tags.json
@@ -11,6 +11,7 @@
"lang_begin": "Begin",
"lang_begin_date": "Begin Date",
"lang_begin_time": "Time",
+ "lang_comfirmGlobalExam": "Do you really want to create a global exam? Every single room will be set to lecture mode during the selected time period.",
"lang_deleteConfirmation": "Are you sure?",
"lang_description": "Description",
"lang_duration": "Duration",
@@ -20,7 +21,7 @@
"lang_end_time": "Time",
"lang_examModeDescription": "Here you can define time spans during which selected rooms will be set to exam mode. In exam mode, the client computers are more locked down than usual so it is suitable for writing electronic exams.",
"lang_global": "Global",
- "lang_headingAllExamLectures": "Upcoming lectures marked as exams",
+ "lang_headingAllExamLectures": "Upcoming lectures marked as exams (30 days)",
"lang_headingGraphicalOverview": "Graphical overview",
"lang_headingMain": "bwLehrpool exam mode",
"lang_id": "ID",
@@ -29,6 +30,7 @@
"lang_location": "Room\/Location",
"lang_locationInfo": "Select the rooms and locations you want to enable the exam mode in. Selecting nothing at all means that all clients will boot into exam mode during the given time period.",
"lang_locations": "Rooms\/Locations",
+ "lang_moreThanOneDay": "More than one day",
"lang_noDescription": "No description",
"lang_none": "(None)",
"lang_timeFrame": "Time frame"
diff --git a/modules-available/exams/page.inc.php b/modules-available/exams/page.inc.php
index 930ba62c..48af287a 100644
--- a/modules-available/exams/page.inc.php
+++ b/modules-available/exams/page.inc.php
@@ -3,9 +3,9 @@
class Page_Exams extends Page
{
var $action = false;
- var $exams;
- var $locations;
- var $lectures;
+ var $exams = [];
+ var $locations = [];
+ var $lectures = [];
private $currentExam;
private $rangeMin;
private $rangeMax;
@@ -35,7 +35,8 @@ class Page_Exams extends Page
. "NATURAL LEFT JOIN exams_x_location exl "
. "NATURAL LEFT JOIN location loc "
. "LEFT JOIN sat.lecture l USING (lectureid) "
- . "GROUP BY examid ");
+ . "GROUP BY examid "
+ . "ORDER BY examid ASC");
while ($exam = $tmp->fetch(PDO::FETCH_ASSOC)) {
$this->exams[] = $exam;
}
@@ -49,7 +50,8 @@ class Page_Exams extends Page
"INNER JOIN sat.user ON (user.userid = lecture.ownerid) " .
"NATURAL LEFT JOIN sat.lecture_x_location " .
"WHERE isexam <> 0 AND starttime < :rangeMax AND endtime > :rangeMin " .
- "GROUP BY lectureid",
+ "GROUP BY lectureid " .
+ "ORDER BY starttime ASC, displayname ASC",
['rangeMax' => $this->rangeMax, 'rangeMin' => $this->rangeMin]);
while ($lecture = $tmp->fetch(PDO::FETCH_ASSOC)) {
$this->lectures[] = $lecture;
diff --git a/modules-available/exams/templates/page-add-edit-exam.html b/modules-available/exams/templates/page-add-edit-exam.html
index 106c6641..bf000df5 100644
--- a/modules-available/exams/templates/page-add-edit-exam.html
+++ b/modules-available/exams/templates/page-add-edit-exam.html
@@ -110,7 +110,7 @@
<input type="hidden" name="action" value="save">
<input type="hidden" name="token" value="{{token}}">
<input type="hidden" name="examid" value="{{exam.examid}}">
- <button class="btn btn-success">{{lang_save}}</button>
+ <button class="btn btn-success" type="submit">{{lang_save}}</button>
</form>
<script type="application/javascript"><!--
@@ -206,5 +206,13 @@ document.addEventListener("DOMContentLoaded", function () {
end_time.change(startEndChanged);
$('#lecturelist').change(updateLectureInfo);
+ $('#tolleform').submit(function(ev) {
+ if ($('#locations option:selected').length === 0 && $('#locations option').length > 1) {
+ if (!confirm('{{lang_comfirmGlobalExam}}')) {
+ ev.preventDefault();
+ }
+ }
+ });
+
}, false);
// --></script>
diff --git a/modules-available/exams/templates/page-upcoming-lectures.html b/modules-available/exams/templates/page-upcoming-lectures.html
index a1867444..323b1017 100644
--- a/modules-available/exams/templates/page-upcoming-lectures.html
+++ b/modules-available/exams/templates/page-upcoming-lectures.html
@@ -17,7 +17,10 @@
</td>
<td width="30%" class="text-nowrap">
{{starttime_s}} &ensp; {{endtime_s}}
- <div class="small">{{duration_s}}</div>
+ <div class="small">
+ {{lang_duration}}: {{duration_s}}
+ {{^duration_s}}{{lang_moreThanOneDay}}{{/duration_s}}
+ </div>
</td>
<td width="20%">
<div class="pull-right text-nowrap">
diff --git a/modules-available/js_chart/config.json b/modules-available/js_chart/config.json
index 9e26dfee..de4d37b4 100644
--- a/modules-available/js_chart/config.json
+++ b/modules-available/js_chart/config.json
@@ -1 +1,8 @@
-{} \ No newline at end of file
+{
+ "css": {
+ "style.css": true
+ },
+ "scripts": {
+ "clientscript.js": true
+ }
+} \ No newline at end of file
diff --git a/modules-available/js_circles/config.json b/modules-available/js_circles/config.json
index 9e26dfee..de4d37b4 100644
--- a/modules-available/js_circles/config.json
+++ b/modules-available/js_circles/config.json
@@ -1 +1,8 @@
-{} \ No newline at end of file
+{
+ "css": {
+ "style.css": true
+ },
+ "scripts": {
+ "clientscript.js": true
+ }
+} \ No newline at end of file
diff --git a/modules-available/js_jqueryui/config.json b/modules-available/js_jqueryui/config.json
index d1261653..5a0c7960 100644
--- a/modules-available/js_jqueryui/config.json
+++ b/modules-available/js_jqueryui/config.json
@@ -1,3 +1,9 @@
{
- "dependencies" : []
+ "dependencies" : [],
+ "css": {
+ "style.css": true
+ },
+ "scripts": {
+ "clientscript.js": true
+ }
}
diff --git a/modules-available/js_jqueryui/style.css b/modules-available/js_jqueryui/style.css
index 3b7a2c17..be57c0fa 100755
--- a/modules-available/js_jqueryui/style.css
+++ b/modules-available/js_jqueryui/style.css
@@ -1339,37 +1339,7 @@ body .ui-tooltip { border-width:2px; }
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
}
-
-
-/*** Input field styling from Bootstrap **/
- input, textarea {
- -webkit-transition: border linear 0.2s, box-shadow linear 0.2s;
- -moz-transition: border linear 0.2s, box-shadow linear 0.2s;
- -ms-transition: border linear 0.2s, box-shadow linear 0.2s;
- -o-transition: border linear 0.2s, box-shadow linear 0.2s;
- transition: border linear 0.2s, box-shadow linear 0.2s;
- -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
- -moz-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
- box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
-}
-input:focus, textarea:focus {
- outline: 0;
- border-color: rgba(82, 168, 236, 0.8);
- -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6);
- -moz-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6);
- box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6);
-}
-input[type=file]:focus, input[type=checkbox]:focus, select:focus {
- -webkit-box-shadow: none;
- -moz-box-shadow: none;
- box-shadow: none;
- outline: 1px dotted #666;
-}
-
-input[type="text"],
-input[type="password"],
.ui-autocomplete-input,
-textarea,
.uneditable-input {
display: inline-block;
padding: 4px;
diff --git a/modules-available/js_moment/config.json b/modules-available/js_moment/config.json
index d1261653..5a0c7960 100644
--- a/modules-available/js_moment/config.json
+++ b/modules-available/js_moment/config.json
@@ -1,3 +1,9 @@
{
- "dependencies" : []
+ "dependencies" : [],
+ "css": {
+ "style.css": true
+ },
+ "scripts": {
+ "clientscript.js": true
+ }
}
diff --git a/modules-available/js_selectize/config.json b/modules-available/js_selectize/config.json
index d1261653..5a0c7960 100644
--- a/modules-available/js_selectize/config.json
+++ b/modules-available/js_selectize/config.json
@@ -1,3 +1,9 @@
{
- "dependencies" : []
+ "dependencies" : [],
+ "css": {
+ "style.css": true
+ },
+ "scripts": {
+ "clientscript.js": true
+ }
}
diff --git a/modules-available/js_stupidtable/config.json b/modules-available/js_stupidtable/config.json
index 9e26dfee..cf932d7e 100644
--- a/modules-available/js_stupidtable/config.json
+++ b/modules-available/js_stupidtable/config.json
@@ -1 +1,8 @@
-{} \ No newline at end of file
+{
+ "css": {
+ "style.css": true
+ },
+ "scripts": {
+ "clientscript.js": true
+ }
+} \ No newline at end of file
diff --git a/modules-available/js_vis/config.json b/modules-available/js_vis/config.json
index ccdf64b7..3b027d31 100644
--- a/modules-available/js_vis/config.json
+++ b/modules-available/js_vis/config.json
@@ -1,3 +1,9 @@
{
- "dependencies" : ["js_moment"]
+ "dependencies" : ["js_moment"],
+ "css": {
+ "style.css": true
+ },
+ "scripts": {
+ "clientscript.js": true
+ }
}
diff --git a/modules-available/js_weekcalendar/clientscript.js b/modules-available/js_weekcalendar/clientscript.js
new file mode 100755
index 00000000..28b9e3cf
--- /dev/null
+++ b/modules-available/js_weekcalendar/clientscript.js
@@ -0,0 +1,2968 @@
+/*
+ * jQuery.weekCalendar v2.0-dev
+ *
+ * for support join us at the google group:
+ * - http://groups.google.com/group/jquery-week-calendar
+ * have a look to the wiki for documentation:
+ * - http://wiki.github.com/themouette/jquery-week-calendar/
+ * something went bad ? report an issue:
+ * - http://github.com/themouette/jquery-week-calendar/issues
+ * get the last version on github:
+ * - http://github.com/themouette/jquery-week-calendar
+ *
+ * Copyright (c) 2009 Rob Monie
+ * Copyright (c) 2010 Julien MUETTON
+ * Dual licensed under the MIT and GPL licenses:
+ * http://www.opensource.org/licenses/mit-license.php
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * If you're after a monthly calendar plugin, check out this one :
+ * http://arshaw.com/fullcalendar/
+ */
+var startdate = 0;
+
+function SetUpDate(d) {
+ startdate = d.getTime() - new Date().getTime();
+}
+
+/**
+ *
+ * @return {Date}
+ */
+function MyDate() {
+ return new Date(startdate + new Date().getTime());
+}
+
+(function($) {
+
+ // check the jquery version
+ var _v = $.fn.jquery.split('.'),
+ _jQuery14OrLower = (10 * _v[0] + _v[1]) < 15;
+ $.widget('ui.weekCalendar', (function() {
+ var _currentAjaxCall, _hourLineTimeout;
+
+ return {
+ options: {
+ date: MyDate(),
+ timeFormat: null,
+ dateFormat: 'M d, Y',
+ alwaysDisplayTimeMinutes: true,
+ use24Hour: false,
+ daysToShow: 7,
+ minBodyHeight: 100,
+ firstDayOfWeek: function(calendar) {
+ if ($(calendar).weekCalendar('option', 'daysToShow') != 5) {
+ return 0;
+ } else {
+ //workweek
+ return 1;
+ }
+ }, // 0 = Sunday, 1 = Monday, 2 = Tuesday, ... , 6 = Saturday
+ useShortDayNames: false,
+ timeSeparator: ' to ',
+ startParam: 'start',
+ endParam: 'end',
+ businessHours: {start: 8, end: 18, limitDisplay: false},
+ newEventText: 'New Event',
+ timeslotHeight: 20,
+ defaultEventLength: 2,
+ timeslotsPerHour: 4,
+ minDate: null,
+ maxDate: null,
+ showHeader: true,
+ buttons: true,
+ buttonText: {
+ today: 'today',
+ lastWeek: 'previous',
+ nextWeek: 'next'
+ },
+ switchDisplay: {},
+ scrollToHourMillis: 500,
+ allowEventDelete: false,
+ allowCalEventOverlap: false,
+ overlapEventsSeparate: false,
+ totalEventsWidthPercentInOneColumn: 100,
+ readonly: false,
+ allowEventCreation: true,
+ hourLine: false,
+ deletable: function(calEvent, element) {
+ return true;
+ },
+ draggable: function(calEvent, element) {
+ return true;
+ },
+ resizable: function(calEvent, element) {
+ return true;
+ },
+ eventClick: function(calEvent, element, dayFreeBusyManager,
+ calendar, clickEvent) {
+ },
+ eventRender: function(calEvent, element) {
+ return element;
+ },
+ eventAfterRender: function(calEvent, element) {
+ return element;
+ },
+ eventRefresh: function(calEvent, element) {
+ return element;
+ },
+ eventDrag: function(calEvent, element) {
+ },
+ eventDrop: function(calEvent, element) {
+ },
+ eventResize: function(calEvent, element) {
+ },
+ eventNew: function(calEvent, element, dayFreeBusyManager,
+ calendar, mouseupEvent) {
+ },
+ eventMouseover: function(calEvent, $event) {
+ },
+ eventMouseout: function(calEvent, $event) {
+ },
+ eventDelete: function(calEvent, element, dayFreeBusyManager,
+ calendar, clickEvent) {
+ calendar.weekCalendar('removeEvent',calEvent.id);
+ },
+ calendarBeforeLoad: function(calendar) {
+ },
+ calendarAfterLoad: function(calendar) {
+ },
+ noEvents: function() {
+ },
+ eventHeader: function(calEvent, calendar) {
+ var options = calendar.weekCalendar('option');
+ var one_hour = 3600000;
+ var displayTitleWithTime = calEvent.end.getTime() - calEvent.start.getTime() <= (one_hour / options.timeslotsPerHour);
+ if (displayTitleWithTime) {
+ return calendar.weekCalendar(
+ 'formatTime', calEvent.start) +
+ ': ' + calEvent.title;
+ } else {
+ return calendar.weekCalendar(
+ 'formatTime', calEvent.start) +
+ options.timeSeparator +
+ calendar.weekCalendar(
+ 'formatTime', calEvent.end);
+ }
+ },
+ eventBody: function(calEvent, calendar) {
+ return calEvent.title;
+ },
+ shortMonths: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+ longMonths: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+ shortDays: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+ longDays: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+ /* multi-users options */
+ /**
+ * the available users for calendar.
+ * if you want to display users separately, enable the
+ * showAsSeparateUsers option.
+ * if you provide a list of user and do not enable showAsSeparateUsers
+ * option, then only the events that belongs to one or several of
+ * given users will be displayed
+ * @type {array}
+ */
+ users: [],
+ /**
+ * should the calendar be displayed with separate column for each
+ * users.
+ * note that this option does nothing if you do not provide at least
+ * one user.
+ * @type {boolean}
+ */
+ showAsSeparateUsers: true,
+ /**
+ * callback used to read user id from a user object.
+ * @param {Object} user the user to retrieve the id from.
+ * @param {number} index the user index from user list.
+ * @param {jQuery} calendar the calendar object.
+ * @return {int|String} the user id.
+ */
+ getUserId: function(user, index, calendar) {
+ return index;
+ },
+ /**
+ * callback used to read user name from a user object.
+ * @param {Object} user the user to retrieve the name from.
+ * @param {number} index the user index from user list.
+ * @param {jQuery} calendar the calendar object.
+ * @return {String} the user name.
+ */
+ getUserName: function(user, index, calendar) {
+ return user;
+ },
+ /**
+ * reads the id(s) of user(s) for who the event should be displayed.
+ * @param {Object} calEvent the calEvent to read informations from.
+ * @param {jQuery} calendar the calendar object.
+ * @return {number|String|Array} the user id(s) to appened events for.
+ */
+ getEventUserId: function(calEvent, calendar) {
+ return calEvent.userId;
+ },
+ /**
+ * sets user id(s) to the calEvent
+ * @param {Object} calEvent the calEvent to set informations to.
+ * @param {jQuery} calendar the calendar object.
+ * @return {Object} the calEvent with modified user id.
+ */
+ setEventUserId: function(userId, calEvent, calendar) {
+ calEvent.userId = userId;
+ return calEvent;
+ },
+ /* freeBusy options */
+ /**
+ * should the calendar display freebusys ?
+ * @type {boolean}
+ */
+ displayFreeBusys: false,
+ /**
+ * read the id(s) for who the freebusy is available
+ * @param {Object} calEvent the calEvent to read informations from.
+ * @param {jQuery} calendar the calendar object.
+ * @return {number|String|Array} the user id(s) to appened events for.
+ */
+ getFreeBusyUserId: function(calFreeBusy, calendar) {
+ return calFreeBusy.userId;
+ },
+ /**
+ * the default freeBusy object, used to manage default state
+ * @type {Object}
+ */
+ defaultFreeBusy: {free: false},
+ /**
+ * function used to display the freeBusy element
+ * @type {Function}
+ * @param {Object} freeBusy the freeBusy timeslot to render.
+ * @param {jQuery} $freeBusy the freeBusy HTML element.
+ * @param {jQuery} calendar the calendar element.
+ */
+ freeBusyRender: function(freeBusy, $freeBusy, calendar) {
+ if (!freeBusy.free) {
+ $freeBusy.addClass('free-busy-busy');
+ }
+ else {
+ $freeBusy.addClass('free-busy-free');
+ }
+ return $freeBusy;
+ },
+ /* other options */
+ /**
+ * true means start on first day of week, false means starts on
+ * startDate.
+ * @param {jQuery} calendar the calendar object.
+ * @type {Function|bool}
+ */
+ startOnFirstDayOfWeek: function(calendar) {
+ return $(calendar).weekCalendar('option', 'daysToShow') >= 5;
+ },
+ /**
+ * should the columns be rendered alternatively using odd/even
+ * class
+ * @type {boolean}
+ */
+ displayOddEven: false,
+ textSize: 13,
+ /**
+ * the title attribute for the calendar. possible placeholders are:
+ * <ul>
+ * <li>%start%</li>
+ * <li>%end%</li>
+ * <li>%date%</li>
+ * </ul>
+ * @type {Function|string}
+ * @param {number} option daysToShow.
+ * @return {String} the title attribute for the calendar.
+ */
+ title: '%start% - %end%',
+ /**
+ * default options to pass to callback
+ * you can pass a function returning an object or a litteral object
+ * @type {object|function(#calendar)}
+ */
+ jsonOptions: {},
+ headerSeparator: '<br />',
+ /**
+ * returns formatted header for day display
+ * @type {function(date,calendar)}
+ */
+ getHeaderDate: null,
+ preventDragOnEventCreation: false,
+ /**
+ * the event on which to bind calendar resize
+ * @type {string}
+ */
+ resizeEvent: 'resize.weekcalendar'
+ },
+
+ /***********************
+ * Initialise calendar *
+ ***********************/
+ _create: function() {
+ var self = this;
+ self._computeOptions();
+ self._setupEventDelegation();
+ self._renderCalendar();
+ self._loadCalEvents();
+ self._resizeCalendar();
+ self._scrollToHour(self.options.date.getHours(), true);
+
+ if (this.options.resizeEvent) {
+ $(window).unbind(this.options.resizeEvent);
+ $(window).bind(this.options.resizeEvent, function() {
+ self._resizeCalendar();
+ });
+ }
+
+ },
+
+ /********************
+ * public functions *
+ ********************/
+ /*
+ * Refresh the events for the currently displayed week.
+ */
+ refresh: function() {
+ //reload with existing week
+ this._loadCalEvents(this.element.data('startDate'));
+ },
+
+ resizeCalendar:function(){
+ this._resizeCalendar();
+ },
+ scrollToHour:function(){
+ this._scrollToHour(MyDate().getHours(), false);
+ },
+ /*
+ * Clear all events currently loaded into the calendar
+ */
+ clear: function() {
+ this._clearCalendar();
+ },
+
+ /*
+ * Go to this week
+ */
+ today: function() {
+ this._clearCalendar();
+ this._loadCalEvents(MyDate());
+ },
+
+ /*
+ * Go to the previous week relative to the currently displayed week
+ */
+ prevWeek: function() {
+ //minus more than 1 day to be sure we're in previous week - account for daylight savings or other anomolies
+ var newDate = new Date(this.element.data('startDate').getTime() - (MILLIS_IN_WEEK / 6));
+ this._clearCalendar();
+ this._loadCalEvents(newDate);
+ },
+
+ /*
+ * Go to the next week relative to the currently displayed week
+ */
+ nextWeek: function() {
+ //add 8 days to be sure of being in prev week - allows for daylight savings or other anomolies
+ var newDate = new Date(this.element.data('startDate').getTime() + MILLIS_IN_WEEK + MILLIS_IN_DAY);
+ this._clearCalendar();
+ this._loadCalEvents(newDate);
+ },
+
+ /*
+ * Reload the calendar to whatever week the date passed in falls on.
+ */
+ gotoWeek: function(date) {
+ this._clearCalendar();
+ this._loadCalEvents(date);
+ },
+
+ /*
+ * Reload the calendar to whatever week the date passed in falls on.
+ */
+ gotoDate: function(date) {
+ this._clearCalendar();
+ this._loadCalEvents(date);
+ },
+
+ /**
+ * change the number of days to show
+ */
+ setDaysToShow: function(daysToShow) {
+ var self = this;
+ var hour = self._getCurrentScrollHour();
+ self.options.daysToShow = daysToShow;
+ $(self.element).html('');
+ self._renderCalendar();
+ self._loadCalEvents();
+ self._resizeCalendar();
+ self._scrollToHour(hour, false);
+
+ if (this.options.resizeEvent) {
+ $(window).unbind(this.options.resizeEvent);
+ $(window).bind(this.options.resizeEvent, function() {
+ self._resizeCalendar();
+ });
+ }
+ },
+
+ /*
+ * Remove an event based on it's id
+ */
+ removeEvent: function(eventId) {
+
+ var self = this;
+
+ self.element.find('.wc-cal-event').each(function() {
+ if ($(this).data('calEvent').id === eventId) {
+ $(this).remove();
+ return false;
+ }
+ });
+
+ //this could be more efficient rather than running on all days regardless...
+ self.element.find('.wc-day-column-inner').each(function() {
+ self._adjustOverlappingEvents($(this));
+ });
+ },
+
+ /*
+ * Removes any events that have been added but not yet saved (have no id).
+ * This is useful to call after adding a freshly saved new event.
+ */
+ removeUnsavedEvents: function() {
+
+ var self = this;
+
+ self.element.find('.wc-new-cal-event').each(function() {
+ $(this).remove();
+ });
+
+ //this could be more efficient rather than running on all days regardless...
+ self.element.find('.wc-day-column-inner').each(function() {
+ self._adjustOverlappingEvents($(this));
+ });
+ },
+
+ /*
+ * update an event in the calendar. If the event exists it refreshes
+ * it's rendering. If it's a new event that does not exist in the calendar
+ * it will be added.
+ */
+ updateEvent: function(calEvent) {
+ this._updateEventInCalendar(calEvent);
+ },
+
+ /*
+ * Returns an array of timeslot start and end times based on
+ * the configured grid of the calendar. Returns in both date and
+ * formatted time based on the 'timeFormat' config option.
+ */
+ getTimeslotTimes: function(date) {
+ var options = this.options;
+ var firstHourDisplayed = options.businessHours.limitDisplay ? options.businessHours.start : 0;
+ var startDate = new Date(date.getFullYear(), date.getMonth(), date.getDate(), firstHourDisplayed);
+
+ var times = [],
+ startMillis = startDate.getTime();
+ for (var i = 0; i < options.timeslotsPerDay; i++) {
+ var endMillis = startMillis + options.millisPerTimeslot;
+ times[i] = {
+ start: new Date(startMillis),
+ startFormatted: this.formatTime(new Date(startMillis), options.timeFormat),
+ end: new Date(endMillis),
+ endFormatted: this.formatTime(new Date(endMillis), options.timeFormat)
+ };
+ startMillis = endMillis;
+ }
+ return times;
+ },
+
+ formatDate: function(date, format) {
+ if (format) {
+ return this._formatDate(date, format);
+ } else {
+ return this._formatDate(date, this.options.dateFormat);
+ }
+ },
+
+ formatTime: function(date, format) {
+ if (format) {
+ return this._formatDate(date, format);
+ } else if (this.options.timeFormat) {
+ return this._formatDate(date, this.options.timeFormat);
+ } else if (this.options.use24Hour) {
+ return this._formatDate(date, 'H:i');
+ } else {
+ return this._formatDate(date, 'h:i a');
+ }
+ },
+
+ serializeEvents: function() {
+ var self = this;
+ var calEvents = [];
+
+ self.element.find('.wc-cal-event').each(function() {
+ calEvents.push($(this).data('calEvent'));
+ });
+ return calEvents;
+ },
+
+ next: function() {
+ if (this._startOnFirstDayOfWeek()) {
+ return this.nextWeek();
+ }
+ var newDate = new Date(this.element.data('startDate').getTime());
+ newDate.setDate(newDate.getDate() + this.options.daysToShow);
+
+ this._clearCalendar();
+ this._loadCalEvents(newDate);
+ },
+
+ prev: function() {
+ if (this._startOnFirstDayOfWeek()) {
+ return this.prevWeek();
+ }
+ var newDate = new Date(this.element.data('startDate').getTime());
+ newDate.setDate(newDate.getDate() - this.options.daysToShow);
+
+ this._clearCalendar();
+ this._loadCalEvents(newDate);
+ },
+ getCurrentFirstDay: function() {
+ return this._dateFirstDayOfWeek(this.options.date || MyDate());
+ },
+ getCurrentLastDay: function() {
+ return this._addDays(this.getCurrentFirstDay(), this.options.daysToShow - 1);
+ },
+
+ /*********************
+ * private functions *
+ *********************/
+ _setOption: function(key, value) {
+ var self = this;
+ if (self.options[key] != value) {
+ // event callback change, no need to re-render the events
+ if (key == 'beforeEventNew') {
+ self.options[key] = value;
+ return;
+ }
+
+ // this could be made more efficient at some stage by caching the
+ // events array locally in a store but this should be done in conjunction
+ // with a proper binding model.
+
+ var currentEvents = self.element.find('.wc-cal-event').map(function() {
+ return $(this).data('calEvent');
+ });
+
+ var newOptions = {};
+ newOptions[key] = value;
+ self._renderEvents({events: currentEvents, options: newOptions}, self.element.find('.wc-day-column-inner'));
+ }
+ },
+
+ // compute dynamic options based on other config values
+ _computeOptions: function() {
+ var options = this.options;
+ if (options.businessHours.limitDisplay) {
+ options.timeslotsPerDay = options.timeslotsPerHour * (options.businessHours.end - options.businessHours.start);
+ options.millisToDisplay = (options.businessHours.end - options.businessHours.start) * 3600000; // 60 * 60 * 1000
+ options.millisPerTimeslot = options.millisToDisplay / options.timeslotsPerDay;
+ } else {
+ options.timeslotsPerDay = options.timeslotsPerHour * 24;
+ options.millisToDisplay = MILLIS_IN_DAY;
+ options.millisPerTimeslot = MILLIS_IN_DAY / options.timeslotsPerDay;
+ }
+ },
+
+ /*
+ * Resize the calendar scrollable height based on the provided function in options.
+ */
+ _resizeCalendar: function() {
+ var options = this.options;
+ if (options && $.isFunction(options.height)) {
+ var calendarHeight = options.height(this.element);
+ var headerHeight = this.element.find('.wc-header').outerHeight(true);
+ var navHeight = this.element.find('.wc-toolbar').outerHeight(true);
+ var scrollContainerHeight = Math.max(calendarHeight - navHeight - headerHeight, options.minBodyHeight);
+ var timeslotHeight = this.element.find('.wc-time-slots').outerHeight(true);
+ this.element.find('.wc-scrollable-grid').height(scrollContainerHeight);
+ if (timeslotHeight <= scrollContainerHeight) {
+ this.element.find('.wc-scrollbar-shim').width(0);
+ }
+ else {
+ this.element.find('.wc-scrollbar-shim').width(this._findScrollBarWidth());
+ }
+ this._trigger('resize', this.element);
+ }
+ },
+
+ _findScrollBarWidth: function() {
+ var parent = $('<div style="width:50px;height:50px;overflow:auto"><div/></div>').appendTo('body');
+ var child = parent.children();
+ var width = child.innerWidth() - child.height(99).innerWidth();
+ parent.remove();
+ return width || /* default to 16 that is the average */ 16;
+ },
+
+ /*
+ * configure calendar interaction events that are able to use event
+ * delegation for greater efficiency
+ */
+ _setupEventDelegation: function() {
+ var self = this;
+ var options = this.options;
+
+ this.element.click(function(event) {
+ var $target = $(event.target),
+ freeBusyManager;
+
+ // click is disabled
+ if ($target.data('preventClick')) {
+ return;
+ }
+
+ var $calEvent = $target.hasClass('wc-cal-event') ?
+ $target :
+ $target.parents('.wc-cal-event');
+ if (!$calEvent.length || !$calEvent.data('calEvent')) {
+ return;
+ }
+
+ freeBusyManager = self.getFreeBusyManagerForEvent($calEvent.data('calEvent'));
+
+ if (options.allowEventDelete && $target.hasClass('wc-cal-event-delete')) {
+ options.eventDelete($calEvent.data('calEvent'), $calEvent, freeBusyManager, self.element, event);
+ } else {
+ options.eventClick($calEvent.data('calEvent'), $calEvent, freeBusyManager, self.element, event);
+ }
+ }).mouseover(function(event) {
+ var $target = $(event.target);
+ var $calEvent = $target.hasClass('wc-cal-event') ?
+ $target :
+ $target.parents('.wc-cal-event');
+
+ if (!$calEvent.length || !$calEvent.data('calEvent')) {
+ return;
+ }
+
+ if (self._isDraggingOrResizing($calEvent)) {
+ return;
+ }
+
+ options.eventMouseover($calEvent.data('calEvent'), $calEvent, event);
+ }).mouseout(function(event) {
+ var $target = $(event.target);
+ var $calEvent = $target.hasClass('wc-cal-event') ?
+ $target :
+ $target.parents('.wc-cal-event');
+
+ if (!$calEvent.length || !$calEvent.data('calEvent')) {
+ return;
+ }
+
+ if (self._isDraggingOrResizing($calEvent)) {
+ return;
+ }
+
+ options.eventMouseout($calEvent.data('calEvent'), $calEvent, event);
+ });
+ },
+
+ /**
+ * check if a ui draggable or resizable is currently being dragged or
+ * resized.
+ */
+ _isDraggingOrResizing: function($target) {
+ return $target.hasClass('ui-draggable-dragging') ||
+ $target.hasClass('ui-resizable-resizing');
+ },
+
+ /*
+ * Render the main calendar layout
+ */
+ _renderCalendar: function() {
+ var $calendarContainer, $weekDayColumns;
+ var self = this;
+ var options = this.options;
+
+ $calendarContainer = $('<div class=\"ui-widget wc-container\">').appendTo(self.element);
+
+ //render the different parts
+ // nav links
+ self._renderCalendarButtons($calendarContainer);
+ // header
+ self._renderCalendarHeader($calendarContainer);
+ // body
+ self._renderCalendarBody($calendarContainer);
+
+ $weekDayColumns = $calendarContainer.find('.wc-day-column-inner');
+ $weekDayColumns.each(function(i, val) {
+ if (!options.readonly) {
+ self._addDroppableToWeekDay($(this));
+ if (options.allowEventCreation) {
+ self._setupEventCreationForWeekDay($(this));
+ }
+ }
+ });
+ },
+
+ /**
+ * render the nav buttons on top of the calendar
+ */
+ _renderCalendarButtons: function($calendarContainer) {
+ var self = this, options = this.options;
+ if ( !options.showHeader ) return;
+ if (options.buttons) {
+ var calendarNavHtml = '';
+
+ calendarNavHtml += '<div class=\"ui-widget-header wc-toolbar\">';
+ calendarNavHtml += '<div class=\"wc-display\"></div>';
+ calendarNavHtml += '<div class=\"wc-nav\">';
+ calendarNavHtml += '<button class=\"wc-prev\">' + options.buttonText.lastWeek + '</button>';
+ calendarNavHtml += '<button class=\"wc-today\">' + options.buttonText.today + '</button>';
+ calendarNavHtml += '<button class=\"wc-next\">' + options.buttonText.nextWeek + '</button>';
+ calendarNavHtml += '</div>';
+ calendarNavHtml += '<h1 class=\"wc-title\"></h1>';
+ calendarNavHtml += '</div>';
+
+ $(calendarNavHtml).appendTo($calendarContainer);
+
+ $calendarContainer.find('.wc-nav .wc-today')
+ .button({
+ icons: {primary: 'ui-icon-home'}})
+ .click(function() {
+ self.today();
+ return false;
+ });
+
+ $calendarContainer.find('.wc-nav .wc-prev')
+ .button({
+ text: false,
+ icons: {primary: 'ui-icon-seek-prev'}})
+ .click(function() {
+ self.element.weekCalendar('prev');
+ return false;
+ });
+
+ $calendarContainer.find('.wc-nav .wc-next')
+ .button({
+ text: false,
+ icons: {primary: 'ui-icon-seek-next'}})
+ .click(function() {
+ self.element.weekCalendar('next');
+ return false;
+ });
+
+ // now add buttons to switch display
+ if (this.options.switchDisplay && $.isPlainObject(this.options.switchDisplay)) {
+ var $container = $calendarContainer.find('.wc-display');
+ $.each(this.options.switchDisplay, function(label, option) {
+ var _id = 'wc-switch-display-' + option;
+ var _input = $('<input type="radio" id="' + _id + '" name="wc-switch-display" class="wc-switch-display"/>');
+ var _label = $('<label for="' + _id + '"></label>');
+ _label.html(label);
+ _input.val(option);
+ if (parseInt(self.options.daysToShow, 10) === parseInt(option, 10)) {
+ _input.attr('checked', 'checked');
+ }
+ $container
+ .append(_input)
+ .append(_label);
+ });
+ $container.find('input').change(function() {
+ self.setDaysToShow(parseInt($(this).val(), 10));
+ });
+ }
+ $calendarContainer.find('.wc-nav, .wc-display').buttonset();
+ var _height = $calendarContainer.find('.wc-nav').outerHeight();
+ $calendarContainer.find('.wc-title')
+ .height(_height)
+ .css('line-height', _height + 'px');
+ }else{
+ var calendarNavHtml = '';
+ calendarNavHtml += '<div class=\"ui-widget-header wc-toolbar\">';
+ calendarNavHtml += '<h1 class=\"wc-title\"></h1>';
+ calendarNavHtml += '</div>';
+ $(calendarNavHtml).appendTo($calendarContainer);
+
+ }
+ },
+
+ /**
+ * render the calendar header, including date and user header
+ */
+ _renderCalendarHeader: function($calendarContainer) {
+ var self = this, options = this.options,
+ showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length,
+ rowspan = '', colspan = '', calendarHeaderHtml;
+
+ if (showAsSeparatedUser) {
+ rowspan = ' rowspan=\"2\"';
+ colspan = ' colspan=\"' + options.users.length + '\" ';
+ }
+
+ //first row
+ calendarHeaderHtml = '<div class=\"ui-widget-content wc-header\">';
+ calendarHeaderHtml += '<table><tbody><tr><td class=\"wc-time-column-header\"></td>';
+ for (var i = 1; i <= options.daysToShow; i++) {
+ calendarHeaderHtml += '<td class=\"wc-day-column-header wc-day-' + i + '\"' + colspan + '></td>';
+ }
+ calendarHeaderHtml += '<td class=\"wc-scrollbar-shim\"' + rowspan + '></td></tr>';
+
+ //users row
+ if (showAsSeparatedUser) {
+ calendarHeaderHtml += '<tr><td class=\"wc-time-column-header\"></td>';
+ var uLength = options.users.length,
+ _headerClass = '';
+
+ for (var i = 1; i <= options.daysToShow; i++) {
+ for (var j = 0; j < uLength; j++) {
+ _headerClass = [];
+ if (j == 0) {
+ _headerClass.push('wc-day-column-first');
+ }
+ if (j == uLength - 1) {
+ _headerClass.push('wc-day-column-last');
+ }
+ if (!_headerClass.length) {
+ _headerClass = 'wc-day-column-middle';
+ }
+ else {
+ _headerClass = _headerClass.join(' ');
+ }
+ calendarHeaderHtml += '<td class=\"' + _headerClass + ' wc-user-header wc-day-' + i + ' wc-user-' + self._getUserIdFromIndex(j) + '\">';
+// calendarHeaderHtml+= "<div class=\"wc-user-header wc-day-" + i + " wc-user-" + self._getUserIdFromIndex(j) +"\" >";
+ calendarHeaderHtml += self._getUserName(j);
+// calendarHeaderHtml+= "</div>";
+ calendarHeaderHtml += '</td>';
+ }
+ }
+ calendarHeaderHtml += '</tr>';
+ }
+ //close the header
+ calendarHeaderHtml += '</tbody></table></div>';
+
+ $(calendarHeaderHtml).appendTo($calendarContainer);
+ },
+
+ /**
+ * render the calendar body.
+ * Calendar body is composed of several distinct parts.
+ * Each part is displayed in a separated row to ease rendering.
+ * for further explanations, see each part rendering function.
+ */
+ _renderCalendarBody: function($calendarContainer) {
+ var self = this, options = this.options,
+ showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length,
+ $calendarBody, $calendarTableTbody;
+ // create the structure
+ $calendarBody = '<div class=\"wc-scrollable-grid\">';
+ $calendarBody += '<table class=\"wc-time-slots\">';
+ $calendarBody += '<tbody>';
+ $calendarBody += '</tbody>';
+ $calendarBody += '</table>';
+ $calendarBody += '</div>';
+ $calendarBody = $($calendarBody);
+ $calendarTableTbody = $calendarBody.find('tbody');
+
+ self._renderCalendarBodyTimeSlots($calendarTableTbody);
+ self._renderCalendarBodyOddEven($calendarTableTbody);
+ self._renderCalendarBodyFreeBusy($calendarTableTbody);
+ self._renderCalendarBodyEvents($calendarTableTbody);
+
+ $calendarBody.appendTo($calendarContainer);
+
+ //set the column height
+ $calendarContainer.find('.wc-full-height-column').height(options.timeslotHeight * options.timeslotsPerDay);
+ //set the timeslot height
+ $calendarContainer.find('.wc-time-slot').height(options.timeslotHeight - 1); //account for border
+ //init the time row header height
+ /**
+ TODO if total height for an hour is less than 11px, there is a display problem.
+ Find a way to handle it
+ */
+ $calendarContainer.find('.wc-time-header-cell').css({
+ height: (options.timeslotHeight * options.timeslotsPerHour) - 11,
+ padding: 5
+ });
+ //add the user data to every impacted column
+ if (showAsSeparatedUser) {
+ for (var i = 0, uLength = options.users.length; i < uLength; i++) {
+ $calendarContainer.find('.wc-user-' + self._getUserIdFromIndex(i))
+ .data('wcUser', options.users[i])
+ .data('wcUserIndex', i)
+ .data('wcUserId', self._getUserIdFromIndex(i));
+ }
+ }
+ },
+
+ /**
+ * render the timeslots separation
+ */
+ _renderCalendarBodyTimeSlots: function($calendarTableTbody) {
+ var options = this.options,
+ renderRow, i, j,
+ showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length,
+ start = (options.businessHours.limitDisplay ? options.businessHours.start : 0),
+ end = (options.businessHours.limitDisplay ? options.businessHours.end : 24),
+ rowspan = 1;
+
+ //calculate the rowspan
+ if (options.displayOddEven) { rowspan += 1; }
+ if (options.displayFreeBusys) { rowspan += 1; }
+ if (rowspan > 1) {
+ rowspan = ' rowspan=\"' + rowspan + '\"';
+ }
+ else {
+ rowspan = '';
+ }
+
+ renderRow = '<tr class=\"wc-grid-row-timeslot\">';
+ renderRow += '<td class=\"wc-grid-timeslot-header\"' + rowspan + '></td>';
+ renderRow += '<td colspan=\"' + options.daysToShow * (showAsSeparatedUser ? options.users.length : 1) + '\">';
+ renderRow += '<div class=\"wc-no-height-wrapper wc-time-slot-wrapper\">';
+ renderRow += '<div class=\"wc-time-slots\">';
+
+ for (i = start; i < end; i++) {
+ for (j = 0; j < options.timeslotsPerHour - 1; j++) {
+ renderRow += '<div class=\"wc-time-slot\"></div>';
+ }
+ renderRow += '<div class=\"wc-time-slot wc-hour-end\"></div>';
+ }
+
+ renderRow += '</div>';
+ renderRow += '</div>';
+ renderRow += '</td>';
+ renderRow += '</tr>';
+
+ $(renderRow).appendTo($calendarTableTbody);
+ },
+
+ /**
+ * render the odd even columns
+ */
+ _renderCalendarBodyOddEven: function($calendarTableTbody) {
+ if (this.options.displayOddEven) {
+ var options = this.options,
+ renderRow = '<tr class=\"wc-grid-row-oddeven\">',
+ showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length,
+ oddEven,
+ // let's take advantage of the jquery ui framework
+ oddEvenClasses = {'odd': 'wc-column-odd', 'even': 'ui-state-hover wc-column-even'};
+
+ //now let's display oddEven placeholders
+ for (var i = 1; i <= options.daysToShow; i++) {
+ if (!showAsSeparatedUser) {
+ oddEven = (oddEven == 'odd' ? 'even' : 'odd');
+ renderRow += '<td class=\"wc-day-column day-' + i + '\">';
+ renderRow += '<div class=\"wc-no-height-wrapper wc-oddeven-wrapper\">';
+ renderRow += '<div class=\"wc-full-height-column ' + oddEvenClasses[oddEven] + '\"></div>';
+ renderRow += '</div>';
+ renderRow += '</td>';
+ }
+ else {
+ var uLength = options.users.length;
+ for (var j = 0; j < uLength; j++) {
+ oddEven = (oddEven == 'odd' ? 'even' : 'odd');
+ renderRow += '<td class=\"wc-day-column day-' + i + '\">';
+ renderRow += '<div class=\"wc-no-height-wrapper wc-oddeven-wrapper\">';
+ renderRow += '<div class=\"wc-full-height-column ' + oddEvenClasses[oddEven] + '\" ></div>';
+ renderRow += '</div>';
+ renderRow += '</td>';
+ }
+ }
+ }
+ renderRow += '</tr>';
+
+ $(renderRow).appendTo($calendarTableTbody);
+ }
+ },
+
+ /**
+ * render the freebusy placeholders
+ */
+ _renderCalendarBodyFreeBusy: function($calendarTableTbody) {
+ if (this.options.displayFreeBusys) {
+ var self = this, options = this.options,
+ renderRow = '<tr class=\"wc-grid-row-freebusy\">',
+ showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length;
+ renderRow += '</td>';
+
+ //now let's display freebusy placeholders
+ for (var i = 1; i <= options.daysToShow; i++) {
+ if (options.displayFreeBusys) {
+ if (!showAsSeparatedUser) {
+ renderRow += '<td class=\"wc-day-column day-' + i + '\">';
+ renderRow += '<div class=\"wc-no-height-wrapper wc-freebusy-wrapper\">';
+ renderRow += '<div class=\"wc-full-height-column wc-column-freebusy wc-day-' + i + '\"></div>';
+ renderRow += '</div>';
+ renderRow += '</td>';
+ }
+ else {
+ var uLength = options.users.length;
+ for (var j = 0; j < uLength; j++) {
+ renderRow += '<td class=\"wc-day-column day-' + i + '\">';
+ renderRow += '<div class=\"wc-no-height-wrapper wc-freebusy-wrapper\">';
+ renderRow += '<div class=\"wc-full-height-column wc-column-freebusy wc-day-' + i;
+ renderRow += ' wc-user-' + self._getUserIdFromIndex(j) + '\">';
+ renderRow += '</div>';
+ renderRow += '</div>';
+ renderRow += '</td>';
+ }
+ }
+ }
+ }
+
+ renderRow += '</tr>';
+
+ $(renderRow).appendTo($calendarTableTbody);
+ }
+ },
+
+ /**
+ * render the calendar body for event placeholders
+ */
+ _renderCalendarBodyEvents: function($calendarTableTbody) {
+ var self = this, options = this.options,
+ renderRow,
+ showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length,
+ start = (options.businessHours.limitDisplay ? options.businessHours.start : 0),
+ end = (options.businessHours.limitDisplay ? options.businessHours.end : 24);
+ renderRow = '<tr class=\"wc-grid-row-events\">';
+ renderRow += '<td class=\"wc-grid-timeslot-header\">';
+ for (var i = start; i < end; i++) {
+ var bhClass = (options.businessHours.start <= i && options.businessHours.end > i) ? 'ui-state-active wc-business-hours' : 'ui-state-default';
+ renderRow += '<div class=\"wc-hour-header ' + bhClass + '\">';
+ if (options.use24Hour) {
+ renderRow += '<div class=\"wc-time-header-cell\">' + self._24HourForIndex(i) + '</div>';
+ }
+ else {
+ renderRow += '<div class=\"wc-time-header-cell\">' + self._hourForIndex(i) + '<span class=\"wc-am-pm\">' + self._amOrPm(i) + '</span></div>';
+ }
+ renderRow += '</div>';
+ }
+ renderRow += '</td>';
+
+ //now let's display events placeholders
+ var _columnBaseClass = 'ui-state-default wc-day-column';
+ for (var i = 1; i <= options.daysToShow; i++) {
+ if (!showAsSeparatedUser) {
+ renderRow += '<td class=\"' + _columnBaseClass + ' wc-day-column-first wc-day-column-last day-' + i + '\">';
+ renderRow += '<div class=\"wc-full-height-column wc-day-column-inner day-' + i + '\"></div>';
+ renderRow += '</td>';
+ }
+ else {
+ var uLength = options.users.length;
+ var columnclass;
+ for (var j = 0; j < uLength; j++) {
+ columnclass = [];
+ if (j == 0) {
+ columnclass.push('wc-day-column-first');
+ }
+ if (j == uLength - 1) {
+ columnclass.push('wc-day-column-last');
+ }
+ if (!columnclass.length) {
+ columnclass = 'wc-day-column-middle';
+ }
+ else {
+ columnclass = columnclass.join(' ');
+ }
+ renderRow += '<td class=\"' + _columnBaseClass + ' ' + columnclass + ' day-' + i + '\">';
+ renderRow += '<div class=\"wc-full-height-column wc-day-column-inner day-' + i;
+ renderRow += ' wc-user-' + self._getUserIdFromIndex(j) + '\">';
+ renderRow += '</div>';
+ renderRow += '</td>';
+ }
+ }
+ }
+
+ renderRow += '</tr>';
+
+ $(renderRow).appendTo($calendarTableTbody);
+ },
+
+ /*
+ * setup mouse events for capturing new events
+ */
+ _setupEventCreationForWeekDay: function($weekDay) {
+ var self = this;
+ var options = this.options;
+ $weekDay.mousedown(function(event) {
+ var $target = $(event.target);
+ if ($target.hasClass('wc-day-column-inner')) {
+
+ var $newEvent = $('<div class=\"wc-cal-event wc-new-cal-event wc-new-cal-event-creating\"></div>');
+
+ $newEvent.css({lineHeight: (options.timeslotHeight - 2) + 'px', fontSize: (options.timeslotHeight / 2) + 'px'});
+ $target.append($newEvent);
+
+ var columnOffset = $target.offset().top;
+ var clickY = event.pageY - columnOffset;
+ var clickYRounded = (clickY - (clickY % options.timeslotHeight)) / options.timeslotHeight;
+ var topPosition = clickYRounded * options.timeslotHeight;
+ $newEvent.css({top: topPosition});
+
+ if (!options.preventDragOnEventCreation) {
+ $target.bind('mousemove.newevent', function(event) {
+ $newEvent.show();
+ $newEvent.addClass('ui-resizable-resizing');
+ var height = Math.round(event.pageY - columnOffset - topPosition);
+ var remainder = height % options.timeslotHeight;
+ //snap to closest timeslot
+ if (remainder < 0) {
+ var useHeight = height - remainder;
+ $newEvent.css('height', useHeight < options.timeslotHeight ? options.timeslotHeight : useHeight);
+ } else {
+ $newEvent.css('height', height + (options.timeslotHeight - remainder));
+ }
+ }).mouseup(function() {
+ $target.unbind('mousemove.newevent');
+ $newEvent.addClass('ui-corner-all');
+ });
+ }
+ }
+
+ }).mouseup(function(event) {
+ var $target = $(event.target);
+
+ var $weekDay = $target.closest('.wc-day-column-inner');
+ var $newEvent = $weekDay.find('.wc-new-cal-event-creating');
+
+ if ($newEvent.length) {
+ var createdFromSingleClick = !$newEvent.hasClass('ui-resizable-resizing');
+
+ //if even created from a single click only, default height
+ if (createdFromSingleClick) {
+ $newEvent.css({height: options.timeslotHeight * options.defaultEventLength}).show();
+ }
+ var top = parseInt($newEvent.css('top'));
+ var eventDuration = self._getEventDurationFromPositionedEventElement($weekDay, $newEvent, top);
+
+ $newEvent.remove();
+ var newCalEvent = {start: eventDuration.start, end: eventDuration.end, title: options.newEventText};
+ var showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length;
+
+ if (showAsSeparatedUser) {
+ newCalEvent = self._setEventUserId(newCalEvent, $weekDay.data('wcUserId'));
+ }
+ else if (!options.showAsSeparateUsers && options.users && options.users.length == 1) {
+ newCalEvent = self._setEventUserId(newCalEvent, self._getUserIdFromIndex(0));
+ }
+
+ var freeBusyManager = self.getFreeBusyManagerForEvent(newCalEvent);
+
+ var $renderedCalEvent = self._renderEvent(newCalEvent, $weekDay);
+
+ if (!options.allowCalEventOverlap) {
+ self._adjustForEventCollisions($weekDay, $renderedCalEvent, newCalEvent, newCalEvent);
+ self._positionEvent($weekDay, $renderedCalEvent);
+ } else {
+ self._adjustOverlappingEvents($weekDay);
+ }
+
+ var proceed = self._trigger('beforeEventNew', event, {
+ 'calEvent': newCalEvent,
+ 'createdFromSingleClick': createdFromSingleClick,
+ 'calendar': self.element
+ });
+ if (proceed) {
+ options.eventNew(newCalEvent, $renderedCalEvent, freeBusyManager, self.element, event);
+ }
+ else {
+ $($renderedCalEvent).remove();
+ }
+ }
+ });
+ },
+
+ /*
+ * load calendar events for the week based on the date provided
+ */
+ _loadCalEvents: function(dateWithinWeek) {
+
+ var date, weekStartDate, weekEndDate, $weekDayColumns;
+ var self = this;
+ var options = this.options;
+ date = this._fixMinMaxDate(dateWithinWeek || options.date);
+ // if date is not provided
+ // or was not set
+ // or is different than old one
+ if ((!date || !date.getTime) ||
+ (!options.date || !options.date.getTime) ||
+ date.getTime() != options.date.getTime()
+ ) {
+ // trigger the changedate event
+ this._trigger('changedate', this.element, date);
+ }
+ this.options.date = date;
+ weekStartDate = self._dateFirstDayOfWeek(date);
+ weekEndDate = self._dateLastMilliOfWeek(date);
+
+ options.calendarBeforeLoad(self.element);
+
+ self.element.data('startDate', weekStartDate);
+ self.element.data('endDate', weekEndDate);
+
+ $weekDayColumns = self.element.find('.wc-day-column-inner');
+
+ self._updateDayColumnHeader($weekDayColumns);
+
+ //load events by chosen means
+ if (typeof options.data == 'string') {
+ if (options.loading) {
+ options.loading(true);
+ }
+ if (_currentAjaxCall) {
+ // first abort current request.
+ if (!_jQuery14OrLower) {
+ _currentAjaxCall.abort();
+ } else {
+ // due to the fact that jquery 1.4 does not detect a request was
+ // aborted, we need to replace the onreadystatechange and
+ // execute the "complete" callback.
+ _currentAjaxCall.onreadystatechange = null;
+ _currentAjaxCall.abort();
+ _currentAjaxCall = null;
+ if (options.loading) {
+ options.loading(false);
+ }
+ }
+ }
+ var jsonOptions = self._getJsonOptions();
+ jsonOptions[options.startParam || 'start'] = Math.round(weekStartDate.getTime() / 1000);
+ jsonOptions[options.endParam || 'end'] = Math.round(weekEndDate.getTime() / 1000);
+ _currentAjaxCall = $.ajax({
+ url: options.data,
+ data: jsonOptions,
+ dataType: 'json',
+ error: function(XMLHttpRequest, textStatus, errorThrown) {
+ // only prevent error with jQuery 1.5
+ // see issue #34. thanks to dapplebeforedawn
+ // (https://github.com/themouette/jquery-week-calendar/issues#issue/34)
+ // for 1.5+, aborted request mean errorThrown == 'abort'
+ // for prior version it means !errorThrown && !XMLHttpRequest.status
+ // fixes #55
+ if (errorThrown != 'abort' && XMLHttpRequest.status != 0) {
+ alert('unable to get data, error:' + textStatus);
+ }
+ },
+ success: function(data) {
+ self._renderEvents(data, $weekDayColumns);
+ },
+ complete: function() {
+ _currentAjaxCall = null;
+ if (options.loading) {
+ options.loading(false);
+ }
+ }
+ });
+ }
+ else if ($.isFunction(options.data)) {
+ options.data(weekStartDate, weekEndDate,
+ function(data) {
+ self._renderEvents(data, $weekDayColumns);
+ });
+ }
+ else if (options.data) {
+ self._renderEvents(options.data, $weekDayColumns);
+ }
+
+ self._disableTextSelect($weekDayColumns);
+ },
+
+ /**
+ * Draws a thin line which indicates the current time.
+ */
+ _drawCurrentHourLine: function() {
+ var self = this;
+ var d = MyDate(),
+ options = this.options,
+ businessHours = options.businessHours;
+
+ self._scrollToHour(d.getHours() ,false);
+ // first, we remove the old hourline if it exists
+ $('.wc-hourline', this.element).remove();
+
+ // the line does not need to be displayed
+ if (businessHours.limitDisplay && d.getHours() > businessHours.end) {
+ return;
+ }
+
+ // then we recreate it
+ var paddingStart = businessHours.limitDisplay ? businessHours.start : 0;
+ var nbHours = d.getHours() - paddingStart + d.getMinutes() / 60;
+ var positionTop = nbHours * options.timeslotHeight * options.timeslotsPerHour;
+ var lineWidth = $('.wc-scrollable-grid .wc-today', this.element).width() + 3;
+ $('.wc-scrollable-grid .wc-today', this.element).append(
+ $('<div>', {
+ 'class': 'wc-hourline',
+ style: 'top: ' + positionTop + 'px; width: ' + lineWidth + 'px'
+ })
+ );
+ },
+
+ /*
+ * update the display of each day column header based on the calendar week
+ */
+ _updateDayColumnHeader: function($weekDayColumns) {
+ var self = this;
+ var options = this.options;
+ var currentDay = self._cloneDate(self.element.data('startDate'));
+ var showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length;
+ var todayClass = 'ui-state-active wc-today';
+
+ self.element.find('.wc-header td.wc-day-column-header').each(function(i, val) {
+ $(this).html(self._getHeaderDate(currentDay));
+ if (self._isToday(currentDay)) {
+ $(this).addClass(todayClass);
+ } else {
+ $(this).removeClass(todayClass);
+ }
+ currentDay = self._addDays(currentDay, 1);
+
+ });
+
+ currentDay = self._cloneDate(self.element.data('startDate'));
+ if (showAsSeparatedUser)
+ {
+ self.element.find('.wc-header td.wc-user-header').each(function(i, val) {
+ if (self._isToday(currentDay)) {
+ $(this).addClass(todayClass);
+ } else {
+ $(this).removeClass(todayClass);
+ }
+ currentDay = ((i + 1) % options.users.length) ? currentDay : self._addDays(currentDay, 1);
+ });
+ }
+
+ currentDay = self._cloneDate(self.element.data('startDate'));
+
+ $weekDayColumns.each(function(i, val) {
+
+ $(this).data('startDate', self._cloneDate(currentDay));
+ $(this).data('endDate', new Date(currentDay.getTime() + (MILLIS_IN_DAY)));
+ if (self._isToday(currentDay)) {
+ $(this).parent()
+ .addClass(todayClass)
+ .removeClass('ui-state-default');
+ } else {
+ $(this).parent()
+ .removeClass(todayClass)
+ .addClass('ui-state-default');
+ }
+
+ if (!showAsSeparatedUser || !((i + 1) % options.users.length)) {
+ currentDay = self._addDays(currentDay, 1);
+ }
+ });
+
+ //now update the freeBusy placeholders
+ if (options.displayFreeBusys) {
+ currentDay = self._cloneDate(self.element.data('startDate'));
+ self.element.find('.wc-grid-row-freebusy .wc-column-freebusy').each(function(i, val) {
+ $(this).data('startDate', self._cloneDate(currentDay));
+ $(this).data('endDate', new Date(currentDay.getTime() + (MILLIS_IN_DAY)));
+ if (!showAsSeparatedUser || !((i + 1) % options.users.length)) {
+ currentDay = self._addDays(currentDay, 1);
+ }
+ });
+ }
+
+ // now update the calendar title
+ if (this.options.title) {
+ var date = this.options.date,
+ start = self._cloneDate(self.element.data('startDate')),
+ end = self._dateLastDayOfWeek(new Date(this._cloneDate(self.element.data('endDate')).getTime() - (MILLIS_IN_DAY))),
+ title = this._getCalendarTitle(),
+ date_format = options.dateFormat;
+
+ // replace the placeholders contained in the title
+ title = title.replace('%start%', self._formatDate(start, date_format));
+ title = title.replace('%end%', self._formatDate(end, date_format));
+ title = title.replace('%date%', self._formatDate(date, date_format));
+
+ $('.wc-toolbar .wc-title', self.element).html(title);
+ }
+ //self._clearFreeBusys();
+ },
+
+ /**
+ * Gets the calendar raw title.
+ */
+ _getCalendarTitle: function() {
+ if ($.isFunction(this.options.title)) {
+ return this.options.title(this.options.daysToShow);
+ }
+
+ return this.options.title || '';
+ },
+
+ /**
+ * Render the events into the calendar
+ */
+ _renderEvents: function(data, $weekDayColumns) {
+ var self = this;
+ var options = this.options;
+ var eventsToRender, nbRenderedEvents = 0;
+
+ if (data.options) {
+ var updateLayout = false;
+ // update options
+ $.each(data.options, function(key, value) {
+ if (value !== options[key]) {
+ options[key] = value;
+ updateLayout = updateLayout || $.ui.weekCalendar.updateLayoutOptions[key];
+ }
+ });
+
+ self._computeOptions();
+
+ if (updateLayout) {
+ var hour = self._getCurrentScrollHour();
+ self.element.empty();
+ self._renderCalendar();
+ $weekDayColumns = self.element.find('.wc-time-slots .wc-day-column-inner');
+ self._updateDayColumnHeader($weekDayColumns);
+ self._resizeCalendar();
+ self._scrollToHour(hour, false);
+ }
+ }
+ this._clearCalendar();
+
+ if ($.isArray(data)) {
+ eventsToRender = self._cleanEvents(data);
+ } else if (data.events) {
+ eventsToRender = self._cleanEvents(data.events);
+ self._renderFreeBusys(data);
+ }
+
+ $.each(eventsToRender, function(i, calEvent) {
+ // render a multi day event as various event :
+ // thanks to http://github.com/fbeauchamp/jquery-week-calendar
+ if (!calEvent || !calEvent.start || !calEvent.end) return;
+ var initialStart = new Date(calEvent.start);
+ var initialEnd = new Date(calEvent.end);
+ var maxHour = self.options.businessHours.limitDisplay ? self.options.businessHours.end : 24;
+ var minHour = self.options.businessHours.limitDisplay ? self.options.businessHours.start : 0;
+ var start = new Date(initialStart);
+ var startDate = self._formatDate(start, 'Ymd');
+ var endDate = self._formatDate(initialEnd, 'Ymd');
+ var $weekDay;
+ var isMultiday = false;
+
+ while (startDate < endDate) {
+ calEvent.start = start;
+
+ // end of this virual calEvent is set to the end of the day
+ calEvent.end.setFullYear(start.getFullYear());
+ calEvent.end.setDate(start.getDate());
+ calEvent.end.setMonth(start.getMonth());
+ calEvent.end.setHours(maxHour, 0, 0);
+
+ if (($weekDay = self._findWeekDayForEvent(calEvent, $weekDayColumns))) {
+ self._renderEvent(calEvent, $weekDay);
+ nbRenderedEvents += 1;
+ }
+
+ // start is set to the begin of the new day
+ start.setDate(start.getDate() + 1);
+ start.setHours(minHour, 0, 0);
+
+ startDate = self._formatDate(start, 'Ymd');
+ isMultiday = true;
+ }
+
+ if (start <= initialEnd) {
+ calEvent.start = start;
+ calEvent.end = initialEnd;
+
+ if (((isMultiday && calEvent.start.getTime() != calEvent.end.getTime()) || !isMultiday) && ($weekDay = self._findWeekDayForEvent(calEvent, $weekDayColumns))) {
+ self._renderEvent(calEvent, $weekDay);
+ nbRenderedEvents += 1;
+ }
+ }
+
+ // put back the initial start date
+ calEvent.start = initialStart;
+ });
+
+ $weekDayColumns.each(function() {
+ self._adjustOverlappingEvents($(this));
+ });
+
+ options.calendarAfterLoad(self.element);
+
+ if (self._hourLineTimeout) {
+ clearInterval(self._hourLineTimeout);
+ self._hourLineTimeout = false;
+ }
+
+ if (options.hourLine) {
+ self._drawCurrentHourLine();
+
+ self._hourLineTimeout = setInterval(function() {
+ self._drawCurrentHourLine();
+ }, 60 * 1000); // redraw the line each minute
+ }
+
+ !nbRenderedEvents && options.noEvents();
+ },
+
+ /*
+ * Render a specific event into the day provided. Assumes correct
+ * day for calEvent date
+ */
+ _renderEvent: function(calEvent, $weekDay) {
+ var self = this;
+ var options = this.options;
+ if (calEvent.start.getTime() > calEvent.end.getTime()) {
+ return; // can't render a negative height
+ }
+
+ var eventClass, eventHtml, $calEventList, $modifiedEvent;
+
+ eventClass = calEvent.id ? 'wc-cal-event' : 'wc-cal-event wc-new-cal-event';
+ eventHtml = '<div class=\"' + eventClass + ' ui-corner-all\">';
+ eventHtml += '<div class=\"wc-time ui-corner-top\"></div>';
+ eventHtml += '<div class=\"wc-title\"></div></div>';
+
+ $weekDay.each(function() {
+ var $calEvent = $(eventHtml);
+ $modifiedEvent = options.eventRender(calEvent, $calEvent);
+ $calEvent = $modifiedEvent ? $modifiedEvent.appendTo($(this)) : $calEvent.appendTo($(this));
+ $calEvent.css({lineHeight: (options.textSize + 2) + 'px', fontSize: options.textSize + 'px'});
+
+ self._refreshEventDetails(calEvent, $calEvent);
+ self._positionEvent($(this), $calEvent);
+
+ //add to event list
+ if ($calEventList) {
+ $calEventList = $calEventList.add($calEvent);
+ }
+ else {
+ $calEventList = $calEvent;
+ }
+ });
+ $calEventList.show();
+
+ if (!options.readonly && options.resizable(calEvent, $calEventList)) {
+ self._addResizableToCalEvent(calEvent, $calEventList, $weekDay);
+ }
+ if (!options.readonly && options.draggable(calEvent, $calEventList)) {
+ self._addDraggableToCalEvent(calEvent, $calEventList);
+ }
+ options.eventAfterRender(calEvent, $calEventList);
+
+ return $calEventList;
+
+ },
+ addEvent: function() {
+ return this._renderEvent.apply(this, arguments);
+ },
+
+ _adjustOverlappingEvents: function($weekDay) {
+ var self = this;
+ if (self.options.allowCalEventOverlap) {
+ var groupsList = self._groupOverlappingEventElements($weekDay);
+ $.each(groupsList, function() {
+ var curGroups = this;
+ $.each(curGroups, function(groupIndex) {
+ var curGroup = this;
+
+ // do we want events to be displayed as overlapping
+ if (self.options.overlapEventsSeparate) {
+ var newWidth = self.options.totalEventsWidthPercentInOneColumn / curGroups.length;
+ var newLeft = groupIndex * newWidth;
+ } else {
+ // TODO what happens when the group has more than 10 elements
+ var newWidth = self.options.totalEventsWidthPercentInOneColumn - ((curGroups.length - 1) * 10);
+ var newLeft = groupIndex * 10;
+ }
+ $.each(curGroup, function() {
+ // bring mouseovered event to the front
+ if (!self.options.overlapEventsSeparate) {
+ $(this).bind('mouseover.z-index', function() {
+ var $elem = $(this);
+ $.each(curGroup, function() {
+ $(this).css({'z-index': '1'});
+ });
+ $elem.css({'z-index': '3'});
+ });
+ }
+ $(this).css({width: newWidth + '%', left: newLeft + '%', right: 0});
+ });
+ });
+ });
+ }
+ },
+
+
+ /*
+ * Find groups of overlapping events
+ */
+ _groupOverlappingEventElements: function($weekDay) {
+ var $events = $weekDay.find('.wc-cal-event:visible');
+ var sortedEvents = $events.sort(function(a, b) {
+ return $(a).data('calEvent').start.getTime() - $(b).data('calEvent').start.getTime();
+ });
+
+ var lastEndTime = new Date(0, 0, 0);
+ var groups = [];
+ var curGroups = [];
+ var $curEvent;
+ $.each(sortedEvents, function() {
+ $curEvent = $(this);
+ //checks, if the current group list is not empty, if the overlapping is finished
+ if (curGroups.length > 0) {
+ if (lastEndTime.getTime() <= $curEvent.data('calEvent').start.getTime()) {
+ //finishes the current group list by adding it to the resulting list of groups and cleans it
+
+ groups.push(curGroups);
+ curGroups = [];
+ }
+ }
+
+ //finds the first group to fill with the event
+ for (var groupIndex = 0; groupIndex < curGroups.length; groupIndex++) {
+ if (curGroups[groupIndex].length > 0) {
+ //checks if the event starts after the end of the last event of the group
+ if (curGroups[groupIndex][curGroups[groupIndex].length - 1].data('calEvent').end.getTime() <= $curEvent.data('calEvent').start.getTime()) {
+ curGroups[groupIndex].push($curEvent);
+ if (lastEndTime.getTime() < $curEvent.data('calEvent').end.getTime()) {
+ lastEndTime = $curEvent.data('calEvent').end;
+ }
+ return;
+ }
+ }
+ }
+ //if not found, creates a new group
+ curGroups.push([$curEvent]);
+ if (lastEndTime.getTime() < $curEvent.data('calEvent').end.getTime()) {
+ lastEndTime = $curEvent.data('calEvent').end;
+ }
+ });
+ //adds the last groups in result
+ if (curGroups.length > 0) {
+ groups.push(curGroups);
+ }
+ return groups;
+ },
+
+
+ /*
+ * find the weekday in the current calendar that the calEvent falls within
+ */
+ _findWeekDayForEvent: function(calEvent, $weekDayColumns) {
+
+ var $weekDay,
+ options = this.options,
+ showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length,
+ user_ids = this._getEventUserId(calEvent);
+
+ if (!$.isArray(user_ids)) {
+ user_ids = [user_ids];
+ }
+
+ $weekDayColumns.each(function(index, curDay) {
+ if ($(this).data('startDate').getTime() <= calEvent.start.getTime() &&
+ $(this).data('endDate').getTime() >= calEvent.end.getTime() &&
+ (!showAsSeparatedUser || $.inArray($(this).data('wcUserId'), user_ids) !== -1)
+ ) {
+ if ($weekDay) {
+ $weekDay = $weekDay.add($(curDay));
+ }
+ else {
+ $weekDay = $(curDay);
+ }
+ }
+ });
+
+ return $weekDay;
+ },
+
+ /*
+ * update the events rendering in the calendar. Add if does not yet exist.
+ */
+ _updateEventInCalendar: function(calEvent) {
+ var self = this;
+ self._cleanEvent(calEvent);
+
+ if (calEvent.id) {
+ self.element.find('.wc-cal-event').each(function() {
+ if ($(this).data('calEvent').id === calEvent.id || $(this).hasClass('wc-new-cal-event')) {
+ $(this).remove();
+ // return false;
+ }
+ });
+ }
+
+ var $weekDays = self._findWeekDayForEvent(calEvent, self.element.find('.wc-grid-row-events .wc-day-column-inner'));
+ if ($weekDays) {
+ $weekDays.each(function(index, weekDay) {
+ var $weekDay = $(weekDay);
+ var $calEvent = self._renderEvent(calEvent, $weekDay);
+ self._adjustForEventCollisions($weekDay, $calEvent, calEvent, calEvent);
+ self._refreshEventDetails(calEvent, $calEvent);
+ self._positionEvent($weekDay, $calEvent);
+ self._adjustOverlappingEvents($weekDay);
+ });
+ }
+ },
+
+ /*
+ * Position the event element within the weekday based on it's start / end dates.
+ */
+ _positionEvent: function($weekDay, $calEvent) {
+ var options = this.options;
+ var calEvent = $calEvent.data('calEvent');
+ var pxPerMillis = $weekDay.height() / options.millisToDisplay;
+ var firstHourDisplayed = options.businessHours.limitDisplay ? options.businessHours.start : 0;
+ var startMillis = this._getDSTdayShift(calEvent.start).getTime() - this._getDSTdayShift(new Date(calEvent.start.getFullYear(), calEvent.start.getMonth(), calEvent.start.getDate(), firstHourDisplayed)).getTime();
+ var eventMillis = this._getDSTdayShift(calEvent.end).getTime() - this._getDSTdayShift(calEvent.start).getTime();
+ var pxTop = pxPerMillis * startMillis;
+ var pxHeight = pxPerMillis * eventMillis;
+ //var pxHeightFallback = pxPerMillis * (60 / options.timeslotsPerHour) * 60 * 1000;
+ $calEvent.css({top: pxTop, height: pxHeight || (pxPerMillis * 3600000 / options.timeslotsPerHour)});
+ },
+
+ /*
+ * Determine the actual start and end times of a calevent based on it's
+ * relative position within the weekday column and the starting hour of the
+ * displayed calendar.
+ */
+ _getEventDurationFromPositionedEventElement: function($weekDay, $calEvent, top) {
+ var options = this.options;
+ var startOffsetMillis = options.businessHours.limitDisplay ? options.businessHours.start * 3600000 : 0;
+ var start = new Date($weekDay.data('startDate').getTime() + startOffsetMillis + Math.round(top / options.timeslotHeight) * options.millisPerTimeslot);
+ var end = new Date(start.getTime() + ($calEvent.height() / options.timeslotHeight) * options.millisPerTimeslot);
+ return {start: this._getDSTdayShift(start, -1), end: this._getDSTdayShift(end, -1)};
+ },
+
+ /*
+ * If the calendar does not allow event overlap, adjust the start or end date if necessary to
+ * avoid overlapping of events. Typically, shortens the resized / dropped event to it's max possible
+ * duration based on the overlap. If no satisfactory adjustment can be made, the event is reverted to
+ * it's original location.
+ */
+ _adjustForEventCollisions: function($weekDay, $calEvent, newCalEvent, oldCalEvent, maintainEventDuration) {
+ var options = this.options;
+
+ if (options.allowCalEventOverlap) {
+ return;
+ }
+ var adjustedStart, adjustedEnd;
+ var self = this;
+
+ $weekDay.find('.wc-cal-event').not($calEvent).each(function() {
+ var currentCalEvent = $(this).data('calEvent');
+
+ //has been dropped onto existing event overlapping the end time
+ if (newCalEvent.start.getTime() < currentCalEvent.end.getTime() &&
+ newCalEvent.end.getTime() >= currentCalEvent.end.getTime()) {
+
+ adjustedStart = currentCalEvent.end;
+ }
+
+
+ //has been dropped onto existing event overlapping the start time
+ if (newCalEvent.end.getTime() > currentCalEvent.start.getTime() &&
+ newCalEvent.start.getTime() <= currentCalEvent.start.getTime()) {
+
+ adjustedEnd = currentCalEvent.start;
+ }
+ //has been dropped inside existing event with same or larger duration
+ if (oldCalEvent.resizable == false ||
+ (newCalEvent.end.getTime() <= currentCalEvent.end.getTime() &&
+ newCalEvent.start.getTime() >= currentCalEvent.start.getTime())) {
+
+ adjustedStart = oldCalEvent.start;
+ adjustedEnd = oldCalEvent.end;
+ return false;
+ }
+
+ });
+
+
+ newCalEvent.start = adjustedStart || newCalEvent.start;
+
+ if (adjustedStart && maintainEventDuration) {
+ newCalEvent.end = new Date(adjustedStart.getTime() + (oldCalEvent.end.getTime() - oldCalEvent.start.getTime()));
+ self._adjustForEventCollisions($weekDay, $calEvent, newCalEvent, oldCalEvent);
+ } else {
+ newCalEvent.end = adjustedEnd || newCalEvent.end;
+ }
+
+
+ //reset if new cal event has been forced to zero size
+ if (newCalEvent.start.getTime() >= newCalEvent.end.getTime()) {
+ newCalEvent.start = oldCalEvent.start;
+ newCalEvent.end = oldCalEvent.end;
+ }
+
+ $calEvent.data('calEvent', newCalEvent);
+ },
+
+ /**
+ * Add draggable capabilities to an event
+ */
+ _addDraggableToCalEvent: function(calEvent, $calEvent) {
+ var options = this.options;
+
+ $calEvent.draggable({
+ handle: '.wc-time',
+ containment: 'div.wc-time-slots',
+ snap: '.wc-day-column-inner',
+ snapMode: 'inner',
+ snapTolerance: options.timeslotHeight - 1,
+ revert: 'invalid',
+ opacity: 0.5,
+ grid: [$calEvent.outerWidth() + 1, options.timeslotHeight],
+ start: function(event, ui) {
+ var $calEvent = ui.draggable || ui.helper;
+ options.eventDrag(calEvent, $calEvent);
+ }
+ });
+ },
+
+ /*
+ * Add droppable capabilites to weekdays to allow dropping of calEvents only
+ */
+ _addDroppableToWeekDay: function($weekDay) {
+ var self = this;
+ var options = this.options;
+ $weekDay.droppable({
+ accept: '.wc-cal-event',
+ drop: function(event, ui) {
+ var $calEvent = ui.draggable;
+ var top = Math.round(parseInt(ui.position.top));
+ var eventDuration = self._getEventDurationFromPositionedEventElement($weekDay, $calEvent, top);
+ var calEvent = $calEvent.data('calEvent');
+ var newCalEvent = $.extend(true, {}, calEvent, {start: eventDuration.start, end: eventDuration.end});
+ var showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length;
+ if (showAsSeparatedUser) {
+ // we may have dragged the event on column with a new user.
+ // nice way to handle that is:
+ // - get the newly dragged on user
+ // - check if user is part of the event
+ // - if yes, nothing changes, if not, find the old owner to remove it and add new one
+ var newUserId = $weekDay.data('wcUserId');
+ var userIdList = self._getEventUserId(calEvent);
+ var oldUserId = $(ui.draggable.parents('.wc-day-column-inner').get(0)).data('wcUserId');
+ if (!$.isArray(userIdList)) {
+ userIdList = [userIdList];
+ }
+ if ($.inArray(newUserId, userIdList) == -1) {
+ // remove old user
+ var _index = $.inArray(oldUserId, userIdList);
+ userIdList.splice(_index, 1);
+ // add new user ?
+ if ($.inArray(newUserId, userIdList) == -1) {
+ userIdList.push(newUserId);
+ }
+ }
+ newCalEvent = self._setEventUserId(newCalEvent, ((userIdList.length == 1) ? userIdList[0] : userIdList));
+ }
+ self._adjustForEventCollisions($weekDay, $calEvent, newCalEvent, calEvent, true);
+ var $weekDayColumns = self.element.find('.wc-day-column-inner');
+
+ //trigger drop callback
+ options.eventDrop(newCalEvent, calEvent, $calEvent);
+
+ var $newEvent = self._renderEvent(newCalEvent, self._findWeekDayForEvent(newCalEvent, $weekDayColumns));
+ $calEvent.hide();
+
+ $calEvent.data('preventClick', true);
+
+ var $weekDayOld = self._findWeekDayForEvent($calEvent.data('calEvent'), self.element.find('.wc-time-slots .wc-day-column-inner'));
+
+ if ($weekDayOld.data('startDate') != $weekDay.data('startDate')) {
+ self._adjustOverlappingEvents($weekDayOld);
+ }
+ self._adjustOverlappingEvents($weekDay);
+
+ setTimeout(function() {
+ $calEvent.remove();
+ }, 1000);
+
+ }
+ });
+ },
+
+ /*
+ * Add resizable capabilities to a calEvent
+ */
+ _addResizableToCalEvent: function(calEvent, $calEvent, $weekDay) {
+ var self = this;
+ var options = this.options;
+ $calEvent.resizable({
+ grid: options.timeslotHeight,
+ containment: $weekDay,
+ handles: 's',
+ minHeight: options.timeslotHeight,
+ stop: function(event, ui) {
+ var $calEvent = ui.element;
+ var newEnd = new Date($calEvent.data('calEvent').start.getTime() + Math.max(1, Math.round(ui.size.height / options.timeslotHeight)) * options.millisPerTimeslot);
+ if (self._needDSTdayShift($calEvent.data('calEvent').start, newEnd))
+ newEnd = self._getDSTdayShift(newEnd, -1);
+ var newCalEvent = $.extend(true, {}, calEvent, {start: calEvent.start, end: newEnd});
+ self._adjustForEventCollisions($weekDay, $calEvent, newCalEvent, calEvent);
+
+ //trigger resize callback
+ options.eventResize(newCalEvent, calEvent, $calEvent);
+ self._refreshEventDetails(newCalEvent, $calEvent);
+ self._positionEvent($weekDay, $calEvent);
+ self._adjustOverlappingEvents($weekDay);
+ $calEvent.data('preventClick', true);
+ setTimeout(function() {
+ $calEvent.removeData('preventClick');
+ }, 500);
+ }
+ });
+ $('.ui-resizable-handle', $calEvent).text('=');
+ },
+
+ /*
+ * Refresh the displayed details of a calEvent in the calendar
+ */
+ _refreshEventDetails: function(calEvent, $calEvent) {
+ var suffix = '';
+ if (!this.options.readonly &&
+ this.options.allowEventDelete &&
+ this.options.deletable(calEvent,$calEvent)) {
+ suffix = '<div class="wc-cal-event-delete ui-icon ui-icon-close"></div>';
+ }
+ $calEvent.find('.wc-time').html(this.options.eventHeader(calEvent, this.element) + suffix);
+ $calEvent.find('.wc-title').html(this.options.eventBody(calEvent, this.element));
+ $calEvent.data('calEvent', calEvent);
+ this.options.eventRefresh(calEvent, $calEvent);
+ },
+
+ /*
+ * Clear all cal events from the calendar
+ */
+ _clearCalendar: function() {
+ this.element.find('.wc-day-column-inner div').remove();
+ this._clearFreeBusys();
+ },
+
+ /*
+ * Scroll the calendar to a specific hour
+ */
+ _scrollToHour: function(hour, animate) {
+ var self = this;
+ var options = this.options;
+ var $scrollable = this.element.find('.wc-scrollable-grid');
+ var slot = hour;
+ if (self.options.businessHours.limitDisplay) {
+ if (hour <= self.options.businessHours.start) {
+ slot = 0;
+ } else if (hour >= self.options.businessHours.end) {
+ slot = self.options.businessHours.end - self.options.businessHours.start - 1;
+ } else {
+ slot = hour - self.options.businessHours.start;
+ }
+ }
+
+ //scroll to the hour plus some padding so that hour is in middle of viewport
+ var hourHeaderHeight = this.element.find(".wc-grid-timeslot-header .wc-hour-header").outerHeight();
+ var calHeight = this.element.find(".wc-scrollable-grid").outerHeight();
+ var scroll = (hourHeaderHeight * slot) - calHeight/3;
+ if (animate) {
+ $scrollable.animate({scrollTop: scroll}, options.scrollToHourMillis);
+ }
+ else {
+ $scrollable.animate({scrollTop: scroll}, 0);
+ }
+ },
+
+ /*
+ * find the hour (12 hour day) for a given hour index
+ */
+ _hourForIndex: function(index) {
+ if (index === 0) { //midnight
+ return 12;
+ } else if (index < 13) { //am
+ return index;
+ } else { //pm
+ return index - 12;
+ }
+ },
+
+ _24HourForIndex: function(index) {
+ if (index === 0) { //midnight
+ return '00:00';
+ } else if (index < 10) {
+ return '0' + index + ':00';
+ } else {
+ return index + ':00';
+ }
+ },
+
+ _amOrPm: function(hourOfDay) {
+ return hourOfDay < 12 ? 'AM' : 'PM';
+ },
+
+ _isToday: function(date) {
+ var clonedDate = this._cloneDate(date);
+ this._clearTime(clonedDate);
+ var today = MyDate();
+ this._clearTime(today);
+ return today.getTime() === clonedDate.getTime();
+ },
+
+ /*
+ * Clean events to ensure correct format
+ */
+ _cleanEvents: function(events) {
+ var self = this;
+ $.each(events, function(i, event) {
+ self._cleanEvent(event);
+ });
+ return events;
+ },
+
+ /*
+ * Clean specific event
+ */
+ _cleanEvent: function(event) {
+ event.start = this._cleanDate(event.start);
+ event.end = this._cleanDate(event.end);
+ if (!event.end) {
+ event.end = this._addDays(this._cloneDate(event.start), 1);
+ }
+ },
+
+ /*
+ * Disable text selection of the elements in different browsers
+ */
+ _disableTextSelect: function($elements) {
+ $elements.each(function() {
+ $(this).attr('unselectable', 'on')
+ .css({
+ '-moz-user-select': '-moz-none',
+ '-moz-user-select': 'none',
+ '-o-user-select': 'none',
+ '-khtml-user-select': 'none', /* you could also put this in a class */
+ '-webkit-user-select': 'none',/* and add the CSS class here instead */
+ '-ms-user-select': 'none',
+ 'user-select': 'none'
+ }).bind('selectstart', function () { return false; });
+
+ });
+ },
+
+ /*
+ * returns the date on the first millisecond of the week
+ */
+ _dateFirstDayOfWeek: function(date) {
+ var self = this;
+ var midnightCurrentDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
+ var adjustedDate = new Date(midnightCurrentDate);
+ adjustedDate.setDate(adjustedDate.getDate() - self._getAdjustedDayIndex(midnightCurrentDate));
+
+ return adjustedDate;
+ },
+
+ /*
+ * returns the date on the first millisecond of the last day of the week
+ */
+ _dateLastDayOfWeek: function(date) {
+ var self = this;
+ var midnightCurrentDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
+ var adjustedDate = new Date(midnightCurrentDate);
+ var daysToAdd = (self.options.daysToShow - 1 - self._getAdjustedDayIndex(midnightCurrentDate));
+ adjustedDate.setDate(adjustedDate.getDate() + daysToAdd);
+
+ return adjustedDate;
+ },
+
+ /**
+ * fix the date if it is not within given options
+ * minDate and maxDate
+ */
+ _fixMinMaxDate: function(date) {
+ var minDate, maxDate;
+ date = this._cleanDate(date);
+
+ // not less than minDate
+ if (this.options.minDate) {
+ minDate = this._cleanDate(this.options.minDate);
+ // midnight on minDate
+ minDate = new Date(minDate.getFullYear(), minDate.getMonth(), minDate.getDate());
+ if (date.getTime() < minDate.getTime()) {
+ this._trigger('reachedmindate', this.element, date);
+ }
+ date = this._cleanDate(Math.max(date.getTime(), minDate.getTime()));
+ }
+
+ // not more than maxDate
+ if (this.options.maxDate) {
+ maxDate = this._cleanDate(this.options.maxDate);
+ // apply correction for max date if not startOnFirstDayOfWeek
+ // to make sure no further date is displayed.
+ // otherwise, the complement will still be shown
+ if (!this._startOnFirstDayOfWeek()) {
+ var day = maxDate.getDate() - this.options.daysToShow + 1;
+ maxDate = new Date(maxDate.getFullYear(), maxDate.getMonth(), day);
+ }
+ // microsecond before midnight on maxDate
+ maxDate = new Date(maxDate.getFullYear(), maxDate.getMonth(), maxDate.getDate(), 23, 59, 59, 999);
+ if (date.getTime() > maxDate.getTime()) {
+ this._trigger('reachedmaxdate', this.element, date);
+ }
+ date = this._cleanDate(Math.min(date.getTime(), maxDate.getTime()));
+ }
+
+ return date;
+ },
+
+ /*
+ * gets the index of the current day adjusted based on options
+ */
+ _getAdjustedDayIndex: function(date) {
+ if (!this._startOnFirstDayOfWeek()) {
+ return 0;
+ }
+
+ var midnightCurrentDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
+ var currentDayOfStandardWeek = midnightCurrentDate.getDay();
+ var days = [0, 1, 2, 3, 4, 5, 6];
+ this._rotate(days, this._firstDayOfWeek());
+ return days[currentDayOfStandardWeek];
+ },
+
+ _firstDayOfWeek: function() {
+ if ($.isFunction(this.options.firstDayOfWeek)) {
+ return this.options.firstDayOfWeek(this.element);
+ }
+ return this.options.firstDayOfWeek;
+ },
+
+ /*
+ * returns the date on the last millisecond of the week
+ */
+ _dateLastMilliOfWeek: function(date) {
+ var lastDayOfWeek = this._dateLastDayOfWeek(date);
+ lastDayOfWeek = this._cloneDate(lastDayOfWeek);
+ lastDayOfWeek.setDate(lastDayOfWeek.getDate() + 1);
+ return lastDayOfWeek;
+
+ },
+
+ /*
+ * Clear the time components of a date leaving the date
+ * of the first milli of day
+ */
+ _clearTime: function(d) {
+ d.setHours(0);
+ d.setMinutes(0);
+ d.setSeconds(0);
+ d.setMilliseconds(0);
+ return d;
+ },
+
+ /*
+ * add specific number of days to date
+ */
+ _addDays: function(d, n, keepTime) {
+ d.setDate(d.getDate() + n);
+ if (keepTime) {
+ return d;
+ }
+ return this._clearTime(d);
+ },
+
+ /*
+ * Rotate an array by specified number of places.
+ */
+ _rotate: function(a /*array*/, p /* integer, positive integer rotate to the right, negative to the left... */) {
+ for (var l = a.length, p = (Math.abs(p) >= l && (p %= l), p < 0 && (p += l), p), i, x; p; p = (Math.ceil(l / p) - 1) * p - l + (l = p)) {
+ for (i = l; i > p; x = a[--i], a[i] = a[i - p], a[i - p] = x) {}
+ }
+ return a;
+ },
+
+ _cloneDate: function(d) {
+ return new Date(d.getTime());
+ },
+
+ /**
+ * Return a Date instance for different representations.
+ * Valid representations are:
+ * * timestamps
+ * * Date objects
+ * * textual representations (only these accepted by the Date
+ * constructor)
+ *
+ * @return {Date} The clean date object.
+ */
+ _cleanDate: function(d) {
+ if (typeof d === 'string') {
+ // if is numeric
+ if (!isNaN(Number(d))) {
+ return this._cleanDate(parseInt(d, 10));
+ }
+
+ // this is a human readable date
+ if (d[d.length - 1] !== 'Z') d += 'Z';
+ var o = new Date(d);
+ o.setTime(o.getTime() + (o.getTimezoneOffset() * 60 * 1000));
+ return o;
+ }
+
+ if (typeof d === 'number') {
+ return new Date(d);
+ }
+
+ return d;
+ },
+
+ /*
+ * date formatting is adapted from
+ * http://jacwright.com/projects/javascript/date_format
+ */
+ _formatDate: function(date, format) {
+ var returnStr = '';
+ for (var i = 0; i < format.length; i++) {
+ var curChar = format.charAt(i);
+ if (i !== 0 && format.charAt(i - 1) === '\\') {
+ returnStr += curChar;
+ }
+ else if (this._replaceChars[curChar]) {
+ returnStr += this._replaceChars[curChar](date, this);
+ } else if (curChar !== '\\') {
+ returnStr += curChar;
+ }
+ }
+ return returnStr;
+ },
+
+ _replaceChars: {
+ // Day
+ d: function(date) { return (date.getDate() < 10 ? '0' : '') + date.getDate(); },
+ D: function(date, calendar) { return calendar.options.shortDays[date.getDay()]; },
+ j: function(date) { return date.getDate(); },
+ l: function(date, calendar) { return calendar.options.longDays[date.getDay()]; },
+ N: function(date) { var _d = date.getDay(); return _d ? _d : 7; },
+ S: function(date) { return (date.getDate() % 10 == 1 && date.getDate() != 11 ? 'st' : (date.getDate() % 10 == 2 && date.getDate() != 12 ? 'nd' : (date.getDate() % 10 == 3 && date.getDate() != 13 ? 'rd' : 'th'))); },
+ w: function(date) { return date.getDay(); },
+ z: function(date) { var d = new Date(date.getFullYear(), 0, 1); return Math.ceil((date - d) / 86400000); }, // Fixed now
+ // Week
+ W: function(date) { var d = new Date(date.getFullYear(), 0, 1); return Math.ceil((((date - d) / 86400000) + d.getDay() + 1) / 7); }, // Fixed now
+ // Month
+ F: function(date, calendar) { return calendar.options.longMonths[date.getMonth()]; },
+ m: function(date) { return (date.getMonth() < 9 ? '0' : '') + (date.getMonth() + 1); },
+ M: function(date, calendar) { return calendar.options.shortMonths[date.getMonth()]; },
+ n: function(date) { return date.getMonth() + 1; },
+ t: function(date) { var d = date; return new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate() }, // Fixed now, gets #days of date
+ // Year
+ L: function(date) { var year = date.getFullYear(); return (year % 400 == 0 || (year % 100 != 0 && year % 4 == 0)); }, // Fixed now
+ o: function(date) { var d = new Date(date.valueOf()); d.setDate(d.getDate() - ((date.getDay() + 6) % 7) + 3); return d.getFullYear();}, //Fixed now
+ Y: function(date) { return date.getFullYear(); },
+ y: function(date) { return ('' + date.getFullYear()).substr(2); },
+ // Time
+ a: function(date) { return date.getHours() < 12 ? 'am' : 'pm'; },
+ A: function(date) { return date.getHours() < 12 ? 'AM' : 'PM'; },
+ B: function(date) { return Math.floor((((date.getUTCHours() + 1) % 24) + date.getUTCMinutes() / 60 + date.getUTCSeconds() / 3600) * 1000 / 24); }, // Fixed now
+ g: function(date) { return date.getHours() % 12 || 12; },
+ G: function(date) { return date.getHours(); },
+ h: function(date) { return ((date.getHours() % 12 || 12) < 10 ? '0' : '') + (date.getHours() % 12 || 12); },
+ H: function(date) { return (date.getHours() < 10 ? '0' : '') + date.getHours(); },
+ i: function(date) { return (date.getMinutes() < 10 ? '0' : '') + date.getMinutes(); },
+ s: function(date) { return (date.getSeconds() < 10 ? '0' : '') + date.getSeconds(); },
+ u: function(date) { var m = date.getMilliseconds(); return (m < 10 ? '00' : (m < 100 ? '0' : '')) + m; },
+ // Timezone
+ e: function(date) { return 'Not Yet Supported'; },
+ I: function(date) { return 'Not Yet Supported'; },
+ O: function(date) { return (-date.getTimezoneOffset() < 0 ? '-' : '+') + (Math.abs(date.getTimezoneOffset() / 60) < 10 ? '0' : '') + (Math.abs(date.getTimezoneOffset() / 60)) + '00'; },
+ P: function(date) { return (-date.getTimezoneOffset() < 0 ? '-' : '+') + (Math.abs(date.getTimezoneOffset() / 60) < 10 ? '0' : '') + (Math.abs(date.getTimezoneOffset() / 60)) + ':00'; }, // Fixed now
+ T: function(date) { var m = date.getMonth(); date.setMonth(0); var result = date.toTimeString().replace(/^.+ \(?([^\)]+)\)?$/, '$1'); date.setMonth(m); return result;},
+ Z: function(date) { return -date.getTimezoneOffset() * 60; },
+ // Full Date/Time
+ c: function(date, calendar) { return calendar._formatDate(date, 'Y-m-d\\TH:i:sP'); }, // Fixed now
+ r: function(date, calendar) { return calendar._formatDate(date, 'D, d M Y H:i:s O'); },
+ U: function(date) { return date.getTime() / 1000; }
+ },
+
+ /* USER MANAGEMENT FUNCTIONS */
+
+ getUserForId: function(id) {
+ return $.extend({}, this.options.users[this._getUserIndexFromId(id)]);
+ },
+
+ /**
+ * return the user name for header
+ */
+ _getUserName: function(index) {
+ var self = this;
+ var options = this.options;
+ var user = options.users[index];
+ if ($.isFunction(options.getUserName)) {
+ return options.getUserName(user, index, self.element);
+ }
+ else {
+ return user;
+ }
+ },
+ /**
+ * return the user id for given index
+ */
+ _getUserIdFromIndex: function(index) {
+ var self = this;
+ var options = this.options;
+ if ($.isFunction(options.getUserId)) {
+ return options.getUserId(options.users[index], index, self.element);
+ }
+ return index;
+ },
+ /**
+ * returns the associated user index for given ID
+ */
+ _getUserIndexFromId: function(id) {
+ var self = this;
+ var options = this.options;
+ for (var i = 0; i < options.users.length; i++) {
+ if (self._getUserIdFromIndex(i) == id) {
+ return i;
+ }
+ }
+ return 0;
+ },
+ /**
+ * return the user ids for given calEvent.
+ * default is calEvent.userId field.
+ */
+ _getEventUserId: function(calEvent) {
+ var self = this;
+ var options = this.options;
+ if (options.showAsSeparateUsers && options.users && options.users.length) {
+ if ($.isFunction(options.getEventUserId)) {
+ return options.getEventUserId(calEvent, self.element);
+ }
+ return calEvent.userId;
+ }
+ return [];
+ },
+ /**
+ * sets the event user id on given calEvent
+ * default is calEvent.userId field.
+ */
+ _setEventUserId: function(calEvent, userId) {
+ var self = this;
+ var options = this.options;
+ if ($.isFunction(options.setEventUserId)) {
+ return options.setEventUserId(userId, calEvent, self.element);
+ }
+ calEvent.userId = userId;
+ return calEvent;
+ },
+ /**
+ * return the user ids for given freeBusy.
+ * default is freeBusy.userId field.
+ */
+ _getFreeBusyUserId: function(freeBusy) {
+ var self = this;
+ var options = this.options;
+ if ($.isFunction(options.getFreeBusyUserId)) {
+ return options.getFreeBusyUserId(freeBusy.getOption(), self.element);
+ }
+ return freeBusy.getOption('userId');
+ },
+
+ /* FREEBUSY MANAGEMENT */
+
+ /**
+ * ckean the free busy managers and remove all the freeBusy
+ */
+ _clearFreeBusys: function() {
+ if (this.options.displayFreeBusys) {
+ var self = this,
+ options = this.options,
+ $freeBusyPlaceholders = self.element.find('.wc-grid-row-freebusy .wc-column-freebusy');
+ $freeBusyPlaceholders.each(function() {
+ $(this).data('wcFreeBusyManager', new FreeBusyManager({
+ start: self._cloneDate($(this).data('startDate')),
+ end: self._cloneDate($(this).data('endDate')),
+ defaultFreeBusy: options.defaultFreeBusy || {}
+ }));
+ });
+ self.element.find('.wc-grid-row-freebusy .wc-freebusy').remove();
+ }
+ },
+ /**
+ * retrieve placeholders for given freebusy
+ */
+ _findWeekDaysForFreeBusy: function(freeBusy, $weekDays) {
+ var $returnWeekDays,
+ options = this.options,
+ showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length,
+ self = this,
+ userList = self._getFreeBusyUserId(freeBusy);
+ if (!$.isArray(userList)) {
+ userList = userList != 'undefined' ? [userList] : [];
+ }
+ if (!$weekDays) {
+ $weekDays = self.element.find('.wc-grid-row-freebusy .wc-column-freebusy');
+ }
+ $weekDays.each(function() {
+ var manager = $(this).data('wcFreeBusyManager'),
+ has_overlap = manager.isWithin(freeBusy.getStart()) ||
+ manager.isWithin(freeBusy.getEnd()) ||
+ freeBusy.isWithin(manager.getStart()) ||
+ freeBusy.isWithin(manager.getEnd()),
+ userId = $(this).data('wcUserId');
+ if (has_overlap && (!showAsSeparatedUser || ($.inArray(userId, userList) != -1))) {
+ $returnWeekDays = $returnWeekDays ? $returnWeekDays.add($(this)) : $(this);
+ }
+ });
+ return $returnWeekDays;
+ },
+
+ /**
+ * used to render all freeBusys
+ */
+ _renderFreeBusys: function(freeBusys) {
+ if (this.options.displayFreeBusys) {
+ var self = this,
+ $freeBusyPlaceholders = self.element.find('.wc-grid-row-freebusy .wc-column-freebusy'),
+ freebusysToRender;
+ //insert freebusys to dedicated placeholders freebusy managers
+ if ($.isArray(freeBusys)) {
+ freebusysToRender = self._cleanFreeBusys(freeBusys);
+ } else if (freeBusys.freebusys) {
+ freebusysToRender = self._cleanFreeBusys(freeBusys.freebusys);
+ }
+ else {
+ freebusysToRender = [];
+ }
+
+ $.each(freebusysToRender, function(index, freebusy) {
+ var $placeholders = self._findWeekDaysForFreeBusy(freebusy, $freeBusyPlaceholders);
+ if ($placeholders) {
+ $placeholders.each(function() {
+ var manager = $(this).data('wcFreeBusyManager');
+ manager.insertFreeBusy(new FreeBusy(freebusy.getOption()));
+ $(this).data('wcFreeBusyManager', manager);
+ });
+ }
+ });
+
+ //now display freebusys on place holders
+ self._refreshFreeBusys($freeBusyPlaceholders);
+ }
+ },
+ /**
+ * refresh freebusys for given placeholders
+ */
+ _refreshFreeBusys: function($freeBusyPlaceholders) {
+ if (this.options.displayFreeBusys && $freeBusyPlaceholders) {
+ var self = this,
+ options = this.options,
+ start = (options.businessHours.limitDisplay ? options.businessHours.start : 0),
+ end = (options.businessHours.limitDisplay ? options.businessHours.end : 24);
+
+ $freeBusyPlaceholders.each(function() {
+ var $placehoder = $(this);
+ var s = self._cloneDate($placehoder.data('startDate')),
+ e = self._cloneDate(s);
+ s.setHours(start);
+ e.setHours(end);
+ $placehoder.find('.wc-freebusy').remove();
+ $.each($placehoder.data('wcFreeBusyManager').getFreeBusys(s, e), function() {
+ self._renderFreeBusy(this, $placehoder);
+ });
+ });
+ }
+ },
+ /**
+ * render a freebusy item on dedicated placeholders
+ */
+ _renderFreeBusy: function(freeBusy, $freeBusyPlaceholder) {
+ if (this.options.displayFreeBusys) {
+ var self = this,
+ options = this.options,
+ freeBusyHtml = '<div class="wc-freebusy"></div>';
+
+ var $fb = $(freeBusyHtml);
+ $fb.data('wcFreeBusy', new FreeBusy(freeBusy.getOption()));
+ this._positionFreeBusy($freeBusyPlaceholder, $fb);
+ $fb = options.freeBusyRender(freeBusy.getOption(), $fb, self.element);
+ if ($fb) {
+ $fb.appendTo($freeBusyPlaceholder);
+ }
+ }
+ },
+ /*
+ * Position the freebusy element within the weekday based on it's start / end dates.
+ */
+ _positionFreeBusy: function($placeholder, $freeBusy) {
+ var options = this.options;
+ var freeBusy = $freeBusy.data('wcFreeBusy');
+ var pxPerMillis = $placeholder.height() / options.millisToDisplay;
+ var firstHourDisplayed = options.businessHours.limitDisplay ? options.businessHours.start : 0;
+ var startMillis = freeBusy.getStart().getTime() - new Date(freeBusy.getStart().getFullYear(), freeBusy.getStart().getMonth(), freeBusy.getStart().getDate(), firstHourDisplayed).getTime();
+ var eventMillis = freeBusy.getEnd().getTime() - freeBusy.getStart().getTime();
+ var pxTop = pxPerMillis * startMillis;
+ var pxHeight = pxPerMillis * eventMillis;
+ $freeBusy.css({top: pxTop, height: pxHeight});
+ },
+ /*
+ * Clean freebusys to ensure correct format
+ */
+ _cleanFreeBusys: function(freebusys) {
+ var self = this,
+ freeBusyToReturn = [];
+ if (!$.isArray(freebusys)) {
+ var freebusys = [freebusys];
+ }
+ $.each(freebusys, function(i, freebusy) {
+ if (!freebusy) return;
+ freeBusyToReturn.push(new FreeBusy(self._cleanFreeBusy(freebusy)));
+ });
+ return freeBusyToReturn;
+ },
+
+ /*
+ * Clean specific freebusy
+ */
+ _cleanFreeBusy: function(freebusy) {
+ if (freebusy.date) {
+ freebusy.start = freebusy.date;
+ }
+ freebusy.start = this._cleanDate(freebusy.start);
+ freebusy.end = this._cleanDate(freebusy.end);
+ return freebusy;
+ },
+
+ /**
+ * retrives the first freebusy manager matching demand.
+ */
+ getFreeBusyManagersFor: function(date, users) {
+ var calEvent = {
+ start: date,
+ end: date
+ };
+ this._setEventUserId(calEvent, users);
+ return this.getFreeBusyManagerForEvent(calEvent);
+ },
+ /**
+ * retrives the first freebusy manager for given event.
+ */
+ getFreeBusyManagerForEvent: function(newCalEvent) {
+ var self = this,
+ options = this.options,
+ freeBusyManager;
+ if (options.displayFreeBusys) {
+ var $freeBusyPlaceHoders = self.element.find('.wc-grid-row-freebusy .wc-column-freebusy'),
+ freeBusy = new FreeBusy({start: newCalEvent.start, end: newCalEvent.end}),
+ showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length,
+ userId = showAsSeparatedUser ? self._getEventUserId(newCalEvent) : null;
+ if (!$.isArray(userId)) {
+ userId = [userId];
+ }
+ $freeBusyPlaceHoders.each(function() {
+ var manager = $(this).data('wcFreeBusyManager'),
+ has_overlap = manager.isWithin(freeBusy.getEnd()) ||
+ manager.isWithin(freeBusy.getEnd()) ||
+ freeBusy.isWithin(manager.getStart()) ||
+ freeBusy.isWithin(manager.getEnd());
+ if (has_overlap && (!showAsSeparatedUser || $.inArray($(this).data('wcUserId'), userId) != -1)) {
+ freeBusyManager = $(this).data('wcFreeBusyManager');
+ return false;
+ }
+ });
+ }
+ return freeBusyManager;
+ },
+ /**
+ * appends the freebusys to replace the old ones.
+ * @param {array|object} freeBusys freebusy(s) to apply.
+ */
+ updateFreeBusy: function(freeBusys) {
+ var self = this,
+ options = this.options;
+ if (options.displayFreeBusys) {
+ var $toRender,
+ $freeBusyPlaceHoders = self.element.find('.wc-grid-row-freebusy .wc-column-freebusy'),
+ _freeBusys = self._cleanFreeBusys(freeBusys);
+
+ $.each(_freeBusys, function(index, _freeBusy) {
+
+ var $weekdays = self._findWeekDaysForFreeBusy(_freeBusy, $freeBusyPlaceHoders);
+ //if freebusy has a placeholder
+ if ($weekdays && $weekdays.length) {
+ $weekdays.each(function(index, day) {
+ var manager = $(day).data('wcFreeBusyManager');
+ manager.insertFreeBusy(_freeBusy);
+ $(day).data('wcFreeBusyManager', manager);
+ });
+ $toRender = $toRender ? $toRender.add($weekdays) : $weekdays;
+ }
+ });
+ self._refreshFreeBusys($toRender);
+ }
+ },
+
+ /* NEW OPTIONS MANAGEMENT */
+
+ /**
+ * checks wether or not the calendar should be displayed starting on first day of week
+ */
+ _startOnFirstDayOfWeek: function() {
+ return jQuery.isFunction(this.options.startOnFirstDayOfWeek) ? this.options.startOnFirstDayOfWeek(this.element) : this.options.startOnFirstDayOfWeek;
+ },
+
+ /**
+ * finds out the current scroll to apply it when changing the view
+ */
+ _getCurrentScrollHour: function() {
+ var self = this;
+ var options = this.options;
+ var $scrollable = this.element.find('.wc-scrollable-grid');
+ var scroll = $scrollable.scrollTop();
+ if (self.options.businessHours.limitDisplay) {
+ scroll = scroll + options.businessHours.start * options.timeslotHeight * options.timeslotsPerHour;
+ }
+ return Math.round(scroll / (options.timeslotHeight * options.timeslotsPerHour)) + 1;
+ },
+ _getJsonOptions: function() {
+ if ($.isFunction(this.options.jsonOptions)) {
+ return $.extend({}, this.options.jsonOptions(this.element));
+ }
+ if ($.isPlainObject(this.options.jsonOptions)) {
+ return $.extend({}, this.options.jsonOptions);
+ }
+ return {};
+ },
+ _getHeaderDate: function(date) {
+ var options = this.options;
+ if (options.getHeaderDate && $.isFunction(options.getHeaderDate))
+ {
+ return options.getHeaderDate(date, this.element);
+ }
+ var dayName = options.useShortDayNames ? options.shortDays[date.getDay()] : options.longDays[date.getDay()];
+ return dayName + (options.headerSeparator) + this._formatDate(date, options.dateFormat);
+ },
+
+
+
+ /**
+ * returns corrected date related to DST problem
+ */
+ _getDSTdayShift: function(date, shift) {
+ var start = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0);
+ var offset1 = start.getTimezoneOffset();
+ var offset2 = date.getTimezoneOffset();
+ if (offset1 == offset2)
+ return date;
+ shift = shift ? shift : 1;
+ return new Date(date.getTime() - shift * (offset1 > offset2 ? -1 : 1) * (Math.max(offset1, offset2) - Math.min(offset1, offset2)) * 60000);
+ },
+ _needDSTdayShift: function(date1, date2) {
+ return date1.getTimezoneOffset() != date2.getTimezoneOffset();
+ }
+
+
+
+ }; // end of widget function return
+ })() //end of widget function closure execution
+ ); // end of $.widget("ui.weekCalendar"...
+
+ $.extend($.ui.weekCalendar, {
+ version: '2.0-dev',
+ updateLayoutOptions: {
+ startOnFirstDayOfWeek: true,
+ firstDayOfWeek: true,
+ daysToShow: true,
+ displayOddEven: true,
+ timeFormat: true,
+ dateFormat: true,
+ use24Hour: true,
+ useShortDayNames: true,
+ businessHours: true,
+ timeslotHeight: true,
+ timeslotsPerHour: true,
+ buttonText: true,
+ height: true,
+ shortMonths: true,
+ longMonths: true,
+ shortDays: true,
+ longDays: true,
+ textSize: true,
+ users: true,
+ showAsSeparateUsers: true,
+ displayFreeBusys: true
+ }
+ });
+
+ var MILLIS_IN_DAY = 86400000;
+ var MILLIS_IN_WEEK = MILLIS_IN_DAY * 7;
+
+ /* FREE BUSY MANAGERS */
+ var FreeBusyProto = {
+ getStart: function() {return this.getOption('start')},
+ getEnd: function() {return this.getOption('end')},
+ getOption: function() {
+ if (!arguments.length) { return this.options }
+ if (typeof(this.options[arguments[0]]) !== 'undefined') {
+ return this.options[arguments[0]];
+ }
+ else if (typeof(arguments[1]) !== 'undefined') {
+ return arguments[1];
+ }
+ return null;
+ },
+ setOption: function(key, value) {
+ if (arguments.length == 1) {
+ $.extend(this.options, arguments[0]);
+ return this;
+ }
+ this.options[key] = value;
+ return this;
+ },
+ isWithin: function(dateTime) {return Math.floor(dateTime.getTime() / 1000) >= Math.floor(this.getStart().getTime() / 1000) && Math.floor(dateTime.getTime() / 1000) < Math.floor(this.getEnd().getTime() / 1000)},
+ isValid: function() {return this.getStart().getTime() < this.getEnd().getTime()}
+ };
+
+ /**
+ * @constructor
+ * single user freebusy manager.
+ */
+ var FreeBusy = function(options) {
+ this.options = $.extend({}, options || {});
+ };
+ $.extend(FreeBusy.prototype, FreeBusyProto);
+
+ var FreeBusyManager = function(options) {
+ this.options = $.extend({
+ defaultFreeBusy: {}
+ }, options || {});
+ this.freeBusys = [];
+ this.freeBusys.push(new FreeBusy($.extend({
+ start: this.getStart(),
+ end: this.getEnd()
+ }, this.options.defaultFreeBusy)));
+ };
+ $.extend(FreeBusyManager.prototype, FreeBusyProto, {
+ /**
+ * return matching freeBusys.
+ * if you do not pass any argument, returns all freebusys.
+ * if you only pass a start date, only matchinf freebusy will be returned.
+ * if you pass 2 arguments, then all freebusys available within the time period will be returned
+ * @param {Date} start [optionnal] if you do not pass end date, will return the freeBusy within which this date falls.
+ * @param {Date} end [optionnal] the date where to stop the search.
+ * @return {Array} an array of FreeBusy matching arguments.
+ */
+ getFreeBusys: function() {
+ switch (arguments.length) {
+ case 0:
+ return this.freeBusys;
+ case 1:
+ var freeBusy = [];
+ var start = arguments[0];
+ if (!this.isWithin(start)) {
+ return freeBusy;
+ }
+ $.each(this.freeBusys, function() {
+ if (this.isWithin(start)) {
+ freeBusy.push(this);
+ }
+ if (Math.floor(this.getEnd().getTime() / 1000) > Math.floor(start.getTime() / 1000)) {
+ return false;
+ }
+ });
+ return freeBusy;
+ default:
+ //we assume only 2 first args are revealants
+ var freeBusy = [];
+ var start = arguments[0], end = arguments[1];
+ var tmpFreeBusy = new FreeBusy({start: start, end: end});
+ if (end.getTime() < start.getTime() || this.getStart().getTime() > end.getTime() || this.getEnd().getTime() < start.getTime()) {
+ return freeBusy;
+ }
+ $.each(this.freeBusys, function() {
+ if (this.getStart().getTime() >= end.getTime()) {
+ return false;
+ }
+ if (tmpFreeBusy.isWithin(this.getStart()) && tmpFreeBusy.isWithin(this.getEnd())) {
+ freeBusy.push(this);
+ }
+ else if (this.isWithin(tmpFreeBusy.getStart()) && this.isWithin(tmpFreeBusy.getEnd())) {
+ var _f = new FreeBusy(this.getOption());
+ _f.setOption('end', tmpFreeBusy.getEnd());
+ _f.setOption('start', tmpFreeBusy.getStart());
+ freeBusy.push(_f);
+ }
+ else if (this.isWithin(tmpFreeBusy.getStart()) && this.getStart().getTime() < start.getTime()) {
+ var _f = new FreeBusy(this.getOption());
+ _f.setOption('start', tmpFreeBusy.getStart());
+ freeBusy.push(_f);
+ }
+ else if (this.isWithin(tmpFreeBusy.getEnd()) && this.getEnd().getTime() > end.getTime()) {
+ var _f = new FreeBusy(this.getOption());
+ _f.setOption('end', tmpFreeBusy.getEnd());
+ freeBusy.push(_f);
+ }
+ });
+ return freeBusy;
+ }
+ },
+ insertFreeBusy: function(freeBusy) {
+ var freeBusy = new FreeBusy(freeBusy.getOption());
+ //first, if inserted freebusy is bigger than manager
+ if (freeBusy.getStart().getTime() < this.getStart().getTime()) {
+ freeBusy.setOption('start', this.getStart());
+ }
+ if (freeBusy.getEnd().getTime() > this.getEnd().getTime()) {
+ freeBusy.setOption('end', this.getEnd());
+ }
+ var start = freeBusy.getStart(), end = freeBusy.getEnd(),
+ startIndex = 0, endIndex = this.freeBusys.length - 1,
+ newFreeBusys = [];
+ var pushNewFreeBusy = function(_f) {if (_f.isValid()) newFreeBusys.push(_f);};
+
+ $.each(this.freeBusys, function(index) {
+ //within the loop, we have following vars:
+ // curFreeBusyItem: the current iteration freeBusy, part of manager freeBusys list
+ // start: the insterted freeBusy start
+ // end: the inserted freebusy end
+ var curFreeBusyItem = this;
+ if (curFreeBusyItem.isWithin(start) && curFreeBusyItem.isWithin(end)) {
+ /*
+ we are in case where inserted freebusy fits in curFreeBusyItem:
+ curFreeBusyItem: *-----------------------------*
+ freeBusy: *-------------*
+ obviously, start and end indexes are this item.
+ */
+ startIndex = index;
+ endIndex = index;
+ if (start.getTime() == curFreeBusyItem.getStart().getTime() && end.getTime() == curFreeBusyItem.getEnd().getTime()) {
+ /*
+ in this case, inserted freebusy is exactly curFreeBusyItem:
+ curFreeBusyItem: *-----------------------------*
+ freeBusy: *-----------------------------*
+
+ just replace curFreeBusyItem with freeBusy.
+ */
+ var _f1 = new FreeBusy(freeBusy.getOption());
+ pushNewFreeBusy(_f1);
+ }
+ else if (start.getTime() == curFreeBusyItem.getStart().getTime()) {
+ /*
+ in this case inserted freebusy starts with curFreeBusyItem:
+ curFreeBusyItem: *-----------------------------*
+ freeBusy: *--------------*
+
+ just replace curFreeBusyItem with freeBusy AND the rest.
+ */
+ var _f1 = new FreeBusy(freeBusy.getOption());
+ var _f2 = new FreeBusy(curFreeBusyItem.getOption());
+ _f2.setOption('start', end);
+ pushNewFreeBusy(_f1);
+ pushNewFreeBusy(_f2);
+ }
+ else if (end.getTime() == curFreeBusyItem.getEnd().getTime()) {
+ /*
+ in this case inserted freebusy ends with curFreeBusyItem:
+ curFreeBusyItem: *-----------------------------*
+ freeBusy: *--------------*
+
+ just replace curFreeBusyItem with before part AND freeBusy.
+ */
+ var _f1 = new FreeBusy(curFreeBusyItem.getOption());
+ _f1.setOption('end', start);
+ var _f2 = new FreeBusy(freeBusy.getOption());
+ pushNewFreeBusy(_f1);
+ pushNewFreeBusy(_f2);
+ }
+ else {
+ /*
+ in this case inserted freebusy is within curFreeBusyItem:
+ curFreeBusyItem: *-----------------------------*
+ freeBusy: *--------------*
+
+ just replace curFreeBusyItem with before part AND freeBusy AND the rest.
+ */
+ var _f1 = new FreeBusy(curFreeBusyItem.getOption());
+ var _f2 = new FreeBusy(freeBusy.getOption());
+ var _f3 = new FreeBusy(curFreeBusyItem.getOption());
+ _f1.setOption('end', start);
+ _f3.setOption('start', end);
+ pushNewFreeBusy(_f1);
+ pushNewFreeBusy(_f2);
+ pushNewFreeBusy(_f3);
+ }
+ /*
+ as work is done, no need to go further.
+ return false
+ */
+ return false;
+ }
+ else if (curFreeBusyItem.isWithin(start) && curFreeBusyItem.getEnd().getTime() != start.getTime()) {
+ /*
+ in this case, inserted freebusy starts within curFreeBusyItem:
+ curFreeBusyItem: *----------*
+ freeBusy: *-------------------*
+
+ set start index AND insert before part, we'll insert freebusy later
+ */
+ if (curFreeBusyItem.getStart().getTime() != start.getTime()) {
+ var _f1 = new FreeBusy(curFreeBusyItem.getOption());
+ _f1.setOption('end', start);
+ pushNewFreeBusy(_f1);
+ }
+ startIndex = index;
+ }
+ else if (curFreeBusyItem.isWithin(end) && curFreeBusyItem.getStart().getTime() != end.getTime()) {
+ /*
+ in this case, inserted freebusy starts within curFreeBusyItem:
+ curFreeBusyItem: *----------*
+ freeBusy: *-------------------*
+
+ set end index AND insert freebusy AND insert after part if needed
+ */
+ pushNewFreeBusy(new FreeBusy(freeBusy.getOption()));
+ if (end.getTime() < curFreeBusyItem.getEnd().getTime()) {
+ var _f1 = new FreeBusy(curFreeBusyItem.getOption());
+ _f1.setOption('start', end);
+ pushNewFreeBusy(_f1);
+ }
+ endIndex = index;
+ return false;
+ }
+ });
+ //now compute arguments
+ var tmpFB = this.freeBusys;
+ this.freeBusys = [];
+
+ if (startIndex) {
+ this.freeBusys = this.freeBusys.concat(tmpFB.slice(0, startIndex));
+ }
+ this.freeBusys = this.freeBusys.concat(newFreeBusys);
+ if (endIndex < tmpFB.length) {
+ this.freeBusys = this.freeBusys.concat(tmpFB.slice(endIndex + 1));
+ }
+/* if(start.getDate() == 1){
+ console.info('insert from '+freeBusy.getStart() +' to '+freeBusy.getEnd());
+ console.log('index from '+ startIndex + ' to ' + endIndex);
+ var str = [];
+ $.each(tmpFB, function(i){str.push(i + ": " + this.getStart().getHours() + ' > ' + this.getEnd().getHours() + ' ' + (this.getOption('free') ? 'free' : 'busy'))});
+ console.log(str.join('\n'));
+
+ console.log('insert');
+ var str = [];
+ $.each(newFreeBusys, function(i){str.push(this.getStart().getHours() + ' > ' + this.getEnd().getHours())});
+ console.log(str.join(', '));
+
+ console.log('results');
+ var str = [];
+ $.each(this.freeBusys, function(i){str.push(i + ": " + this.getStart().getHours() + ' > ' + this.getEnd().getHours() + ' ' + (this.getOption('free') ? 'free' :'busy'))});
+ console.log(str.join('\n'));
+ }*/
+ return this;
+ }
+ });
+})(jQuery);
+
diff --git a/modules-available/js_weekcalendar/style.css b/modules-available/js_weekcalendar/style.css
new file mode 100755
index 00000000..aae8f956
--- /dev/null
+++ b/modules-available/js_weekcalendar/style.css
@@ -0,0 +1,284 @@
+.wc-container {
+ font-size: 14px;
+ font-family: arial, helvetica;
+}
+
+.wc-toolbar {
+ padding: 1em;
+ font-size:0.8em;
+}
+
+.wc-toolbar .wc-nav {
+ float:left;
+}
+
+.wc-toolbar .wc-display {
+ float: right;
+}
+
+.wc-toolbar button {
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+.wc-toolbar .wc-title {
+ text-align: center;
+ padding:0;
+ margin:0;
+}
+
+.wc-container table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+.wc-container table td {
+ margin: 0;
+ padding: 0;
+}
+
+.wc-header {
+ background: #eee;
+ border-width:1px 0;
+ border-style:solid;
+}
+.wc-header table{
+ width: 100%;
+ table-layout:fixed;
+}
+
+.wc-grid-timeslot-header,
+.wc-header .wc-time-column-header {
+ width: 45px;
+}
+
+.wc-header .wc-scrollbar-shim {
+ width: 16px;
+}
+
+.wc-header .wc-day-column-header {
+ text-align: center;
+ padding: 0.4em;
+}
+
+.wc-header .wc-user-header{
+ text-align: center;
+ padding: 0.4em 0;
+ overflow:hidden;
+}
+.wc-grid-timeslot-header {
+ background: #eee;
+}
+
+.wc-scrollable-grid {
+ overflow: auto;
+ overflow-x: hidden !important;
+ overflow-y: auto !important;
+ position: relative;
+ background-color: #fff;
+ width: 100%;
+}
+
+
+table.wc-time-slots {
+ width: 100%;
+ table-layout: fixed;
+ cursor: default;
+ overflow:hidden;
+}
+
+.wc-day-column {
+ width: 13.5%;
+ overflow: visible;
+ vertical-align: top;
+}
+.wc-day-column-header{border-width: 0 0 1px 3px; border-style: solid;border-color:transparent;}
+.wc-scrollable-grid .wc-day-column-last,
+.wc-scrollable-grid .wc-day-column-middle{border-width: 0 0 0 1px; border-style: dashed;}
+.wc-scrollable-grid .wc-day-column-first{border-width: 0 0 0 3px; border-style: double;}
+
+.wc-day-column-inner {
+ width: 100%;
+ position:relative;
+}
+
+.wc-no-height-wrapper{
+ position:relative;
+ overflow: visible;
+ height: 0px;
+}
+
+.wc-time-slot-wrapper {
+/* top: 3px;*/
+}
+.wc-oddeven-wrapper .wc-full-height-column{
+/* top: 2px; */
+ /* Modern Browsers */ opacity: 0.4;
+ /* IE 8 */ -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=40)";
+ /* IE 5-7 */ filter: alpha(opacity=40);
+ /* Netscape */ -moz-opacity: 0.4;
+ /* Safari 1 */ -khtml-opacity: 0.4;
+}
+.wc-freebusy-wrapper .wc-freebusy{
+/* top: 1px;*/
+ /* Modern Browsers */ opacity: 0.4;
+ /* IE 8 */ -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=40)";
+ /* IE 5-7 */ filter: alpha(opacity=40);
+ /* Netscape */ -moz-opacity: 0.4;
+ /* Safari 1 */ -khtml-opacity: 0.4;
+}
+
+.wc-time-slots {
+ position: absolute;
+ width: 100%;
+}
+
+.wc-column-odd,
+.wc-column-even.ui-state-hover{background-image:none;border:none;}
+
+.wc-header .wc-today.ui-state-active{background-image:none;}
+.wc-header .wc-today.wc-day-column-header{border-width:0 3px; border-style: solid;}
+.wc-header .wc-user-header{border-width:0;}
+
+.wc-time-slots .wc-day-column.ui-state-default{background:transparent;}
+.wc-time-slots .wc-today.ui-state-active{background-image:none;}
+.wc-header .wc-today.ui-state-active.wc-day-column-middle{border-width:0;}
+.wc-header .wc-today.ui-state-active.wc-day-column-first{border-left-width:3px;}
+.wc-header .wc-today.ui-state-active.wc-day-column-last{border-right-width:3px;}
+
+.wc-full-height-column{
+ display:block;
+/* width:100%;*/
+}
+
+
+.wc-time-header-cell {
+ padding: 5px;
+ height: 80px; /* reference height */
+}
+
+
+.wc-time-slot {
+ border-bottom: 1px dotted #ddd;
+}
+
+.wc-hour-header {
+ text-align: right;
+}
+.wc-hour-header.ui-state-active,
+.wc-hour-header.ui-state-default{
+ border-width:0 0 1px 0;
+}
+
+.wc-hour-end, .wc-hour-header {
+ border-bottom: 1px solid #ccc;
+ color: #555;
+}
+
+.wc-business-hours {
+ background-color: #E6EEF1;
+ border-bottom: 1px solid #ccc;
+ color: #333;
+ font-size: 1em;
+ font-weight: bold;
+}
+
+.wc-business-hours .wc-am-pm {
+ font-size: 0.6em;
+}
+
+.wc-day-header-cell {
+ text-align: center;
+ vertical-align: middle;
+ padding: 5px;
+}
+
+
+
+.wc-time-slot-header .wc-header-cell {
+ text-align: right;
+ padding-right: 10px;
+}
+
+.wc-cal-event {
+ background-color: #68a1e5;
+ /* Modern Browsers */ opacity: 0.8;
+ /* IE 8 */ -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=80)";
+ /* IE 5-7 */ filter: alpha(opacity=80);
+ /* Netscape */ -moz-opacity: 0.8;
+ /* Safari 1 */ -khtml-opacity: 0.8;
+ position: absolute;
+ text-align: center;
+ overflow: hidden;
+ cursor: pointer;
+ color: #fff;
+ width: 100%;
+ display: none;
+}
+
+
+.wc-cal-event-delete {
+ float: right;
+ cursor: pointer;
+ width: 16px;
+ height: 16px;
+}
+
+.wc-cal-event.ui-resizable-resizing {
+ cursor: s-resize;
+}
+
+.wc-cal-event .wc-time {
+ background-color: #2b72d0;
+ border: 1px solid #1b62c0;
+ color: #fff;
+ padding: 0;
+ font-weight: bold;
+}
+
+.wc-container .ui-draggable .wc-time {
+ cursor: move;
+}
+
+.wc-cal-event .wc-title {
+ position: relative;
+}
+
+.wc-container .ui-resizable-s {
+ height: 10px;
+ line-height: 10px;
+ bottom: -2px;
+ font-size: .75em;
+}
+
+
+.wc-container .ui-draggable-dragging {
+ z-index: 1000;
+}
+
+.free-busy-free{}
+.free-busy-busy{
+ background:url("./libs/css/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png") repeat scroll 50% 50% #666666;
+}
+
+/** hourLine */
+
+.wc-hourline {
+ height: 0pt;
+ border-top: 2px solid #FF7F6E;
+ overflow: hidden;
+ position: absolute;
+ width: inherit;
+}
+
+/* IE6 hacks */
+* html .wc-no-height-wrapper{position:absolute;}
+* html .wc-time-slot-wrapper{top:3px;}
+* html .wc-grid-row-oddeven{top:2px;}
+* html .wc-grid-row-freebusy{top:1px;}
+
+/* IE7 hacks */
+*:first-child+html .wc-no-height-wrapper{position:relative;}
+*:first-child+html .wc-time-slot-wrapper{top:3px;}
+*:first-child+html .wc-grid-row-oddeven{top:2px;}
+*:first-child+html .wc-grid-row-freebusy{top:1px;}
+*:first-child+html .wc-time-slots .wc-today{/* due to rendering issues, no background */background:none;}
diff --git a/modules-available/locationinfo/api.inc.php b/modules-available/locationinfo/api.inc.php
index 0d84ebce..e14fe9d7 100644
--- a/modules-available/locationinfo/api.inc.php
+++ b/modules-available/locationinfo/api.inc.php
@@ -1,7 +1,249 @@
<?php
-echo json_encode(array(
- 'key' => 'value',
- 'number' => 123,
- 'list' => array(1,2,3,4,5,6,'foo')
-));
+if (Request::get('redirect', false, 'int') !== false) {
+ // Redirect to actual panel from uuid
+ $uuid = Request::get('uuid', false, 'string');
+ if ($uuid === false) {
+ http_response_code(400);
+ die('Missing uuid parameter');
+ }
+ $row = Database::queryFirst('SELECT paneltype, panelconfig FROM locationinfo_panel WHERE paneluuid = :uuid', compact('uuid'));
+ if ($row === false) {
+ http_response_code(404);
+ die('Panel not found');
+ }
+ if ($row['paneltype'] === 'DEFAULT') {
+ Util::redirect(dirname($_SERVER['SCRIPT_NAME']) . '/modules/locationinfo/frontend/doorsign.html?uuid=' . $uuid);
+ } elseif ($row['paneltype'] === 'SUMMARY') {
+ Util::redirect(dirname($_SERVER['SCRIPT_NAME']) . '/modules/locationinfo/frontend/overview.html?uuid=' . $uuid);
+ } elseif ($row['paneltype'] === 'URL') {
+ $data = json_decode($row['panelconfig'], true);
+ if (!$data || !isset($data['url'])) {
+ http_response_code('500');
+ die('Panel config corrupted on server');
+ }
+ Util::redirect($data['url']);
+ }
+ http_response_code('500');
+ die('Panel has invalid type "' . $row['paneltype'] . '"');
+}
+
+/*
+ * vvv - API to Panel - vvv
+ */
+
+HandleParameters();
+
+/**
+ * Handles the API paramenters.
+ */
+function HandleParameters()
+{
+
+ $get = Request::get('get', 0, 'string');
+ $uuid = Request::get('uuid', false, 'string');
+ $output = false;
+ if ($get === "timestamp") {
+ $output = array('ts' => getLastChangeTs($uuid));
+ } elseif ($get === "machines") {
+ $locationIds = getLocationsOr404($uuid);
+ $output = array();
+ InfoPanel::appendMachineData($output, $locationIds, false);
+ $output = array_values($output);
+ } elseif ($get === "config") {
+ $type = InfoPanel::getConfig($uuid, $output);
+ if ($type === false) {
+ http_response_code(404);
+ die('Panel not found');
+ }
+ } elseif ($get === "pcstates") {
+ $locationIds = getLocationsOr404($uuid);
+ $output = getPcStates($locationIds);
+ } elseif ($get === "locationtree") {
+ $locationIds = getLocationsOr404($uuid);
+ $output = getLocationTree($locationIds);
+ } elseif ($get === "calendar") {
+ $locationIds = getLocationsOr404($uuid);
+ $output = getCalendar($locationIds);
+ }
+ if ($output !== false) {
+ Header('Content-Type: application/json; charset=utf-8');
+ echo json_encode($output);
+ } else {
+ http_response_code(404);
+ echo 'Unknown get option';
+ }
+}
+
+/**
+ * Return list of locationids associated with given panel.
+ * @param string $paneluuid panel
+ * @return int[] locationIds
+ */
+function getLocationsOr404($paneluuid)
+{
+ $panel = Database::queryFirst('SELECT locationids FROM locationinfo_panel WHERE paneluuid = :paneluuid',
+ compact('paneluuid'));
+ if ($panel !== false) {
+ return array_map('intval', explode(',', $panel['locationids']));
+ }
+ http_response_code(404);
+ die('Panel not found');
+}
+
+/**
+ * Get last config modification timestamp for given panel.
+ * This was planned to be smart and check the involved locations,
+ * even going up the location tree if the opening time schedule
+ * is inherited, but this would still be incomplete by design, as
+ * it wouldn't react to the linked room plan being considered
+ * for changes, or added/removed PCs etc. So rather than giving
+ * an incomplete "clever" design for detecting changes, we only
+ * consider direct editing of the panel now. So the advice would
+ * simply be "if you want the panel to reload automatically, hit
+ * the edit button and click save". Might even add a shortcut
+ * reload-button to the list of panels at some point.
+ *
+ * @param string $paneluuid panels uuid
+ * @return int UNIX_TIMESTAMP
+ */
+function getLastChangeTs($paneluuid)
+{
+ $panel = Database::queryFirst('SELECT lastchange FROM locationinfo_panel WHERE paneluuid = :paneluuid',
+ compact('paneluuid'));
+ if ($panel === false) {
+ http_response_code(404);
+ die('Panel not found');
+ }
+ return (int)$panel['lastchange'];
+}
+
+/**
+ * Gets the pc states of the given locations.
+ *
+ * @param int[] $idList list of the location ids.
+ * @return array aggregated PC states
+ */
+function getPcStates($idList)
+{
+ $pcStates = array();
+ foreach ($idList as $id) {
+ $pcStates[$id] = array(
+ 'id' => $id,
+ 'idle' => 0,
+ 'occupied' => 0,
+ 'off' => 0,
+ 'broken' => 0,
+ );
+ }
+
+ $locationInfoList = array();
+ InfoPanel::appendMachineData($locationInfoList, $idList);
+ foreach ($locationInfoList as $locationInfo) {
+ $id = $locationInfo['id'];
+ foreach ($locationInfo['machines'] as $pc) {
+ $key = strtolower($pc['pcState']);
+ if (isset($pcStates[$id][$key])) {
+ $pcStates[$id][$key]++;
+ }
+ }
+ }
+
+ return array_values($pcStates);
+}
+
+/**
+ * Gets the location tree of the given locations.
+ *
+ * @param int[] $idList Array list of the locations.
+ * @return array location tree data
+ */
+function getLocationTree($idList)
+{
+ if (in_array(0, $idList)) {
+ return array_values(Location::getTree());
+ }
+ $locations = Location::getTree();
+
+ $ret = findLocations($locations, $idList);
+ return $ret;
+}
+
+function findLocations($locations, $idList)
+{
+ $ret = array();
+ foreach ($locations as $location) {
+ if (in_array($location['locationid'], $idList)) {
+ $ret[] = $location;
+ } elseif (!empty($location['children'])) {
+ $ret = array_merge($ret, findLocations($location['children'], $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 locationid ' . $server['locationid']
+ . ': Backend type ' . $server['type'] . ' unknown. Disabling location.');
+ Database::exec("UPDATE locationinfo_locationconfig SET serverid = 0 WHERE locationid = :lid",
+ array('lid' => $server['locationid']));
+ continue;
+ }
+ $credentialsOk = $serverInstance->setCredentials($serverid, $server['credentials']);
+
+ if ($credentialsOk) {
+ $calendarFromBackend = $serverInstance->fetchSchedule($server['idlist']);
+ } else {
+ $calendarFromBackend = array();
+ }
+
+ LocationInfo::setServerError($serverid, $serverInstance->getError());
+
+ 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
new file mode 100644
index 00000000..f9872e02
--- /dev/null
+++ b/modules-available/locationinfo/clientscript.js
@@ -0,0 +1,144 @@
+/*
+ * 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...
+ */
+
+/**
+ * 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]).attr('checked', vals['days'].indexOf(allDays[i]) !== -1);
+ }
+ }
+ $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/composer.json b/modules-available/locationinfo/composer.json
new file mode 100644
index 00000000..0d5f391b
--- /dev/null
+++ b/modules-available/locationinfo/composer.json
@@ -0,0 +1,7 @@
+{
+ "require": {
+ "php-ews/php-ews": "dev-master"
+ },
+ "minimum-stability": "dev",
+ "prefer-stable" : true
+}
diff --git a/modules-available/locationinfo/config.json b/modules-available/locationinfo/config.json
index 706412d0..87825809 100644
--- a/modules-available/locationinfo/config.json
+++ b/modules-available/locationinfo/config.json
@@ -1,3 +1,4 @@
{
- "category":"main.content"
+ "category":"main.beta",
+ "dependencies": ["js_jqueryui", "bootstrap_timepicker", "locations", "bootstrap_switch"]
}
diff --git a/modules-available/locationinfo/frontend/img/overlay/rollstuhl.svg b/modules-available/locationinfo/frontend/img/overlay/rollstuhl.svg
new file mode 100755
index 00000000..8237d543
--- /dev/null
+++ b/modules-available/locationinfo/frontend/img/overlay/rollstuhl.svg
@@ -0,0 +1,104 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://web.resource.org/cc/"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.0"
+ width="240"
+ height="360"
+ id="svg2"
+ sodipodi:version="0.32"
+ inkscape:version="0.44.1"
+ sodipodi:docname="Rollstuhl_aus_Zusatzzeichen_1044-10.svg"
+ sodipodi:docbase="L:\Wiki\pedia\ksh\svg">
+ <metadata
+ id="metadata4235">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title>Symbol: Rollstuhl</dc:title>
+ <dc:creator>
+ <cc:Agent>
+ <dc:title>Purodha Blissenbach</dc:title>
+ </cc:Agent>
+ </dc:creator>
+ <dc:source>
+http://commons.mediawiki.org/wiki/Image:Rollstuhl_Zusatzzeichen_1044-10.svg</dc:source>
+ <cc:license
+ rdf:resource="http://web.resource.org/cc/PublicDomain" />
+ </cc:Work>
+ <cc:License
+ rdf:about="http://web.resource.org/cc/PublicDomain">
+ <cc:permits
+ rdf:resource="http://web.resource.org/cc/Reproduction" />
+ <cc:permits
+ rdf:resource="http://web.resource.org/cc/Distribution" />
+ <cc:permits
+ rdf:resource="http://web.resource.org/cc/DerivativeWorks" />
+ </cc:License>
+ </rdf:RDF>
+ </metadata>
+ <sodipodi:namedview
+ inkscape:window-height="734"
+ inkscape:window-width="1001"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="1"
+ guidetolerance="10.0"
+ gridtolerance="10.0"
+ objecttolerance="10.0"
+ borderopacity="1.0"
+ bordercolor="#666666"
+ pagecolor="white"
+ id="base"
+ inkscape:zoom="1.0806949"
+ inkscape:cx="150"
+ inkscape:cy="171.76267"
+ inkscape:window-x="169"
+ inkscape:window-y="84"
+ inkscape:current-layer="svg2"
+ height="360px"
+ width="240px"
+ inkscape:showpageshadow="false"
+ gridempspacing="20"
+ showgrid="true" />
+ <defs
+ id="defs4" />
+ <g
+ transform="matrix(7.357704,0,0,7.357704,434.9445,-2099.79)"
+ id="g8408">
+ <path
+ d="M -35.212572,318.50098 C -35.211894,322.66083 -38.583928,326.03341 -42.743778,326.03341 C -46.903628,326.03341 -50.275662,322.66083 -50.274984,318.50098 C -50.275662,314.34113 -46.903628,310.96855 -42.743778,310.96855 C -38.583928,310.96855 -35.211894,314.34113 -35.212572,318.50098 L -35.212572,318.50098 z "
+ transform="matrix(1.103775,0,0,1.103775,8.653227,-30.10701)"
+ style="opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1.81196344;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ id="path3069" />
+ <path
+ d="M -36.78532,312.21483 C -36.78532,316.98239 -36.78532,314.68153 -36.78532,314.68153"
+ style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:white;stroke-width:1.76695538px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="path6617" />
+ <path
+ d="M -47.292192,317.15326 C -41.96024,317.15326 -44.051937,317.15326 -44.051937,317.15326"
+ style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:white;stroke-width:1.976125px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="path5730" />
+ <path
+ d="M -44.56103,313.67108 C -39.229078,313.67108 -41.320775,313.67108 -41.320775,313.67108"
+ style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:white;stroke-width:1.976125px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="path8406" />
+ <path
+ d="M -55.667836,330.11748 C -54.347105,325.16278 -51.67252,315.22275 -51.515523,314.89338 C -51.186883,314.20393 -51.121917,313.65381 -50.034883,313.60631 L -42.886955,313.69168 L -42.886955,310.66214 L -50.333498,310.66214 C -50.178853,309.98746 -50.383012,309.31278 -49.164921,308.6381 L -42.934292,308.6381 L -42.934292,302.66225 C -42.934292,301.09614 -37.064554,301.09983 -37.064554,302.66225 L -37.064554,314.73308 C -37.064554,315.9654 -38.193966,316.81589 -39.857413,316.81589 L -49.324732,316.81589 L -53.490353,330.16481 C -53.745584,331.11734 -55.667836,330.83456 -55.667836,330.11748 z "
+ style="fill:black;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="path6630" />
+ <path
+ d="M -53.443015,298.59131 C -53.442426,300.16032 -54.714197,301.43257 -56.283211,301.43257 C -57.852224,301.43257 -59.123995,300.16032 -59.123406,298.59131 C -59.123995,297.0223 -57.852224,295.75005 -56.283211,295.75005 C -54.714197,295.75005 -53.442426,297.0223 -53.443015,298.59131 z "
+ transform="matrix(1.083333,0,0,1.083333,20.67569,-26.8545)"
+ style="opacity:1;fill:black;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ id="path6632" />
+ </g>
+</svg>
diff --git a/modules-available/locationinfo/hooks/runmode/config.json b/modules-available/locationinfo/hooks/runmode/config.json
new file mode 100644
index 00000000..4dd7282f
--- /dev/null
+++ b/modules-available/locationinfo/hooks/runmode/config.json
@@ -0,0 +1,7 @@
+{
+ "getModeName": "LocationInfo::getPanelName",
+ "isClient": false,
+ "configHook": "LocationInfo::configHook",
+ "noSysconfig": true,
+ "systemdDefaultTarget": "kiosk-mode"
+} \ No newline at end of file
diff --git a/modules-available/locationinfo/hooks/translation.inc.php b/modules-available/locationinfo/hooks/translation.inc.php
new file mode 100644
index 00000000..e83dfd2d
--- /dev/null
+++ b/modules-available/locationinfo/hooks/translation.inc.php
@@ -0,0 +1,24 @@
+<?php
+
+$HANDLER = array();
+
+if (Module::isAvailable('locationinfo')) {
+ $HANDLER['subsections'] = array();
+ foreach (CourseBackend::getList() as $backend) {
+ // Define subsections
+ $HANDLER['subsections'][] = 'backend-' . $backend;
+ // Grep handlers to detect tags
+ $HANDLER['grep_backend-' . $backend] = function($module) use ($backend) {
+ $b = CourseBackend::getInstance($backend);
+ if ($b === false)
+ return array();
+ $props = $b->getCredentialDefinitions();
+ $return = array();
+ foreach ($props as $prop) {
+ $return[$prop->property] = true;
+ $return[$prop->property . '_helptext'] = true;
+ }
+ return $return;
+ };
+ }
+}
diff --git a/modules-available/locationinfo/inc/coursebackend.inc.php b/modules-available/locationinfo/inc/coursebackend.inc.php
new file mode 100644
index 00000000..1fe87202
--- /dev/null
+++ b/modules-available/locationinfo/inc/coursebackend.inc.php
@@ -0,0 +1,365 @@
+<?php
+
+/**
+ * Base class for course query backends
+ */
+abstract class CourseBackend
+{
+
+ /*
+ * Static part for handling interfaces
+ */
+
+ /**
+ * @var array list of known backends
+ */
+ private static $backendTypes = false;
+ /**
+ * @var boolean|string false = no error, error message otherwise
+ */
+ protected $error;
+ /**
+ * @var int as internal serverId
+ */
+ protected $serverId;
+ /**
+ * @const int max number of additional locations to fetch (for backends that benefit from request coalesc.)
+ */
+ const MAX_ADDIDIONAL_LOCATIONS = 5;
+
+ /**
+ * CourseBackend constructor.
+ */
+ public final function __construct()
+ {
+ $this->error = false;
+ }
+
+ /**
+ * Load all known backend types. This is done
+ * by including *.inc.php from inc/coursebackend/.
+ */
+ public static function loadDb()
+ {
+ if (self::$backendTypes !== false)
+ return;
+ 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);
+ if (!class_exists('coursebackend_' . $out[1])) {
+ trigger_error("Backend type source unit $file doesn't seem to define class CourseBackend_{$out[1]}", E_USER_ERROR);
+ }
+ self::$backendTypes[$out[1]] = true;
+ }
+ }
+
+ /**
+ * Get all known config module types.
+ *
+ * @return array list of modules
+ */
+ public static function getList()
+ {
+ self::loadDb();
+ return array_keys(self::$backendTypes);
+ }
+
+ /**
+ * Get fresh instance of ConfigModule subclass for given module type.
+ *
+ * @param string $moduleType name of module type
+ * @return \CourseBackend module instance
+ */
+ public static function getInstance($moduleType)
+ {
+ self::loadDb();
+ if (!isset(self::$backendTypes[$moduleType])) {
+ error_log('Unknown module type: ' . $moduleType);
+ return false;
+ }
+ if (!is_object(self::$backendTypes[$moduleType])) {
+ $class = "coursebackend_$moduleType";
+ self::$backendTypes[$moduleType] = new $class;
+ }
+ return self::$backendTypes[$moduleType];
+ }
+
+ /**
+ * @return string return display name of backend
+ */
+ public abstract function getDisplayName();
+
+
+ /**
+ * @returns \BackendProperty[] list of properties that need to be set
+ */
+ public abstract function getCredentialDefinitions();
+
+ /**
+ * @return boolean true if the connection works, false otherwise
+ */
+ public abstract function checkConnection();
+
+ /**
+ * uses json to setCredentials, the json must follow the form given in
+ * getCredentials
+ *
+ * @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);
+
+ /**
+ * @return int desired caching time of results, in seconds. 0 = no caching
+ */
+ public abstract function getCacheTime();
+
+ /**
+ * @return int age after which timetables are no longer refreshed should be
+ * greater then CacheTime
+ */
+ public abstract function getRefreshTime();
+
+ /**
+ * Internal version of fetch, to be overridden by subclasses.
+ *
+ * @param $roomIds array with local ID as key and serverId as value
+ * @return array a recursive array that uses the roomID as key
+ * and has the schedule array as value. A shedule array contains an array in this format:
+ * ["start"=>'JJJJ-MM-DD HH:MM:SS',"end"=>'JJJJ-MM-DD HH:MM:SS',"title"=>string]
+ */
+ protected abstract function fetchSchedulesInternal($roomId);
+
+ /**
+ * Method for fetching the schedule of the given rooms on a server.
+ *
+ * @param array $roomId 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
+ */
+ public final function fetchSchedule($requestedLocationIds)
+ {
+ if (!is_array($requestedLocationIds)) {
+ $this->error = 'No array of roomids was given to fetchSchedule';
+ return false;
+ }
+ if (empty($requestedLocationIds))
+ return array();
+ $NOW = time();
+ $dbquery1 = Database::simpleQuery("SELECT locationid, calendar, serverlocationid, lastcalendarupdate
+ FROM locationinfo_locationconfig WHERE locationid IN (:locations)",
+ array('locations' => array_values($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 {
+ $remoteIds[$row['locationid']] = $row['serverlocationid'];
+ }
+
+ }
+ // No need for additional round trips to backend
+ if (empty($remoteIds)) {
+ return $returnValue;
+ }
+ // Check if we should refresh other rooms recently requested by front ends
+ if ($this->getRefreshTime() > $this->getCacheTime()) {
+ $dbquery4 = Database::simpleQuery("SELECT locationid, serverlocationid FROM locationinfo_locationconfig
+ WHERE serverid = :serverid AND serverlocationid NOT IN (:skiplist)
+ AND lastcalendarupdate BETWEEN :lowerage AND :upperage
+ LIMIT " . self::MAX_ADDIDIONAL_LOCATIONS, array(
+ 'serverid' => $this->serverId,
+ 'skiplist' => array_values($remoteIds),
+ 'lowerage' => $NOW - $this->getRefreshTime(),
+ 'upperage' => $NOW - $this->getCacheTime(),
+ ));
+ while ($row = $dbquery4->fetch(PDO::FETCH_ASSOC)) {
+ $remoteIds[$row['locationid']] = $row['serverlocationid'];
+ }
+ }
+ $backendResponse = $this->fetchSchedulesInternal($remoteIds);
+ if ($backendResponse === false) {
+ return false;
+ }
+
+ if ($this->getCacheTime() > 0) {
+ // Caching requested by backend, write to DB
+ foreach ($backendResponse as $serverRoomId => $calendar) {
+ $value = json_encode($calendar);
+ Database::simpleQuery("UPDATE locationinfo_locationconfig SET calendar = :ttable, lastcalendarupdate = :now
+ WHERE serverid = :serverid AND serverlocationid = :serverlocationid", array(
+ 'serverid' => $this->serverId,
+ 'serverlocationid' => $serverRoomId,
+ 'ttable' => $value,
+ 'now' => $NOW
+ ));
+ }
+ }
+ // Add rooms that were requested to the final return value
+ foreach ($remoteIds as $location => $serverRoomId) {
+ if (isset($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];
+ }
+ }
+
+ return $returnValue;
+ }
+
+ public final function setCredentials($serverId, $data)
+ {
+ foreach ($this->getCredentialDefinitions() as $prop) {
+ if (!isset($data[$prop->property])) {
+ $data[$prop->property] = $prop->default;
+ }
+ if (in_array($prop->type, ['string', 'bool', 'int'])) {
+ settype($data[$prop->property], $prop->type);
+ } else {
+ settype($data[$prop->property], 'string');
+ }
+ }
+ if ($this->setCredentialsInternal($data)) {
+ $this->serverId = $serverId;
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * @return false if there was no error string with error message if there was one
+ */
+ public final function getError()
+ {
+ return $this->error;
+ }
+
+ /**
+ * Query path in array-representation of XML document.
+ * e.g. 'path/syntax/foo/wanteditem'
+ * This works for intermediate nodes (that have more children)
+ * and leaf nodes. The result is always an array on success, or
+ * false if not found.
+ */
+ protected function getArrayPath($array, $path)
+ {
+ if (!is_array($path)) {
+ // Convert 'path/syntax/foo/wanteditem' to array for further processing and recursive calls
+ $path = explode('/', $path);
+ }
+ if (isset($array[0])) {
+ // The currently handled element of the path exists multiple times on the current level, so it is
+ // wrapped in a plain array - recurse into each one of them and merge the results
+ $return = [];
+ foreach ($array as $item) {
+ $test = $this->getArrayPath($item, $path);
+ If (is_array($test)) {
+ $return = array_merge($return, $test);
+ }
+
+ }
+ return $return;
+ }
+ do {
+ // Get next element from array, loop to ignore empty elements (so double slashes in the path are allowed)
+ $element = array_shift($path);
+ } while (empty($element) && !empty($path));
+ if (!isset($array[$element])) {
+ // Current path element does not exist - error
+ return false;
+ }
+ if (empty($path)) {
+ // Path is now empty which means we're at 'wanteditem' from out example above
+ if (!is_array($array[$element]) || !isset($array[$element][0])) {
+ // If it's a leaf node of the array, wrap it in plain array, so the function will
+ // always return an array on success
+ return array($array[$element]);
+ }
+ // 'wanteditem' is not a unique leaf node, return as is
+ // This means it's either a plain array, in case there are multiple 'wanteditem' elements on the same level
+ // or it's an associative array if 'wanteditem' has any sub-nodes
+ return $array[$element];
+ }
+ // Recurse
+ if (!is_array($array[$element])) {
+ // We're in the middle of the requested path, but the current element is already a leaf node with no
+ // children - error
+ return false;
+ }
+ // Non-leaf node - simple recursion
+ return $this->getArrayPath($array[$element], $path);
+ }
+
+ /**
+ * @param string $response xml document to convert
+ * @return bool|array array representation of the xml if possible, false otherwise
+ */
+ protected function xmlStringToArray($response)
+ {
+ $cleanresponse = preg_replace('/(<\/?)(\w+):([^>]*>)/', '$1$2$3', $response);
+ try {
+ $xml = new SimpleXMLElement($cleanresponse);
+ } catch (Exception $e) {
+ $this->error = 'Could not parse reply as XML, got ' . get_class($e) . ': ' . $e->getMessage();
+ return false;
+ }
+ $array = json_decode(json_encode((array)$xml), true);
+ return $array;
+ }
+
+}
+
+/**
+ * Class BackendProperty describes a property a backend requires to define its functionality
+ */
+class BackendProperty {
+ public $property;
+ public $type;
+ public $default;
+ public function __construct($property, $type, $default = '')
+ {
+ $this->property = $property;
+ $this->type = $type;
+ $this->default = $default;
+ }
+
+ /**
+ * 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) {
+ if (is_array($this->type)) {
+ $this->template = 'dropdown';
+ $this->select_list = [];
+ foreach ($this->type as $item) {
+ $this->select_list[] = [
+ 'option' => $item,
+ 'active' => $item == $current,
+ ];
+ }
+ } elseif ($this->type === 'bool') {
+ $this->template = $this->type;
+ } else {
+ $this->template = 'generic';
+ }
+ if ($this->type === 'string') {
+ $this->inputtype = 'text';
+ } elseif ($this->type === 'int') {
+ $this->inputtype = 'number';
+ } elseif ($this->type === 'password') {
+ $this->inputtype = Property::getPasswordFieldType();
+ }
+ $this->currentvalue = $current === null ? $this->default : $current;
+ }
+ public $inputtype;
+ public $template;
+ public $title;
+ public $helptext;
+ public $currentvalue;
+ public $select_list;
+ public $credentialsHtml;
+}
diff --git a/modules-available/locationinfo/inc/coursebackend/coursebackend_davinci.inc.php b/modules-available/locationinfo/inc/coursebackend/coursebackend_davinci.inc.php
new file mode 100644
index 00000000..fac3f296
--- /dev/null
+++ b/modules-available/locationinfo/inc/coursebackend/coursebackend_davinci.inc.php
@@ -0,0 +1,142 @@
+<?php
+
+class CourseBackend_Davinci extends CourseBackend
+{
+
+ private $location;
+ private $verifyHostname = true;
+ private $verifyCert = true;
+
+ public function setCredentialsInternal($data)
+ {
+ if (empty($data['baseUrl'])) {
+ $this->error = "No url is given";
+ return false;
+ }
+ $location = preg_replace('#/+(davinciis\.dll)?\W*$#i', '', $data['baseUrl']);
+ $this->location = $location . "/DAVINCIIS.dll?";
+ $this->verifyHostname = $data['verifyHostname'];
+ $this->verifyCert = $data['verifyCert'];
+ return true;
+ }
+
+ public function checkConnection()
+ {
+ if (empty($this->location)) {
+ $this->error = "Credentials are not set";
+ } else {
+ $startDate = new DateTime('today 0:00');
+ $endDate = new DateTime('+7 days 0:00');
+ $data = $this->fetchRoomRaw('someroomid123', $startDate, $endDate);
+ if ($data !== false && strpos($data, 'DAVINCI SERVER') === false) {
+ $this->error = "Unknown reply; this doesn't seem to be a DAVINCI server.";
+ }
+ }
+ return $this->error === false;
+ }
+
+ public function getCredentialDefinitions()
+ {
+ return [
+ new BackendProperty('baseUrl', 'string'),
+ new BackendProperty('verifyCert', 'bool', true),
+ new BackendProperty('verifyHostname', 'bool', true)
+ ];
+ }
+
+ public function getDisplayName()
+ {
+ return 'Davinci';
+ }
+
+ public function getCacheTime()
+ {
+ return 30 * 60;
+ }
+
+ public function getRefreshTime()
+ {
+ return 0;
+ }
+
+ /**
+ * @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
+ */
+ private function fetchRoomRaw($roomId, $startDate, $endDate)
+ {
+ $url = $this->location . "content=xml&type=room&name=" . urlencode($roomId)
+ . "&startdate=" . $startDate->format('d.m.Y') . "&enddate=" . $endDate->format('d.m.Y');
+ $ch = 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 => $url,
+ );
+
+ curl_setopt_array($ch, $options);
+ $output = curl_exec($ch);
+ if ($output === false) {
+ $this->error = 'Curl error: ' . curl_error($ch);
+ return false;
+ } else {
+ $this->error = false;
+ ///Operation completed successfully
+ }
+ curl_close($ch);
+ return $output;
+
+ }
+
+ public function fetchSchedulesInternal($requestedRoomIds)
+ {
+ $startDate = new DateTime('today 0:00');
+ $endDate = new DateTime('+7 days 0:00');
+ $lower = (int)$startDate->format('Ymd');
+ $upper = (int)$endDate->format('Ymd');
+ $schedules = [];
+ foreach ($requestedRoomIds as $roomId) {
+ $return = $this->fetchRoomRaw($roomId, $startDate, $endDate);
+ if ($return === false) {
+ continue;
+ }
+ $return = $this->xmlStringToArray($return);
+ if ($return === false) {
+ continue;
+ }
+ $lessons = $this->getArrayPath($return, '/Lessons/Lesson');
+ if ($lessons === false) {
+ $this->error = "Cannot find /Lessons/Lesson in XML";
+ continue;
+ }
+ $timetable = [];
+ foreach ($lessons as $lesson) {
+ if (!isset($lesson['Date']) || !isset($lesson['Start']) || !isset($lesson['Finish'])) {
+ $this->error = 'Lesson is missing Date, Start or Finish';
+ continue;
+ }
+ $c = (int)$lesson['Date'];
+ if ($c < $lower || $c > $upper)
+ continue;
+ $date = $lesson['Date'];
+ $date = substr($date, 0, 4) . '-' . substr($date, 4, 2) . '-' . substr($date, 6, 2);
+ $start = $lesson['Start'];
+ $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'] : '???';
+ $timetable[] = array(
+ 'title' => $subject,
+ 'start' => $date . "T" . $start . ':00',
+ 'end' => $date . "T" . $end . ':00'
+ );
+ }
+ $schedules[$roomId] = $timetable;
+ }
+ return $schedules;
+ }
+}
diff --git a/modules-available/locationinfo/inc/coursebackend/coursebackend_dummy.inc.php b/modules-available/locationinfo/inc/coursebackend/coursebackend_dummy.inc.php
new file mode 100644
index 00000000..e2577284
--- /dev/null
+++ b/modules-available/locationinfo/inc/coursebackend/coursebackend_dummy.inc.php
@@ -0,0 +1,128 @@
+<?php
+
+class CourseBackend_Dummy extends CourseBackend
+{
+ private $pw;
+
+ /**
+ * uses json to setCredentials, the json must follow the form given in
+ * getCredentials
+ *
+ * @param array $data with the credentials
+ * @param string $url address of the server
+ * @param int $serverId ID of the server
+ * @returns bool if the credentials were in the correct format
+ */
+ public function setCredentialsInternal($json)
+ {
+ $x = $json;
+ $this->pw = $x['password'];
+
+ if ($this->pw === "mfg") {
+ $this->error = false;
+ return true;
+ } else {
+ $this->error = "USE mfg as password!";
+ return false;
+ }
+ }
+
+ /**
+ * @return boolean true if the connection works, false otherwise
+ */
+ public function checkConnection()
+ {
+ if ($this->pw == "mfg") {
+ $this->error = false;
+ return true;
+ } else {
+ $this->error = "USE mfg as password!";
+ return false;
+ }
+ }
+
+ /**
+ * @returns array with parameter name as key and and an array with type, help text and mask as value
+ */
+ public function getCredentialDefinitions()
+ {
+ $options = ["opt1", "opt2", "opt3", "opt4", "opt5", "opt6", "opt7", "opt8"];
+ return [
+ new BackendProperty('username', 'string', 'default-user'),
+ new BackendProperty('password', 'password'),
+ new BackendProperty('integer', 'int', 7),
+ new BackendProperty('option', $options),
+ new BackendProperty('CheckTheBox', 'bool'),
+ new BackendProperty('CB2t', 'bool', true)
+ ];
+ }
+
+ /**
+ * @return string return display name of backend
+ */
+ public function getDisplayName()
+ {
+ return 'Dummy with array';
+ }
+
+ /**
+ * @return int desired caching time of results, in seconds. 0 = no caching
+ */
+ public function getCacheTime()
+ {
+ return 0;
+ }
+
+ /**
+ * @return int age after which timetables are no longer refreshed should be
+ * greater then CacheTime
+ */
+ public function getRefreshTime()
+ {
+ return 0;
+ }
+
+ /**
+ * Internal version of fetch, to be overridden by subclasses.
+ *
+ * @param $roomIds array with local ID as key and serverId as value
+ * @return array a recursive array that uses the roomID as key
+ * and has the schedule array as value. A shedule array contains an array in this format:
+ * ["start"=>'JJJJ-MM-DD HH:MM:SS',"end"=>'JJJJ-MM-DD HH:MM:SS',"title"=>string]
+ */
+ public function fetchSchedulesInternal($roomId)
+ {
+ $a = array();
+ foreach ($roomId as $id) {
+ $x = array();
+ $time = strtotime('today');
+ $end = strtotime('+7 days', $time);
+ srand(crc32($id) ^ $time);
+ $last = $time;
+ do {
+ do {
+ $time += rand(4, 10) * 900;
+ $h = date('G', $time);
+ } while ($h < 7 || $h > 19);
+ $today = strtotime('today', $time);
+ if ($today !== $last) {
+ srand(crc32($id) ^ $today);
+ $last = $today;
+ }
+ $dur = rand(2,6) * 1800;
+ $x[] = array(
+ 'title' => 'Test ' . rand(1000,9999),
+ 'start' => date('Y-m-d\TH:i:s', $time),
+ 'end' => date('Y-m-d\TH:i:s', $time + $dur),
+ );
+ $time += $dur;
+ } while ($time < $end);
+ $a[$id] = $x;
+ }
+
+ return $a;
+ }
+
+}
+
+?>
diff --git a/modules-available/locationinfo/inc/coursebackend/coursebackend_exchange.inc.php b/modules-available/locationinfo/inc/coursebackend/coursebackend_exchange.inc.php
new file mode 100755
index 00000000..538f7382
--- /dev/null
+++ b/modules-available/locationinfo/inc/coursebackend/coursebackend_exchange.inc.php
@@ -0,0 +1,203 @@
+<?php
+
+require_once __DIR__ . '/../../vendor/autoload.php';
+
+use jamesiarmes\PhpEws\Client;
+use jamesiarmes\PhpEws\Enumeration\DefaultShapeNamesType;
+use jamesiarmes\PhpEws\Enumeration\DistinguishedFolderIdNameType;
+use jamesiarmes\PhpEws\Enumeration\ItemQueryTraversalType;
+use jamesiarmes\PhpEws\Enumeration\ResponseClassType;
+use jamesiarmes\PhpEws\Request\FindItemType;
+use jamesiarmes\PhpEws\Request\ResolveNamesType;
+use jamesiarmes\PhpEws\Type\CalendarViewType;
+use jamesiarmes\PhpEws\Type\DistinguishedFolderIdType;
+use jamesiarmes\PhpEws\Type\EmailAddressType;
+use jamesiarmes\PhpEws\Type\ItemResponseShapeType;
+
+class CourseBackend_Exchange extends CourseBackend {
+
+ private $username = '';
+ private $password = '';
+ private $baseUrl;
+ private $client_version;
+ private $timezone = 'W. Europe Standard Time'; // TODO: make this configurable some time
+ private $verifyHostname = true;
+ private $verifyCert = true;
+
+ /**
+ * @return string return display name of backend
+ */
+ public function getDisplayName() {
+ return "Microsoft Exchange";
+ }
+
+ /**
+ * @returns \BackendProperty[] list of properties that need to be set
+ */
+ public function getCredentialDefinitions() {
+ $options = [Client::VERSION_2007, Client::VERSION_2007_SP1, Client::VERSION_2009, Client::VERSION_2010,
+ Client::VERSION_2010_SP1, Client::VERSION_2010_SP2, Client::VERSION_2013, Client::VERSION_2013_SP1, Client::VERSION_2016];
+ return [
+ new BackendProperty('baseUrl', 'string'),
+ new BackendProperty('username', 'string'),
+ new BackendProperty('password', 'password'),
+ new BackendProperty('client_version', $options),
+ new BackendProperty('verifyCert', 'bool', true),
+ new BackendProperty('verifyHostname', 'bool', true)
+ ];
+ }
+
+ /**
+ * @return boolean true if the connection works, false otherwise
+ */
+ public function checkConnection() {
+ $client = $this->getClient();
+ $request = new ResolveNamesType();
+ $request->UnresolvedEntry = $this->username;
+ $request->ReturnFullContactData = false;
+
+ try {
+ $response = $client->ResolveNames($request);
+ } catch (Exception $e) {
+ error_log("There was an error");
+ error_log($e->getMessage());
+ return false;
+ }
+
+ if ($response->ResponseMessages->ResolveNamesResponseMessage[0]->ResponseCode == "NoError") {
+ $mailadress = $response->ResponseMessages->ResolveNamesResponseMessage[0]->ResolutionSet->Resolution[0]->Mailbox->EmailAddress;
+ return !empty($mailadress);
+ }
+ return false;
+ }
+
+ /**
+ * uses json to setCredentials, the json must follow the form given in
+ * getCredentials
+ *
+ * @param array $data assoc array with data required by backend
+ * @returns bool if the credentials were in the correct format
+ */
+ public function setCredentialsInternal($data) {
+ foreach (['username', 'password'] as $field) {
+ if (empty($data[$field])) {
+ $this->error = 'setCredentials: Missing field ' . $field;
+ return false;
+ }
+ }
+
+ if (empty($data['baseUrl'])) {
+ $this->error = "No url is given";
+ return false;
+ }
+
+ $this->username = $data['username'];
+ $this->password = $data['password'];
+
+ $this->baseUrl = $data['baseUrl'];
+ $this->client_version = $data['client_version'];
+
+ $this->verifyHostname = $data['verifyHostname'];
+ $this->verifyCert = $data['verifyCert'];
+
+ return true;
+ }
+
+ /**
+ * @return int desired caching time of results, in seconds. 0 = no caching
+ */
+ public function getCacheTime() {
+ return 0;
+ }
+
+ /**
+ * @return int age after which timetables are no longer refreshed should be
+ * greater then CacheTime
+ */
+ public function getRefreshTime() {
+ return 0;
+ }
+
+ /**
+ * Internal version of fetch, to be overridden by subclasses.
+ *
+ * @param $roomIds array with local ID as key and serverId as value
+ * @return array a recursive array that uses the roomID as key
+ * and has the schedule array as value. A shedule array contains an array in this format:
+ * ["start"=>'JJJJ-MM-DD HH:MM:SS',"end"=>'JJJJ-MM-DD HH:MM:SS',"title"=>string]
+ */
+ protected function fetchSchedulesInternal($requestedRoomIds) {
+ $startDate = new DateTime('today 0:00');
+ $endDate = new DateTime('+7 days 0:00');
+ $client = $this->getClient();
+
+ $schedules = [];
+ foreach ($requestedRoomIds as $roomId) {
+ $items = $this->findEventsForRoom($client, $startDate, $endDate, $roomId);
+
+ // 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);
+
+ $schedules[$roomId][] = array(
+ 'title' => $item->Subject,
+ 'start' => $start->format('Y-m-d') . "T" . $start->format('G:i:s'),
+ 'end' => $end->format('Y-m-d') . "T" . $end->format('G:i:s')
+ );
+ }
+ }
+ return $schedules;
+ }
+
+ public function findEventsForRoom($client, $start_date, $end_date, $email_room) {
+ $request = new FindItemType();
+ $request->Traversal = ItemQueryTraversalType::SHALLOW;
+ $request->ItemShape = new ItemResponseShapeType();
+ $request->ItemShape->BaseShape = DefaultShapeNamesType::ALL_PROPERTIES;
+
+ $request->CalendarView = new CalendarViewType();
+ $request->CalendarView->StartDate = $start_date->format('c');
+ $request->CalendarView->EndDate = $end_date->format('c');
+ $folder_id = new DistinguishedFolderIdType();
+ $folder_id->Id = DistinguishedFolderIdNameType::CALENDAR;
+ $folder_id->Mailbox = new EmailAddressType();
+ $folder_id->Mailbox->EmailAddress = $email_room;
+ $request->ParentFolderIds->DistinguishedFolderId[] = $folder_id;
+ $response = $client->FindItem($request);
+ $response_messages = $response->ResponseMessages->FindItemResponseMessage;
+
+ $items = [];
+ foreach ($response_messages as $response_message) {
+ // Make sure the request succeeded.
+ if ($response_message->ResponseClass != ResponseClassType::SUCCESS) {
+ $code = $response_message->ResponseCode;
+ $message = $response_message->MessageText;
+ error_log("Failed to search for events with \"$code: $message\"\n");
+ continue;
+ }
+ $items = $response_message->RootFolder->Items->CalendarItem;
+ }
+ return $items;
+ }
+
+ public function getClient() {
+ $client = new Client($this->baseUrl, $this->username, $this->password, $this->client_version);
+ $client->setTimezone($this->timezone);
+ $client->setCurlOptions(array(
+ CURLOPT_SSL_VERIFYPEER => $this->verifyHostname,
+ CURLOPT_SSL_VERIFYHOST => $this->verifyCert
+ ));
+
+ return $client;
+ }
+
+ function var_error_log($object = null) {
+ ob_start(); // start buffer capture
+ var_dump($object); // dump the values
+ $contents = ob_get_contents(); // put the buffer into a variable
+ ob_end_clean(); // end capture
+ error_log($contents); // log contents of the result of var_dump( $object )
+ }
+}
+?>
diff --git a/modules-available/locationinfo/inc/coursebackend/coursebackend_hisinone.inc.php b/modules-available/locationinfo/inc/coursebackend/coursebackend_hisinone.inc.php
new file mode 100644
index 00000000..59bd9dc8
--- /dev/null
+++ b/modules-available/locationinfo/inc/coursebackend/coursebackend_hisinone.inc.php
@@ -0,0 +1,368 @@
+<?php
+
+class CourseBackend_HisInOne extends CourseBackend
+{
+ private $username = '';
+ private $password = '';
+ private $open = true;
+ private $location;
+ private $verifyHostname = true;
+ private $verifyCert = true;
+
+
+ public function setCredentialsInternal($data)
+ {
+ if (!$data['open']) {
+ // If not using OpenCourseService, require credentials
+ foreach (['username', 'password'] as $field) {
+ if (empty($data[$field])) {
+ $this->error = 'setCredentials: Missing field ' . $field;
+ return false;
+ }
+ }
+ }
+ if (empty($data['baseUrl'])) {
+ $this->error = "No url is given";
+ return false;
+ }
+
+ $this->error = false;
+ $this->username = $data['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'];
+
+ return true;
+ }
+
+ public function getCredentialDefinitions()
+ {
+ 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()
+ {
+ if (empty($this->location)) {
+ $this->error = "Credentials are not set";
+ } else {
+ $this->findUnit(123456789, true);
+ }
+ return $this->error === 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, $connectionCheckOnly = false)
+ {
+ $termYear = date('Y');
+ $termType1 = date('n');
+ if ($termType1 > 3 && $termType1 < 10) {
+ $termType = 2;
+ } elseif ($termType1 > 10) {
+ $termType = 1;
+ $termYear = $termYear + 1;
+ } else {
+ $termType = 1;
+ }
+ $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('termYear', $termYear));
+ if ($termType1 != 3 && $termType1 != 10) {
+ $findUnit->appendChild($doc->createElement('termTypeValueId', $termType));
+ }
+ $findUnit->appendChild($doc->createElement('ns1:roomId', $roomId));
+
+ $soap_request = $doc->saveXML();
+ $response1 = $this->postToServer($soap_request, "findUnit");
+ if ($this->error !== false) {
+ return false;
+ }
+ $response2 = $this->xmlStringToArray($response1);
+ if (!is_array($response2)) {
+ if ($this->error === false) {
+ $this->error = 'Cannot convert XML response to array';
+ }
+ return false;
+ }
+ if (!isset($response2['soapenvBody'])) {
+ $this->error = 'Backend reply is missing element soapenvBody';
+ return false;
+ }
+ if (isset($response2['soapenvBody']['soapenvFault'])) {
+ $this->error = 'SOAP-Fault (' . $response2['soapenvBody']['soapenvFault']['faultcode'] . ") " . $response2['soapenvBody']['soapenvFault']['faultstring'];
+ 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->error = 'Cannot find ' . $path;
+ //@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->error = 'Cannot find ' . $subpath . ' after ' . $path;
+ @file_put_contents('/tmp/findUnit-2.' . $roomId . '.' . microtime(true), print_r($idSubDoc, true));
+ }
+ return $idList;
+ }
+
+ /**
+ * @param $doc DOMDocument
+ * @return DOMElement
+ */
+ private function getHeader($doc)
+ {
+ $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;
+ }
+
+ /**
+ * @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)
+ {
+ $header = array(
+ "Content-type: text/xml;charset=\"utf-8\"",
+ "SOAPAction: \"" . $action . "\"",
+ "Content-length: " . strlen($request),
+ );
+
+ $soap_do = 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,
+ );
+
+ curl_setopt_array($soap_do, $options);
+
+ $output = curl_exec($soap_do);
+
+ if ($output === false) {
+ $this->error = 'Curl error: ' . curl_error($soap_do);
+ } else {
+ $this->error = false;
+ ///Operation completed successfully
+ }
+ curl_close($soap_do);
+ return $output;
+ }
+
+ public function getCacheTime()
+ {
+ return 30 * 60;
+ }
+
+
+ public function getRefreshTime()
+ {
+ return 60 * 60;
+ }
+
+
+ public function getDisplayName()
+ {
+ return "HisInOne";
+ }
+
+ public function fetchSchedulesInternal($requestedRoomIds)
+ {
+ if (empty($requestedRoomIds)) {
+ return array();
+ }
+ $tTables = [];
+ //get all eventIDs in a given room
+ $eventIds = [];
+ foreach ($requestedRoomIds as $roomId) {
+ $roomEventIds = $this->findUnit($roomId);
+ if ($roomEventIds === false) {
+ if ($this->error !== false) {
+ error_log('Cannot findUnit(' . $roomId . '): ' . $this->error);
+ $this->error = false;
+ }
+ // TODO: Error gets swallowed
+ continue;
+ }
+ $tTables[$roomId] = [];
+ $eventIds = array_merge($eventIds, $roomEventIds);
+ }
+ $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) {
+ error_log('Cannot readUnit(' . $eventId . '): ' . $this->error);
+ $this->error = false;
+ // TODO: Error gets swallowed
+ continue;
+ }
+ $eventDetails = array_merge($eventDetails, $event);
+ }
+ $currentWeek = $this->getCurrentWeekDates();
+ $name = false;
+ foreach ($eventDetails as $event) {
+ foreach (array('/hisdefaulttext',
+ '/hisshorttext',
+ '/hisshortcomment',
+ '/hisplanelements/hisplanelement/hisdefaulttext') as $path) {
+ $name = $this->getArrayPath($event, $path);
+ if (!empty($name) && !empty($name[0]))
+ break;
+ $name = false;
+ }
+ if ($name === false) {
+ $name = ['???'];
+ }
+ $unitPlannedDates = $this->getArrayPath($event,
+ '/hisplanelements/hisplanelement/hisplannedDates/hisplannedDate/hisindividualDates/hisindividualDate');
+ if ($unitPlannedDates === false) {
+ $this->error = 'Cannot find ./hisplanelements/hisplanelement/hisplannedDates/hisplannedDate/hisindividualDates/hisindividualDate';
+ error_log('Cannot find ./hisplanelements/hisplanelement/hisplannedDates/hisplannedDate/hisindividualDates/hisindividualDate');
+ error_log(print_r($event, true));
+ continue;
+ }
+ 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' => $name[0],
+ 'start' => $eventDate . "T" . $startTime,
+ 'end' => $eventDate . "T" . $endTime
+ );
+ }
+ }
+ }
+ 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);
+ if ($response2 === false)
+ return false;
+ if (!isset($response2['soapenvBody'])) {
+ $this->error = 'Backend reply is missing element soapenvBody';
+ return false;
+ }
+ if (isset($response2['soapenvBody']['soapenvFault'])) {
+ $this->error = 'SOAP-Fault (' . $response2['soapenvBody']['soapenvFault']['faultcode'] . ") " . $response2['soapenvBody']['soapenvFault']['faultstring'];
+ 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();
+ $startDate = time();
+ for ($i = 0; $i <= 7; $i++) {
+ $returnValue[] = date('Y-m-d', strtotime("+{$i} day 12:00", $startDate));
+ }
+ return $returnValue;
+ }
+
+}
diff --git a/modules-available/locationinfo/inc/infopanel.inc.php b/modules-available/locationinfo/inc/infopanel.inc.php
new file mode 100644
index 00000000..66ee0ae7
--- /dev/null
+++ b/modules-available/locationinfo/inc/infopanel.inc.php
@@ -0,0 +1,219 @@
+<?php
+
+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
+ */
+ public static function getConfig($paneluuid, &$config)
+ {
+ $panel = Database::queryFirst('SELECT panelname, panelconfig, paneltype, locationids, lastchange FROM locationinfo_panel WHERE paneluuid = :paneluuid',
+ compact('paneluuid'));
+
+ if ($panel === false) {
+ return false;
+ }
+
+ if ($panel['paneltype'] === 'URL') {
+ $config = json_decode($panel['panelconfig'], true);
+ return $panel['paneltype'];
+ }
+
+ $config = LocationInfo::defaultPanelConfig($panel['paneltype']);
+ $locations = Location::getLocationsAssoc();
+ $overrides = false;
+
+ if (!empty($panel['panelconfig'])) {
+ $json = json_decode($panel['panelconfig'], true);
+ if (is_array($json)) {
+ if (isset($json['overrides']) && is_array($json['overrides'])) {
+ $overrides = $json['overrides'];
+ }
+ unset($json['overrides']);
+ $config = $json + $config;
+ }
+ }
+ if (isset($config['showtitle']) && $config['showtitle']) {
+ $config['title'] = $panel['title'];
+ }
+ $config['locations'] = array();
+ $lids = array_map('intval', explode(',', $panel['locationids']));
+ foreach ($lids as $lid) {
+ $config['locations'][$lid] = array(
+ 'id' => $lid,
+ 'name' => isset($locations[$lid]) ? $locations[$lid]['locationname'] : 'noname00.pas',
+ );
+ if (isset($overrides[$lid]) && is_array($overrides[$lid])) {
+ $config['locations'][$lid]['config'] = $overrides[$lid];
+ }
+ }
+ self::appendMachineData($config['locations'], $lids, true);
+ self::appendOpeningTimes($config['locations'], $lids);
+
+ $config['ts'] = (int)$panel['lastchange'];
+ $config['locations'] = array_values($config['locations']);
+ $config['time'] = date('Y-n-j-G-') . (int)date('i') . '-' . (int)(date('s'));
+
+ return $panel['paneltype'];
+ }
+
+ /**
+ * Gets the location info of the given locations.
+ * Append to passed array which is expected to
+ * map location ids to properties of that location.
+ * A new key 'machines' will be created in each
+ * entry of $array that will take all the machine data.
+ *
+ * @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)
+ {
+ if (empty($array) && $idList === false)
+ return;
+ if ($idList === false) {
+ $idList = array_keys($array);
+ }
+
+ $positionCol = $withPosition ? 'm.position,' : '';
+ $query = "SELECT m.locationid, m.machineuuid, $positionCol m.logintime, m.lastseen, m.lastboot 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)) {
+ settype($row['locationid'], 'int');
+ if (!isset($array[$row['locationid']])) {
+ $array[$row['locationid']] = array('id' => $row['locationid'], 'machines' => array());
+ }
+ if (!isset($array[$row['locationid']]['machines'])) {
+ $array[$row['locationid']]['machines'] = array();
+ }
+ // Compact the pc data in one array.
+ $pc = array('id' => $row['machineuuid']);
+ if ($withPosition && !empty($row['position'])) {
+ $position = json_decode($row['position'], true);
+ if (isset($position['gridCol']) && isset($position['gridRow'])) {
+ $pc['x'] = $position['gridCol'];
+ $pc['y'] = $position['gridRow'];
+ if (!empty($position['overlays']) && is_array($position['overlays'])) {
+ $pc['overlays'] = $position['overlays'];
+ }
+ }
+ }
+ $pc['pcState'] = LocationInfo::getPcState($row);
+ //$pc['pcState'] = ['BROKEN', 'OFF', 'IDLE', 'OCCUPIED'][mt_rand(0,3)]; // XXX
+
+ // Add the array to the machines list.
+ $array[$row['locationid']]['machines'][] = $pc;
+ }
+ }
+
+ /**
+ * Gets the Opening time of the given locations.
+ *
+ * @param array $array list of locations, indexed by locationId
+ * @param int[] $idList list of locations
+ */
+ public static function appendOpeningTimes(&$array, $idList)
+ {
+ // 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
+ WHERE locationid IN (:lids)", array('lids' => $allIds));
+ $openingTimes = array();
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ $openingTimes[(int)$row['locationid']] = $row;
+ }
+ // Now we got all the calendars for locations and parents
+ // Iterate over the locations we're actually interested in
+ $locations = Location::getLocationsAssoc();
+ foreach ($idList as $locationId) {
+ // Start checking at actual location...
+ $currentId = $locationId;
+ while ($currentId !== 0) {
+ if (!empty($openingTimes[$currentId]['openingtime'])) {
+ $cal = json_decode($openingTimes[$currentId]['openingtime'], true);
+ if (is_array($cal)) {
+ $cal = self::formatOpeningtime($cal);
+ }
+ if (!empty($cal)) {
+ // Got a valid calendar
+ if (!isset($array[$locationId])) {
+ $array[$locationId] = array('id' => $locationId);
+ }
+ $array[$locationId]['openingtime'] = $cal;
+ break;
+ }
+ }
+ // Keep trying with parent
+ $currentId = $locations[$currentId]['parentlocationid'];
+ }
+ }
+ return;
+ }
+
+
+ /**
+ * Returns all the passed location ids and appends
+ * all their direct and indirect parent location ids.
+ *
+ * @param int[] $idList location ids
+ * @return int[] more location ids
+ */
+ private static function getLocationsWithParents($idList)
+ {
+ $locations = Location::getLocationsAssoc();
+ $allIds = $idList;
+ foreach ($idList as $id) {
+ if (isset($locations[$id]) && isset($locations[$id]['parents'])) {
+ $allIds = array_merge($allIds, $locations[$id]['parents']);
+ }
+ }
+ return array_map('intval', $allIds);
+ }
+
+// ########## <Openingtime> ##########
+
+ /**
+ * Format the openingtime in the frontend needed format.
+ * One key per week day, wich contains an array of {
+ * 'HourOpen' => hh, 'MinutesOpen' => mm,
+ * 'HourClose' => hh, 'MinutesClose' => mm }
+ *
+ * @param array $openingtime The opening time in the db saved format.
+ * @return mixed The opening time in the frontend needed format.
+ */
+ private static function formatOpeningtime($openingtime)
+ {
+ $result = array();
+ foreach ($openingtime as $entry) {
+ $openTime = explode(':', $entry['openingtime']);
+ $closeTime = explode(':', $entry['closingtime']);
+ if (count($openTime) !== 2 || count($closeTime) !== 2)
+ continue;
+ $convertedTime = array(
+ 'HourOpen' => $openTime[0],
+ 'MinutesOpen' => $openTime[1],
+ 'HourClose' => $closeTime[0],
+ 'MinutesClose' => $closeTime[1],
+ );
+ foreach ($entry['days'] as $day) {
+ if (!isset($result[$day])) {
+ $result[$day] = array();
+ }
+ $result[$day][] = $convertedTime;
+ }
+ }
+ return $result;
+ }
+
+} \ No newline at end of file
diff --git a/modules-available/locationinfo/inc/locationinfo.inc.php b/modules-available/locationinfo/inc/locationinfo.inc.php
new file mode 100644
index 00000000..a5feb9ed
--- /dev/null
+++ b/modules-available/locationinfo/inc/locationinfo.inc.php
@@ -0,0 +1,109 @@
+<?php
+
+class LocationInfo
+{
+
+ /**
+ * Gets the pc data and returns it's state.
+ *
+ * @param array $pc The pc data from the db. Array('logintime' =>, 'lastseen' =>, 'lastboot' =>)
+ * @return int pc state
+ */
+ public static function getPcState($pc)
+ {
+ $logintime = (int)$pc['logintime'];
+ $lastseen = (int)$pc['lastseen'];
+ $lastboot = (int)$pc['lastboot'];
+ $NOW = time();
+
+ if ($NOW - $lastseen > 14 * 86400) {
+ return "BROKEN";
+ } elseif (($NOW - $lastseen > 610) || $lastboot === 0) {
+ return "OFF";
+ } elseif ($logintime === 0) {
+ return "IDLE";
+ } elseif ($logintime > 0) {
+ return "OCCUPIED";
+ }
+ return -1;
+ }
+
+ /**
+ * Set current error message of given server. Pass null or false to clear.
+ *
+ * @param int $serverId id of server
+ * @param string $message error message to set, null or false clears error.
+ */
+ public static function setServerError($serverId, $message)
+ {
+ if ($message === false || $message === null) {
+ Database::exec("UPDATE `locationinfo_coursebackend` SET error = NULL
+ WHERE serverid = :id", array('id' => $serverId));
+ } else {
+ if (empty($message)) {
+ $message = '<empty error message>';
+ }
+ $error = json_encode(array(
+ 'timestamp' => time(),
+ 'error' => (string)$message
+ ));
+ Database::exec("UPDATE `locationinfo_coursebackend` SET error = :error
+ WHERE serverid = :id", array('id' => $serverId, 'error' => $error));
+ }
+ }
+
+ /**
+ * Creates and returns a default config for room that didn't saved a config yet.
+ *
+ * @return array Return a default config.
+ */
+ public static function defaultPanelConfig($type)
+ {
+ return array(
+ 'language' => 'en',
+ 'mode' => 1,
+ 'vertical' => false,
+ 'eco' => false,
+ 'prettytime' => true,
+ 'scaledaysauto' => true,
+ 'daystoshow' => 7,
+ 'rotation' => 0,
+ 'scale' => 50,
+ 'switchtime' => 20,
+ 'calupdate' => 30,
+ 'roomupdate' => 15,
+ 'configupdate' => 180,
+ );
+ }
+
+ /**
+ * @param string $uuid panel uuid
+ * @return bool|string panel name if exists, false otherwise
+ */
+ public static function getPanelName($uuid)
+ {
+ $ret = Database::queryFirst('SELECT panelname FROM locationinfo_panel WHERE paneluuid = :uuid', compact('uuid'));
+ if ($ret === false) return false;
+ return $ret['panelname'];
+ }
+
+ public static function configHook($machineUuid, $panelUuid)
+ {
+ $row = Database::queryFirst('SELECT paneltype, panelconfig FROM locationinfo_panel WHERE paneluuid = :uuid',
+ array('uuid' => $panelUuid));
+ if ($row === false) {
+ // TODO: Invalid panel - what should we do?
+ } elseif ($row['paneltype'] === 'URL') {
+ // Check if we should set the insecure SSL mode (accept invalid/self signed certs etc.)
+ $data = json_decode($row['panelconfig'], true);
+ if ($data && $data['insecure-ssl']) {
+ ConfigHolder::add('SLX_BROWSER_INSECURE', '1');
+ }
+ }
+ ConfigHolder::add('SLX_BROWSER_URL', 'http://' . $_SERVER['SERVER_ADDR'] . '/panel/' . $panelUuid);
+ ConfigHolder::add('SLX_ADDONS', '', 1000);
+ ConfigHolder::add('SLX_LOGOUT_TIMEOUT', '', 1000);
+ ConfigHolder::add('SLX_SCREEN_STANDBY_TIMEOUT', '', 1000);
+ }
+
+}
diff --git a/modules-available/locationinfo/install.inc.php b/modules-available/locationinfo/install.inc.php
new file mode 100644
index 00000000..7c47ac90
--- /dev/null
+++ b/modules-available/locationinfo/install.inc.php
@@ -0,0 +1,88 @@
+<?php
+
+$res = array();
+
+$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,
+ `lastchange` int(10) UNSIGNED NOT NULL DEFAULT 0,
+ PRIMARY KEY (`locationid`)
+');
+
+$t2 = $res[] = tableCreate('locationinfo_coursebackend', '
+ `serverid` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `servername` VARCHAR(200) NOT NULL,
+ `servertype` VARCHAR(100) NOT NULL,
+ `credentials` BLOB,
+ `error` VARCHAR(500),
+ PRIMARY KEY (`serverid`),
+ KEY `servername` (`servername`)
+');
+
+$t3 = $res[] = tableCreate('locationinfo_panel', "
+ `paneluuid` char(36) CHARACTER SET ascii NOT NULL,
+ `panelname` varchar(30) NOT NULL,
+ `locationids` varchar(20) CHARACTER SET ascii NOT NULL,
+ `paneltype` enum('DEFAULT','SUMMARY', 'URL') NOT NULL,
+ `panelconfig` blob NOT NULL,
+ `lastchange` int(10) UNSIGNED NOT NULL DEFAULT 0,
+ PRIMARY KEY (`paneluuid`),
+ KEY `panelname` (`panelname`)
+");
+
+// Update
+
+if ($t1 === UPDATE_NOOP) {
+ if ($t3 === UPDATE_DONE) {
+ // Upgrade from old beta version - convert panels
+ 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');
+ }
+ Database::exec("ALTER TABLE locationinfo_locationconfig CHANGE `serverid` `serverid` INT(10) UNSIGNED NULL");
+ tableDropColumn('locationinfo_locationconfig', 'hidden');
+ tableDropColumn('locationinfo_locationconfig', 'config');
+ if (!tableHasColumn('locationinfo_locationconfig', 'lastchange')) {
+ $ret = Database::exec('ALTER TABLE locationinfo_locationconfig ADD `lastchange` INT(10) UNSIGNED NOT NULL DEFAULT 0');
+ if ($ret === false) {
+ finalResponse(UPDATE_FAILED, 'Could not add lastchange field');
+ } elseif ($ret > 0) {
+ $ret[] = UPDATE_DONE;
+ }
+ }
+}
+if ($t1 === UPDATE_DONE || $t2 === UPDATE_DONE) {
+ Database::exec('UPDATE locationinfo_locationconfig SET serverid = NULL WHERE serverid = 0');
+ Database::exec('ALTER TABLE `locationinfo_locationconfig` ADD CONSTRAINT `locationinfo_locationconfig_ibfk_1` FOREIGN KEY ( `serverid` )
+ REFERENCES `openslx`.`locationinfo_coursebackend` (`serverid`) ON DELETE SET NULL ON UPDATE CASCADE');
+}
+if ($t1 === UPDATE_DONE) {
+ if (false === Database::exec('ALTER TABLE `locationinfo_locationconfig` ADD CONSTRAINT `locationinfo_locationconfig_ibfk_2` FOREIGN KEY ( `locationid` )
+ REFERENCES `openslx`.`location` (`locationid`) ON DELETE CASCADE ON UPDATE CASCADE')) {
+ $res[] = UPDATE_RETRY;
+ }
+}
+
+if ($t3 === UPDATE_NOOP) {
+ Database::exec("ALTER TABLE `locationinfo_panel` CHANGE `paneltype`
+ `paneltype` ENUM('DEFAULT', 'SUMMARY', 'URL') CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL");
+}
+
+// 2017-07-26 Add servername key
+Database::exec("ALTER TABLE `locationinfo_coursebackend` ADD KEY `servername` (`servername`)");
+
+// Create response for browser
+
+if (in_array(UPDATE_RETRY, $res)) {
+ finalResponse(UPDATE_RETRY, 'Please retry: ' . Database::lastError());
+}
+if (in_array(UPDATE_DONE, $res)) {
+ finalResponse(UPDATE_DONE, 'Tables created successfully');
+}
+
+finalResponse(UPDATE_NOOP, 'Everything already up to date');
diff --git a/modules-available/locationinfo/lang/de/backend-davinci.json b/modules-available/locationinfo/lang/de/backend-davinci.json
new file mode 100644
index 00000000..edf155bf
--- /dev/null
+++ b/modules-available/locationinfo/lang/de/backend-davinci.json
@@ -0,0 +1,8 @@
+{
+ "baseUrl": "Basis-URL",
+ "baseUrl_helptext": "URL zur Davinci-Installation",
+ "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/backend-dummy.json b/modules-available/locationinfo/lang/de/backend-dummy.json
new file mode 100644
index 00000000..cd558860
--- /dev/null
+++ b/modules-available/locationinfo/lang/de/backend-dummy.json
@@ -0,0 +1,14 @@
+{
+ "CB2t": "DBzwo",
+ "CB2t_helptext": "CBzwo Help",
+ "CheckTheBox": "CheckBox",
+ "CheckTheBox_helptext": "Die checkbox ist ein wenig nutzlos",
+ "integer": "Zahl",
+ "integer_helptext": "Ein Zahlen felde?!",
+ "option": "Irgendein Array",
+ "option_helptext": "LALALA Hilfs- Text bla bla",
+ "password": "Passwort 1",
+ "password_helptext": "Bla passwort bla bla",
+ "username": "Benutzer",
+ "username_helptext": "Das ist halt ein Username feld.."
+} \ No newline at end of file
diff --git a/modules-available/locationinfo/lang/de/backend-hisinone.json b/modules-available/locationinfo/lang/de/backend-hisinone.json
new file mode 100644
index 00000000..6ea1a933
--- /dev/null
+++ b/modules-available/locationinfo/lang/de/backend-hisinone.json
@@ -0,0 +1,16 @@
+{
+ "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.",
+ "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
new file mode 100644
index 00000000..8da5bd33
--- /dev/null
+++ b/modules-available/locationinfo/lang/de/messages.json
@@ -0,0 +1,10 @@
+{
+ "auth-failed": "[{{0}}] {{1}} Error: {{2}}",
+ "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-panel-id": "Ung\u00fcltige Panel-ID '{{0}}'",
+ "invalid-server-id": "Ung\u00fcltige Server-ID '{{0}}'"
+} \ No newline at end of file
diff --git a/modules-available/locationinfo/lang/de/module.json b/modules-available/locationinfo/lang/de/module.json
index 166909c3..c344581c 100644
--- a/modules-available/locationinfo/lang/de/module.json
+++ b/modules-available/locationinfo/lang/de/module.json
@@ -1,4 +1,3 @@
{
- "module_name": "Mein erstes Modul",
- "page_title": "Mein erster Seitentitel"
+ "module_name": "Infoscreen"
} \ 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 ce98ce38..1574d9e8 100644
--- a/modules-available/locationinfo/lang/de/template-tags.json
+++ b/modules-available/locationinfo/lang/de/template-tags.json
@@ -1,3 +1,112 @@
{
- "lang_hello": "Hallo"
+ "lang_addServer": "Server",
+ "lang_areYouSure": "Sind Sie sicher?",
+ "lang_autoScale": "Auto Tage",
+ "lang_autoscaleTooltip": "Berechnet anhand der Bildschirmbreite die optimale Anzahl an Tagen, die der Kalender anzeigt",
+ "lang_backend": "Backend",
+ "lang_backends": "Backends",
+ "lang_calendar": "Kalender",
+ "lang_calupdateTooltip": "Zeit nachdem der Kalender aktualisiert wird (in Minuten)",
+ "lang_checkConnection": "Verbindung pr\u00fcfen",
+ "lang_closingTime": "Schlie\u00dfungszeit",
+ "lang_config": "Einstellungen",
+ "lang_configupdateTooltip": "Zeit nach der die Einstellungen aktualisiert werden (in Minuten)",
+ "lang_createPanel": "Panel anlegen",
+ "lang_credentials": "Anmeldung",
+ "lang_day": "Tag",
+ "lang_daysToShow": "Tage",
+ "lang_daysToShowTooltip": "Legt die Anzahl an Tagen im Kalender fest, die angezeigt werden",
+ "lang_defaultPanel": "Standard-Panel",
+ "lang_deleteConfirmation": "Sind Sie sicher?",
+ "lang_display": "Anzeige",
+ "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_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_entryName": "Name",
+ "lang_error": "Fehler",
+ "lang_expertMode": "Expertenmodus",
+ "lang_fourLocsHint": "Hier k\u00f6nnen Sie bis zu vier Orte ausw\u00e4hlen, die in diesem Panel angezeigt werden.",
+ "lang_general": "Allgemein",
+ "lang_language": "Sprache",
+ "lang_languageTooltip": "Legt die Sprache der angezeigten Oberfl\u00e4che fest",
+ "lang_locationName": "Name",
+ "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_mode": "Modus",
+ "lang_mode1": "Kalender & Raum",
+ "lang_mode2": "Kalender",
+ "lang_mode3": "Raum",
+ "lang_mode4": "Wechselnd",
+ "lang_modeTooltip": "Die Anzeigemodi, welche das Frontend unterst\u00fctzt",
+ "lang_monTilFr": "Montag - Freitag",
+ "lang_nameTooltip": "Legt den Namen des Servers fest",
+ "lang_noLocationsWarning": "Sie haben keine Orte f\u00fcr dieses Panel ausgew\u00e4hlt",
+ "lang_noServer": "<Kein Server>",
+ "lang_openingTime": "\u00d6ffnungszeit",
+ "lang_openingtimes": "\u00d6ffnungszeiten",
+ "lang_panel": "Panel",
+ "lang_panelType": "Typ",
+ "lang_panels": "Panels",
+ "lang_panelsTable": "Panels verwalten",
+ "lang_panelsTableHints": "Hier sehen Sie alle erstellen Anzeigetafeln, die per Browser an beliebiger Stelle angezeigt werden k\u00f6nnen, z.B. als digitales T\u00fcrschild.",
+ "lang_pleaseSelect": "Bitte w\u00e4hlen\u2026",
+ "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_remoteSchedule": "Abruf Belegungsplan",
+ "lang_room": "Raum",
+ "lang_roomId": "Raum ID",
+ "lang_roomIdTooltip": "Die Raum ID, die der Server ben\u00f6tigt, um Kalenderdaten abzurufen",
+ "lang_roomupdateTooltip": "Zeit nach der die PCs aktualisiert werden (in Sekunden)",
+ "lang_rotation": "Rotation",
+ "lang_rotation0": "0\u00b0",
+ "lang_rotation1": "90\u00b0 \u27f2",
+ "lang_rotation2": "180\u00b0",
+ "lang_rotation3": "90\u00b0 \u27f3",
+ "lang_rotationTooltip": "Rotiert den angezeigten Raum",
+ "lang_saturday": "Samstag",
+ "lang_scale": "Kalenderbreite",
+ "lang_scaleTooltip": "[10-90] Legt die Kalenderbreite fest (in Prozent)",
+ "lang_sec": "sek",
+ "lang_server": "Server",
+ "lang_serverTable": "Backend-Server verwalten",
+ "lang_serverTableHints": "Liste aller definierten Backend-Server. Diese werden ben\u00f6tigt, um Belegungspl\u00e4ne f\u00fcr R\u00e4ume abzurufen.",
+ "lang_serverTooltip": "Legt fest, von welchem Backend-Server die Kalenderdaten bezogen werden",
+ "lang_serverType": "Typ",
+ "lang_shortFriday": "Fr",
+ "lang_shortMonday": "Mo",
+ "lang_shortSaturday": "Sa",
+ "lang_shortSunday": "So",
+ "lang_shortThursday": "Do",
+ "lang_shortTuesday": "Di",
+ "lang_shortWednesday": "Mi",
+ "lang_summaryPanel": "\u00dcbersichts-Panel",
+ "lang_sunday": "Sonntag",
+ "lang_switchTime": "Wechselintervall",
+ "lang_switchTimeTooltip": "[1-120] Legt die Zeit fest, die vergeht bis ein Wechsel erfolgt (in Sekunden)",
+ "lang_typeTooltip": "Legt fest um welchen Server-Typ es sich handelt",
+ "lang_updateRates": "Aktualisierungsintervall",
+ "lang_vertical": "Vertikaler Modus",
+ "lang_verticalTooltip": "Legt fest, ob Kalender und Raum \u00fcbereinander angezeigt werden sollen",
+ "lang_closed": "Geschlossen",
+ "lang_free": "Frei",
+ "lang_shortSun": "So",
+ "lang_shortMon": "Mo",
+ "lang_shortTue": "Di",
+ "lang_shortWed": "Mi",
+ "lang_shortThu": "Do",
+ "lang_shortFri": "Fr",
+ "lang_shortSat": "Sa",
+ "lang_longSun": "Sonntag",
+ "lang_longMon": "Montag",
+ "lang_longTue": "Dienstag",
+ "lang_longWed": "Mittwoch",
+ "lang_longThu": "Donnerstag",
+ "lang_longFri": "Freitag",
+ "lang_longSat": "Samstag",
+ "lang_to": "bis"
} \ No newline at end of file
diff --git a/modules-available/locationinfo/lang/en/backend-davinci.json b/modules-available/locationinfo/lang/en/backend-davinci.json
new file mode 100644
index 00000000..e85abebe
--- /dev/null
+++ b/modules-available/locationinfo/lang/en/backend-davinci.json
@@ -0,0 +1,8 @@
+{
+ "baseUrl": "Base URL",
+ "baseUrl_helptext": "URL to Davinci installation",
+ "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-dummy.json b/modules-available/locationinfo/lang/en/backend-dummy.json
new file mode 100644
index 00000000..07bf804c
--- /dev/null
+++ b/modules-available/locationinfo/lang/en/backend-dummy.json
@@ -0,0 +1,14 @@
+{
+ "CB2t": "sausages",
+ "CB2t_helptext": "cb2",
+ "CheckTheBox": "CheckBox",
+ "CheckTheBox_helptext": "Check this if you are bored",
+ "integer": "A integer value",
+ "integer_helptext": "What is this for?!",
+ "option": "Option Array",
+ "option_helptext": "LALALA OPtion title bla bla",
+ "password": "Password 1",
+ "password_helptext": "Password 1 title alalala :D",
+ "username": "Username",
+ "username_helptext": "A fkn username field"
+} \ 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
new file mode 100644
index 00000000..0ff12c18
--- /dev/null
+++ b/modules-available/locationinfo/lang/en/backend-hisinone.json
@@ -0,0 +1,16 @@
+{
+ "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).",
+ "verifyCert": "Verify certificate",
+ "verifyCert_helptext": "Wenn das Zertifikat abgelaufen ist, oder von keiner bekannten CA ausgestellt wurde, wird die Verbindung abgelehnt.",
+ "verifyHostname": "Verify host name",
+ "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/en/messages.json b/modules-available/locationinfo/lang/en/messages.json
new file mode 100644
index 00000000..6767bfcf
--- /dev/null
+++ b/modules-available/locationinfo/lang/en/messages.json
@@ -0,0 +1,10 @@
+{
+ "auth-failed": "[{{0}}] {{1}} Error: {{2}}",
+ "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-panel-id": "Invalid panel id '{{0}}'",
+ "invalid-server-id": "Invalid server id '{{0}}'"
+} \ No newline at end of file
diff --git a/modules-available/locationinfo/lang/en/module.json b/modules-available/locationinfo/lang/en/module.json
index b2bcbb0c..2fd14353 100644
--- a/modules-available/locationinfo/lang/en/module.json
+++ b/modules-available/locationinfo/lang/en/module.json
@@ -1,4 +1,3 @@
{
- "module_name": "My first module",
- "page_title": "My first page title"
-} \ No newline at end of file
+ "module_name": "Infoscreen"
+}
diff --git a/modules-available/locationinfo/lang/en/template-tags.json b/modules-available/locationinfo/lang/en/template-tags.json
index c30739e5..900839a9 100644
--- a/modules-available/locationinfo/lang/en/template-tags.json
+++ b/modules-available/locationinfo/lang/en/template-tags.json
@@ -1,3 +1,108 @@
{
- "lang_hello": "Hello"
+ "lang_defaultPanel": "Default panel",
+ "lang_addServer": "Server",
+ "lang_summaryPanel": "Summary panel",
+ "lang_areYouSure": "Are you sure?",
+ "lang_autoScale": "Auto Days",
+ "lang_autoscaleTooltip": "Calculates the optimum amount of days to show from the display width",
+ "lang_backend": "Backend",
+ "lang_backends": "Backends",
+ "lang_calendar": "Calendar",
+ "lang_calupdateTooltip": "Time the calender querys for updates (in minutes)",
+ "lang_checkConnection": "Check connection",
+ "lang_closingTime": "Closing time",
+ "lang_config": "Config",
+ "lang_configupdateTooltip": "Time interval the config gets updated (in minutes)",
+ "lang_createPanel": "Create panel",
+ "lang_credentials": "Login",
+ "lang_day": "Day",
+ "lang_daysToShow": "Days",
+ "lang_daysToShowTooltip": "Defines the amount of days to show in the calendar",
+ "lang_deleteConfirmation": "Are you sure?",
+ "lang_display": "Display",
+ "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_editPanel": "Edit panel",
+ "lang_entryName": "Name",
+ "lang_error": "Error",
+ "lang_expertMode": "Expert mode",
+ "lang_fourLocsHint": "You can pick up to four locations that will be shown in this panel.",
+ "lang_general": "General",
+ "lang_language": "Language",
+ "lang_languageTooltip": "The language the frontend uses",
+ "lang_locationName": "Name",
+ "lang_locationSettings": "Settings",
+ "lang_locations": "Locations",
+ "lang_locationsTable": "Rooms \/ Locations",
+ "lang_mode": "Mode",
+ "lang_mode1": "Calendar & Room",
+ "lang_mode2": "Calendar",
+ "lang_mode3": "Room",
+ "lang_mode4": "Switching",
+ "lang_modeTooltip": "The display modes the frontend supports",
+ "lang_monTilFr": "Monday - Friday",
+ "lang_nameTooltip": "Defines the name of the server",
+ "lang_noLocationsWarning": "Please select at least one location this panel should display",
+ "lang_noServer": "<no server>",
+ "lang_openingTime": "Opening time",
+ "lang_openingtimes": "Opening times",
+ "lang_panel": "Panel",
+ "lang_panelType": "Type",
+ "lang_panels": "Panels",
+ "lang_panelsTable": "Manage panels",
+ "lang_pleaseSelect": "Please select...",
+ "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_remoteSchedule": "Time table retrieval",
+ "lang_room": "Room",
+ "lang_roomId": "Room ID",
+ "lang_roomIdTooltip": "The ID of the room the server needs, for querying the calendar data",
+ "lang_roomupdateTooltip": "Time the PCs in the room gets updated (in seconds)",
+ "lang_rotation": "Rotation",
+ "lang_rotation0": "0\u00b0",
+ "lang_rotation1": "90\u00b0 \u27f2",
+ "lang_rotation2": "180\u00b0",
+ "lang_rotation3": "90\u00b0 \u27f3",
+ "lang_rotationTooltip": "Rotates the room",
+ "lang_saturday": "Saturday",
+ "lang_scale": "Calendar width",
+ "lang_scaleTooltip": "[10-90] Defines the calendar width (in percent)",
+ "lang_sec": "sec",
+ "lang_server": "Server",
+ "lang_serverTable": "Manage backend servers",
+ "lang_serverTooltip": "Defines from which server the room queries the calendar data",
+ "lang_serverType": "Type",
+ "lang_shortFriday": "Fri",
+ "lang_shortMonday": "Mon",
+ "lang_shortSaturday": "Sat",
+ "lang_shortSunday": "Sun",
+ "lang_shortThursday": "Thu",
+ "lang_shortTuesday": "Tue",
+ "lang_shortWednesday": "Wed",
+ "lang_sunday": "Sunday",
+ "lang_switchTime": "Switchtime",
+ "lang_switchTimeTooltip": "[1-120] Sets the time between switching (in seconds)",
+ "lang_typeTooltip": "Defines on which type of server you want to connect to",
+ "lang_updateRates": "Update rates",
+ "lang_vertical": "Vertical mode",
+ "lang_verticalTooltip": "Defines whether the room and calendar are shown above each other",
+ "lang_closed": "Closed",
+ "lang_free": "Free",
+ "lang_shortSun": "Sun",
+ "lang_shortMon": "Mon",
+ "lang_shortTue": "Tue",
+ "lang_shortWed": "Wed",
+ "lang_shortThu": "Thu",
+ "lang_shortFri": "Fri",
+ "lang_shortSat": "Sat",
+ "lang_longSun": "Sunday",
+ "lang_longMon": "Monday",
+ "lang_longTue": "Tuesday",
+ "lang_longWed": "Wednesday",
+ "lang_longThu": "Thursday",
+ "lang_longFri": "Friday",
+ "lang_longSat": "Saturday",
+ "lang_to": "to"
} \ No newline at end of file
diff --git a/modules-available/locationinfo/lang/pt/template-tags.json b/modules-available/locationinfo/lang/pt/template-tags.json
index e7981844..e5c2aba8 100644
--- a/modules-available/locationinfo/lang/pt/template-tags.json
+++ b/modules-available/locationinfo/lang/pt/template-tags.json
@@ -1,3 +1,21 @@
{
- "lang_hello": "Olá"
+ "lang_room": "Quarto",
+ "lang_closed": "Fechado",
+ "lang_free": "Livre",
+ "lang_shortSun": "Do",
+ "lang_shortMon": "Se",
+ "lang_shortTue": "Te",
+ "lang_shortWed": "Qu",
+ "lang_shortThu": "Qu",
+ "lang_shortFri": "Se",
+ "lang_shortSat": "Sá",
+ "lang_longSun": "Domingo",
+ "lang_longMon": "Segunda-feira",
+ "lang_longTue": "Terça-feira",
+ "lang_longWed": "Quarta-feira",
+ "lang_longThu": "Quinta-feira",
+ "lang_longFri": "Sexta-feira",
+ "lang_longSat": "Sábado",
+ "lang_to": "para"
+
} \ No newline at end of file
diff --git a/modules-available/locationinfo/page.inc.php b/modules-available/locationinfo/page.inc.php
index ff73107a..f8aa1c5b 100644
--- a/modules-available/locationinfo/page.inc.php
+++ b/modules-available/locationinfo/page.inc.php
@@ -3,17 +3,48 @@
class Page_LocationInfo extends Page
{
+ private $action;
+
/**
* Called before any page rendering happens - early hook to check parameters etc.
*/
protected function doPreprocess()
{
+ $show = Request::any('show', '', 'string');
+ if ($show === 'panel') {
+ $this->showPanel();
+ exit(0);
+ }
User::load();
-
if (!User::isLoggedIn()) {
Message::addError('main.no-permission');
Util::redirect('?do=Main'); // does not return
}
+ $this->action = Request::post('action');
+ if ($this->action === 'writePanelConfig') {
+ $this->writePanelConfig();
+ } elseif ($this->action === 'writeLocationConfig') {
+ $this->writeLocationConfig();
+ $show = 'locations';
+ } elseif ($this->action === 'deleteServer') {
+ $this->deleteServer();
+ } elseif ($this->action === 'deletePanel') {
+ $this->deletePanel();
+ } elseif ($this->action === 'checkConnection') {
+ $this->checkConnection(Request::post('serverid', 0, 'int'));
+ $show = 'backends';
+ } elseif ($this->action === 'updateServerSettings') {
+ $this->updateServerSettings();
+ $show = 'backends';
+ } elseif (Request::isPost()) {
+ Message::addWarning('main.invalid-action', $this->action);
+ }
+ if (Request::isPost()) {
+ if (!empty($show)) {
+ $show = '&show=' . $show;
+ }
+ Util::redirect('?do=locationinfo' . $show);
+ }
}
/**
@@ -21,10 +52,829 @@ class Page_LocationInfo extends Page
*/
protected function doRender()
{
- Render::addTemplate('_page', array(
- 'foo' => 'bar',
- 'now' => date('d.m.Y H:i:s')
+ // Do this here so we always see backend errors
+ $backends = $this->loadBackends();
+ $show = Request::get('show', '', 'string');
+ Render::addTemplate('page-tabs', array('class-' . $show => 'active'));
+ switch ($show) {
+ case 'locations':
+ $this->showLocationsTable();
+ break;
+ case 'backends':
+ $this->showBackendsTable($backends);
+ break;
+ case 'edit-panel':
+ $this->showPanelConfig();
+ break;
+ case '':
+ $this->showPanelsTable();
+ break;
+ default:
+ Util::redirect('?do=locationinfo');
+ }
+ }
+
+ /**
+ * Deletes the server from the db.
+ */
+ private function deleteServer()
+ {
+ $id = Request::post('serverid', false, 'int');
+ if ($id === false) {
+ Message::addError('server-id-missing');
+ return;
+ }
+ $res = Database::exec("DELETE FROM `locationinfo_coursebackend` WHERE serverid=:id", array('id' => $id));
+ if ($res !== 1) {
+ Message::addWarning('invalid-server-id', $id);
+ }
+ }
+
+ private function deletePanel()
+ {
+ $id = Request::post('uuid', false, 'string');
+ if ($id === false) {
+ Message::addError('main.parameter-missing', 'uuid');
+ return;
+ }
+ $res = Database::exec("DELETE FROM `locationinfo_panel` WHERE paneluuid = :id", array('id' => $id));
+ if ($res !== 1) {
+ Message::addWarning('invalid-panel-id', $id);
+ }
+ }
+
+ private function getTime($str)
+ {
+ $str = explode(':', $str);
+ if (count($str) !== 2) return false;
+ if ($str[0] < 0 || $str[0] > 23 || $str[1] < 0 || $str[1] > 59) return false;
+ return $str[0] * 60 + $str[1];
+ }
+
+ private function writeLocationConfig()
+ {
+ // Check locations
+ $locationid = Request::post('locationid', false, 'int');
+ if ($locationid === false) {
+ Message::addError('main.parameter-missing', 'locationid');
+ return false;
+ }
+ if (Location::get($locationid) === false) {
+ Message::addError('location.invalid-location-id', $locationid);
+ return false;
+ }
+ $serverid = Request::post('serverid', 0, 'int');
+ if ($serverid === 0) {
+ $serverid = null;
+ }
+ $serverlocationid = Request::post('serverlocationid', '', 'string');
+
+ $recursive = (Request::post('recursive', '', 'string') !== '');
+ if (empty($serverlocationid) && !$recursive) {
+ $insertServerId = null;
+ $ignoreServer = 1;
+ } else {
+ $insertServerId = $serverid;
+ $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 = '';
+ } else {
+ $openingtimes = json_encode($mangled);
+ }
+ }
+ }
+
+ Database::exec("INSERT INTO `locationinfo_locationconfig` (locationid, serverid, serverlocationid, openingtime, lastcalendarupdate, lastchange)
+ VALUES (:id, :insertserverid, :serverlocationid, :openingtimes, 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(
+ 'id' => $locationid,
+ 'insertserverid' => $insertServerId,
+ 'serverid' => $serverid,
+ 'openingtimes' => $openingtimes,
+ 'serverlocationid' => $serverlocationid,
+ 'ignore_server' => $ignoreServer,
+ 'now' => time(),
));
+
+ if (!$recursive)
+ return true;
+
+ // Recursive overwriting of serverid
+ $children = Location::getRecursiveFlat($locationid);
+ $array = array();
+ foreach ($children as $loc) {
+ $array[] = $loc['locationid'];
+ }
+ if (!empty($array)) {
+ Database::exec("UPDATE locationinfo_locationconfig
+ SET serverid = :serverid, lastcalendarupdate = IF(serverid <> :serverid, 0, lastcalendarupdate), lastchange = :now
+ WHERE locationid IN (:locations)", array(
+ 'serverid' => $serverid,
+ 'locations' => $array,
+ 'now' => time(),
+ ));
+ }
+ 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
+ * @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)
+ {
+ $locationids = Request::post('locationids', false, 'string');
+ if ($locationids === false) {
+ if (!$failIfEmpty)
+ return array();
+ Message::addError('main.paramter-missing', 'locationids');
+ Util::redirect('?do=locationinfo');
+ }
+ $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)) {
+ Message::addError('main.paramter-empty', 'locationids');
+ Util::redirect('?do=locationinfo');
+ }
+ return $locationids;
+ }
+
+ /**
+ * Updated the config in the db.
+ */
+ private function writePanelConfig()
+ {
+ // UUID - existing or new
+ $paneluuid = Request::post('uuid', false, 'string');
+ if (($paneluuid === false || strlen($paneluuid) !== 36) && $paneluuid !== 'new') {
+ Message::addError('invalid-panel-id', $paneluuid);
+ Util::redirect('?do=locationinfo');
+ }
+ // Check panel type
+ $paneltype = Request::post('ptype', false, 'string');
+
+ if ($paneltype === 'DEFAULT') {
+ $params = $this->preparePanelConfigDefault();
+ } elseif ($paneltype === 'URL') {
+ $params = $this->preparePanelConfigUrl();
+ } else {
+ Message::addError('invalid-panel-type', $paneltype);
+ Util::redirect('?do=locationinfo');
+ }
+
+ if ($paneluuid === 'new') {
+ $paneluuid = Util::randomUuid();
+ $query = "INSERT INTO `locationinfo_panel` (paneluuid, panelname, locationids, paneltype, panelconfig, lastchange)
+ VALUES (:id, :name, :locationids, :type, :config, :now)";
+ } else {
+ $query = "UPDATE `locationinfo_panel`
+ SET panelname = :name, locationids = :locationids, paneltype = :type, panelconfig = :config, lastchange = :now
+ WHERE paneluuid = :id";
+ }
+ $params['id'] = $paneluuid;
+ $params['name'] = Request::post('name', '-', 'string');
+ $params['type'] = $paneltype;
+ $params['now'] = time();
+ $params['config'] = json_encode($params['config']);
+ $params['locationids'] = implode(',', $params['locationids']);
+ Database::exec($query, $params);
+
+ Message::addSuccess('config-saved');
+ Util::redirect('?do=locationinfo');
+ }
+
+ private function preparePanelConfigDefault()
+ {
+ // Check locations
+ $locationids = self::getLocationIdsFromRequest(true);
+ if (count($locationids) > 4) {
+ $locationids = array_slice($locationids, 0, 4);
+ }
+ // Build json struct
+ $conf = array(
+ 'language' => Request::post('language', 'en', 'string'),
+ 'mode' => Request::post('mode', 1, 'int'),
+ 'vertical' => Request::post('vertical', false, 'bool'),
+ 'eco' => Request::post('eco', false, 'bool'),
+ 'prettytime' => Request::post('prettytime', false, 'bool'),
+ 'scaledaysauto' => Request::post('scaledaysauto', false, 'bool'),
+ 'daystoshow' => Request::post('daystoshow', 7, 'int'),
+ 'rotation' => Request::post('rotation', 0, 'int'),
+ 'scale' => Request::post('scale', 50, 'int'),
+ 'switchtime' => Request::post('switchtime', 20, 'int'),
+ 'calupdate' => Request::post('calupdate', 120, 'int'),
+ 'roomupdate' => Request::post('roomupdate', 30, 'int'),
+ );
+ if ($conf['roomupdate'] < 15) {
+ $conf['roomupdate'] = 15;
+ }
+ if ($conf['calupdate'] < 30) {
+ $conf['calupdate'] = 30;
+ }
+ return array('config' => $conf, 'locationids' => $locationids);
+ }
+
+ private function preparePanelConfigUrl()
+ {
+ $conf = array(
+ 'url' => Request::post('url', 'https://www.bwlehrpool.de/', 'string'),
+ 'insecure-ssl' => Request::post('insecure-ssl', 0, 'int'),
+ );
+ return array('config' => $conf, 'locationids' => []);
+ }
+
+ /**
+ * Updates the server settings in the db.
+ */
+ private function updateServerSettings()
+ {
+ $serverid = Request::post('id', -1, 'int');
+ $servername = Request::post('name', 'unnamed', 'string');
+ $servertype = Request::post('type', '', 'string');
+ $backend = CourseBackend::getInstance($servertype);
+
+ if ($backend === false) {
+ Messages::addError('invalid-backend-type', $servertype);
+ Util::redirect('?do=locationinfo');
+ }
+
+ $tmptypeArray = $backend->getCredentialDefinitions();
+
+ $credentialsJson = array();
+ foreach ($tmptypeArray as $cred) {
+ $credentialsJson[$cred->property] = Request::post('prop-' . $cred->property);
+ }
+ $params = array(
+ 'name' => $servername,
+ 'type' => $servertype,
+ 'credentials' => json_encode($credentialsJson)
+ );
+ if ($serverid === 0) {
+ Database::exec('INSERT INTO `locationinfo_coursebackend` (servername, servertype, credentials)
+ VALUES (:name, :type, :credentials)', $params);
+ $this->checkConnection(Database::lastInsertId());
+ } else {
+ $params['id'] = $serverid;
+ Database::exec('UPDATE `locationinfo_coursebackend`
+ SET servername = :name, servertype = :type, credentials = :credentials
+ WHERE serverid = :id', $params);
+ $this->checkConnection($serverid);
+ }
+ }
+
+ /**
+ * Checks if the server connection to a backend is valid.
+ *
+ * @param int $id Server id which connection should be checked.
+ */
+ private function checkConnection($serverid = 0)
+ {
+ if ($serverid === 0) {
+ Util::traceError('checkConnection called with no server id');
+ }
+
+ $dbresult = Database::queryFirst("SELECT servertype, credentials
+ FROM `locationinfo_coursebackend`
+ WHERE serverid = :serverid", array('serverid' => $serverid));
+
+ $serverInstance = CourseBackend::getInstance($dbresult['servertype']);
+ if ($serverInstance === false) {
+ LocationInfo::setServerError($serverid, 'Unknown backend type: ' . $dbresult['servertype']);
+ return;
+ }
+ $credentialsOk = $serverInstance->setCredentials($serverid, json_decode($dbresult['credentials'], true));
+
+ if ($credentialsOk) {
+ $connectionOk = $serverInstance->checkConnection();
+ }
+
+ LocationInfo::setServerError($serverid, $serverInstance->getError());
+ }
+
+ private function loadBackends()
+ {
+ // Get a list of all the backend types.
+ $servertypes = array();
+ $s_list = CourseBackend::getList();
+ foreach ($s_list as $s) {
+ $typeInstance = CourseBackend::getInstance($s);
+ $servertypes[$s] = $typeInstance->getDisplayName();
+ }
+ // 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)) {
+ if (isset($servertypes[$row['servertype']])) {
+ $row['typename'] = $servertypes[$row['servertype']];
+ } else {
+ $row['typename'] = '[' . $row['servertype'] . ']';
+ $row['disabled'] = 'disabled';
+ }
+
+ if (!empty($row['error'])) {
+ $row['autherror'] = true;
+ $error = json_decode($row['error'], true);
+ if (isset($error['timestamp'])) {
+ $time = date('Y/m/d H:i:s', $error['timestamp']);
+ } else {
+ $time = '???';
+ }
+ Message::addError('auth-failed', $row['servername'], $time, $error['error']);
+ }
+ $serverlist[] = $row;
+ }
+ return $serverlist;
+ }
+
+ /**
+ * Show the list of backends
+ */
+ private function showBackendsTable($serverlist)
+ {
+ // Pass the data to the html and render it.
+ Render::addTemplate('page-servers', array(
+ 'serverlist' => $serverlist,
+ ));
+ }
+
+ private function showLocationsTable()
+ {
+ $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.servername
+ FROM `locationinfo_locationconfig` AS li
+ LEFT JOIN `locationinfo_coursebackend` AS cb USING (serverid)");
+
+ while ($row = $dbquery->fetch(PDO::FETCH_ASSOC)) {
+ $locid = (int)$row['locationid'];
+ if (!isset($locations[$locid]))
+ continue;
+ $glyph = !empty($row['openingtime']) ? 'ok' : '';
+ $backend = '';
+ if (!empty($row['serverid']) && !empty($row['serverlocationid'])) {
+ $backend = $row['servername'] . '(' . $row['serverlocationid'] . ')';
+ }
+ $locations[$locid] += array(
+ 'openingGlyph' => $glyph,
+ 'backend' => $backend,
+ 'lastCalendarUpdate' => $row['lastcalendarupdate'], // TODO
+ );
+ }
+
+ $stack = array();
+ $depth = -1;
+ foreach ($locations as &$location) {
+ while ($location['depth'] <= $depth) {
+ array_pop($stack);
+ $depth--;
+ }
+ while ($location['depth'] > $depth) {
+ $depth++;
+ array_push($stack, empty($location['openingGlyph']) ? '' : 'arrow-up');
+ }
+ if ($depth > 0 && empty($location['openingGlyph'])) {
+ $location['openingGlyph'] = $stack[$depth - 1];
+ }
+ }
+
+ Render::addTemplate('page-locations', array(
+ 'list' => array_values($locations),
+ ));
+ }
+
+ private function showPanelsTable()
+ {
+ $res = Database::simpleQuery('SELECT p.paneluuid, p.panelname, p.locationids, p.panelconfig,
+ p.paneltype FROM locationinfo_panel p
+ ORDER BY panelname ASC');
+ $hasRunmode = Module::isAvailable('runmode');
+ if ($hasRunmode) {
+ $runmodes = RunMode::getForModule(Page::getModule(), true);
+ }
+ $panels = array();
+ $locations = Location::getLocationsAssoc();
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ if ($row['paneltype'] === 'URL') {
+ $url = json_decode($row['panelconfig'], true)['url'];
+ $row['locations'] = $row['locationurl'] = $url;
+ } else {
+ $lids = explode(',', $row['locationids']);
+ $locs = array_map(function ($id) use ($locations) {
+ return isset($locations[$id]) ? $locations[$id]['locationname'] : $id;
+ }, $lids);
+ $row['locations'] = implode(', ', $locs);
+ }
+ $len = mb_strlen($row['panelname']);
+ if ($len < 5) {
+ $row['panelname'] .= str_repeat('…', 5 - $len);
+ }
+ if ($hasRunmode && isset($runmodes[$row['paneluuid']])) {
+ $row['assignedMachineCount'] = count($runmodes[$row['paneluuid']]);
+ }
+ $panels[] = $row;
+ }
+ Render::addTemplate('page-panels', compact('panels', 'hasRunmode'));
+ }
+
+ /**
+ * AJAX
+ */
+ protected function doAjax()
+ {
+ User::load();
+ if (!User::isLoggedIn()) {
+ die('Unauthorized');
+ }
+ $action = Request::any('action');
+ $id = Request::any('id', 0, 'int');
+ if ($action === 'config-location') {
+ $this->ajaxConfigLocation($id);
+ } elseif ($action === 'serverSettings') {
+ $this->ajaxServerSettings($id);
+ }
+ }
+
+ /**
+ * Ajax the server settings.
+ *
+ * @param int $id Serverid
+ */
+ private function ajaxServerSettings($id)
+ {
+ $oldConfig = Database::queryFirst('SELECT servername, servertype, credentials
+ FROM `locationinfo_coursebackend` WHERE serverid = :id', array('id' => $id));
+
+ // Credentials stuff.
+ if ($oldConfig !== false) {
+ $oldCredentials = json_decode($oldConfig['credentials'], true);
+ } else {
+ $oldCredentials = array();
+ }
+
+ // Get a list of all the backend types.
+ $serverBackends = array();
+ $s_list = CourseBackend::getList();
+ foreach ($s_list as $s) {
+ $backendInstance = CourseBackend::getInstance($s);
+ $backend = array(
+ 'backendtype' => $s,
+ 'display' => $backendInstance->getDisplayName(),
+ 'active' => ($oldConfig !== false && $s === $oldConfig['servertype']),
+ );
+ $backend['credentials'] = $backendInstance->getCredentialDefinitions();
+ foreach ($backend['credentials'] as $cred) {
+ if ($backend['active'] && isset($oldCredentials[$cred->property])) {
+ $cred->initForRender($oldCredentials[$cred->property]);
+ } else {
+ $cred->initForRender();
+ }
+ $cred->title = Dictionary::translateFile('backend-' . $s, $cred->property, true);
+ $cred->helptext = Dictionary::translateFile('backend-' . $s, $cred->property . "_helptext");
+ $cred->credentialsHtml = Render::parse('server-prop-' . $cred->template, (array)$cred);
+ }
+ $serverBackends[] = $backend;
+ }
+ echo Render::parse('ajax-config-server', array('id' => $id,
+ 'name' => $oldConfig['servername'],
+ 'currentbackend' => $oldConfig['servertype'],
+ 'backendList' => $serverBackends,
+ 'defaultBlank' => $oldConfig === false));
+ }
+
+ /**
+ * Ajax the time table
+ *
+ * @param $id id of the location
+ */
+ private function ajaxConfigLocation($id)
+ {
+ $locConfig = Database::queryFirst("SELECT serverid, serverlocationid, openingtime FROM `locationinfo_locationconfig` WHERE locationid = :id", array('id' => $id));
+ if ($locConfig !== false) {
+ $openingtimes = json_decode($locConfig['openingtime'], true);
+ } else {
+ $locConfig = array('serverid' => null, 'serverlocationid' => '');
+ }
+ if (!isset($openingtimes) || !is_array($openingtimes)) {
+ $openingtimes = array();
+ }
+
+ // Preset serverid from parent if none is set
+ if (is_null($locConfig['serverid'])) {
+ $chain = Location::getLocationRootChain($id);
+ if (!empty($chain)) {
+ $res = Database::simpleQuery("SELECT serverid, locationid FROM locationinfo_locationconfig
+ 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)) {
+ if ($best === false || $chain[$row['locationid']] < $chain[$best['locationid']]) {
+ $best = $row;
+ }
+ }
+ if ($best !== false) {
+ $locConfig['serverid'] = $best['serverid'];
+ }
+ }
+ }
+
+ // 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)) {
+ if ($row['serverid'] == $locConfig['serverid']) {
+ $row['selected'] = 'selected';
+ }
+ $serverList[] = $row;
+ }
+
+ $data = array(
+ 'id' => $id,
+ 'serverlist' => $serverList,
+ 'serverlocationid' => $locConfig['serverlocationid'],
+ );
+ $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);
+ }
+
+ /**
+ * 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.
+ *
+ * @param array $array of the saved openingtimes.
+ * @return bool True if simple mode, false if expert mode
+ */
+ private function isSimpleMode(&$array)
+ {
+ if (empty($array))
+ return true;
+ // Decompose by day
+ $new = array();
+ foreach ($array as $row) {
+ $s = $this->getTime($row['openingtime']);
+ $e = $this->getTime($row['closingtime']);
+ if ($s === false || $e === false || $e <= $s)
+ continue;
+ foreach ($row['days'] as $day) {
+ $this->addDay($new, $day, $s, $e);
+ }
+ }
+ // Merge by timespan, but always keep saturday and sunday separate
+ $merged = array();
+ foreach ($new as $day => $ranges) {
+ foreach ($ranges as $range) {
+ if ($day === 'Saturday' || $day === 'Sunday') {
+ $add = $day;
+ } else {
+ $add = '';
+ }
+ $key = '#' . $range[0] . '#' . $range[1] . '#' . $add;
+ if (!isset($merged[$key])) {
+ $merged[$key] = array();
+ }
+ $merged[$key][$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;
+ }
+ } else {
+ return false;
+ }
+ }
+ // 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;
+ }
+
+ private function addDay(&$array, $day, $s, $e)
+ {
+ if (!isset($array[$day])) {
+ $array[$day] = array(array($s, $e));
+ return;
+ }
+ foreach (array_keys($array[$day]) as $key) {
+ $current = $array[$day][$key];
+ if ($s <= $current[0] && $e >= $current[1]) {
+ // Fully dominated
+ unset($array[$day][$key]);
+ continue; // Might partially overlap with additional ranges, keep going
+ }
+ if ($current[0] <= $s && $current[1] >= $s) {
+ // $start lies within existing range
+ if ($current[0] <= $e && $current[1] >= $e)
+ return; // Fully in existing range, do nothing
+ // $end seems to extend range we're checking against but $start lies within this range, update and keep going
+ $s = $current[0];
+ unset($array[$day][$key]);
+ continue;
+ }
+ // Last possibility: $start is before range, $end within range
+ if ($current[0] <= $e && $current[1] >= $e) {
+ // $start must lie before range start, otherwise we'd have hit the case above
+ $e = $current[1];
+ unset($array[$day][$key]);
+ continue;
+ }
+ }
+ $array[$day][] = array($s, $e);
+ }
+
+ /**
+ * Ajax the config of a panel.
+ *
+ * @param $id Location ID
+ */
+ private function showPanelConfig()
+ {
+ $id = Request::get('uuid', false, 'string');
+ if ($id === false) {
+ Message::addError('main.parameter-missing', 'uuid');
+ return;
+ }
+ $config = false;
+ if ($id === 'new-default') {
+ // Creating new panel
+ $panel = array(
+ 'panelname' => '',
+ 'locationids' => '',
+ 'paneltype' => 'DEFAULT',
+ );
+ $id = 'new';
+ } elseif ($id === 'new-summary') {
+ // Creating new panel
+ $panel = array(
+ 'panelname' => '',
+ 'locationids' => '',
+ 'paneltype' => 'SUMMARY',
+ );
+ $id = 'new';
+ } elseif ($id === 'new-url') {
+ // Creating new panel
+ $panel = array(
+ 'panelname' => '',
+ 'paneltype' => 'URL',
+ );
+ $id = 'new';
+ } else {
+ // Get Config data from db
+ $panel = Database::queryFirst("SELECT panelname, locationids, paneltype, panelconfig
+ FROM locationinfo_panel
+ WHERE paneluuid = :id", array('id' => $id));
+ if ($panel === false) {
+ Message::addError('invalid-panel-id', $id);
+ return;
+ }
+
+ $config = json_decode($panel['panelconfig'], true);
+ }
+
+ $def = LocationInfo::defaultPanelConfig($panel['paneltype']);
+ if (!is_array($config)) {
+ $config = $def;
+ } else {
+ $config += $def;
+ }
+
+ $langs = Dictionary::getLanguages(true);
+ foreach ($langs as &$lang) {
+ if ($lang['cc'] === $config['language']) {
+ $lang['selected'] = 'selected';
+ }
+ }
+
+ if ($panel['paneltype'] === 'DEFAULT') {
+ Render::addTemplate('page-config-panel-default', array(
+ 'new' => $id === 'new',
+ 'uuid' => $id,
+ 'panelname' => $panel['panelname'],
+ 'languages' => $langs,
+ 'mode' => $config['mode'],
+ 'vertical_checked' => $config['vertical'] ? 'checked' : '',
+ 'eco_checked' => $config['eco'] ? 'checked' : '',
+ 'prettytime_checked' => $config['prettytime'] ? 'checked' : '',
+ 'scaledaysauto_checked' => $config['scaledaysauto'] ? 'checked' : '',
+ 'daystoshow' => $config['daystoshow'],
+ 'rotation' => $config['rotation'],
+ 'scale' => $config['scale'],
+ 'switchtime' => $config['switchtime'],
+ 'calupdate' => $config['calupdate'],
+ 'roomupdate' => $config['roomupdate'],
+ 'locations' => Location::getLocations(),
+ 'locationids' => $panel['locationids'],
+ ));
+ } elseif ($panel['paneltype'] === 'URL') {
+ Render::addTemplate('page-config-panel-url', array(
+ 'new' => $id === 'new',
+ 'uuid' => $id,
+ 'panelname' => $panel['panelname'],
+ 'url' => $config['url'],
+ 'ssl_checked' => $config['insecure-ssl'] ? 'checked' : '',
+ ));
+ } else { // TODO
+ Render::addTemplate('page-config-panel-summary', array(
+ 'new' => $id === 'new',
+ 'uuid' => $id,
+ 'panelname' => $panel['panelname'],
+ 'languages' => $langs,
+ 'roomupdate' => $config['roomupdate'],
+ 'locations' => Location::getLocations(),
+ 'locationids' => $panel['locationids'],
+ ));
+ }
+ }
+
+ private function showPanel()
+ {
+ $uuid = Request::get('uuid', false, 'string');
+ if ($uuid === false) {
+ http_response_code(400);
+ die('Missing parameter uuid');
+ }
+ $type = InfoPanel::getConfig($uuid, $config);
+ if ($type === false) {
+ http_response_code(404);
+ die('Panel with given uuid not found');
+ }
+
+ if ($type === 'URL') {
+ Util::redirect($config['url']);
+ }
+
+ $data = array(
+ 'uuid' => $uuid,
+ 'config' => json_encode($config),
+ 'language' => $config['language'],
+ );
+
+ preg_match('#^(.*)/#', $_SERVER['PHP_SELF'], $script);
+ preg_match('#^([^?]+)/#', $_SERVER['REQUEST_URI'], $request);
+ if ($script[1] !== $request[1]) {
+ $data['dirprefix'] = $script[1] . '/';
+ }
+
+ echo Render::parse('frontend-default', $data);
}
}
diff --git a/modules-available/locationinfo/templates/_page.html b/modules-available/locationinfo/templates/_page.html
deleted file mode 100644
index dfc941ae..00000000
--- a/modules-available/locationinfo/templates/_page.html
+++ /dev/null
@@ -1,4 +0,0 @@
-<div style="border:5px solid red">
- <h1>{{lang_hello}}, {{foo}}</h1>
- ** {{now}} **
-</div> \ No newline at end of file
diff --git a/modules-available/locationinfo/templates/ajax-config-location.html b/modules-available/locationinfo/templates/ajax-config-location.html
new file mode 100644
index 00000000..b42ff98d
--- /dev/null
+++ b/modules-available/locationinfo/templates/ajax-config-location.html
@@ -0,0 +1,192 @@
+<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>
+</div>
+
+<h3>{{lang_remoteSchedule}}</h3>
+<div class="row">
+ <div class="col-sm-3">
+ {{lang_server}}
+ </div>
+ <div class="col-sm-7">
+ <select class="form-control" name="serverid">
+ <option value="0">{{lang_noServer}}</option>
+ {{#serverlist}}
+ <option value="{{serverid}}" {{selected}}>{{servername}}</option>
+ {{/serverlist}}
+ </select>
+ </div>
+ <div class="col-sm-2">
+ <a class="btn btn-default helptext" title="{{lang_serverTooltip}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ </div>
+</div>
+<div class="row">
+ <div class="col-sm-3"></div>
+ <div class="col-sm-7">
+ <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">
+ <a class="btn btn-default helptext" title="{{lang_recursiveSetTooltip}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ </div>
+</div>
+<div class="row">
+ <div class="col-sm-3">
+ {{lang_roomId}}
+ </div>
+ <div class="col-sm-7">
+ <input class="form-control" name="serverlocationid" id="serverlocationid" value="{{serverlocationid}}">
+ </div>
+ <div class="col-sm-2">
+ <a class="btn btn-default helptext" title="{{lang_roomIdTooltip}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ </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'));
+
+ $('a.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
new file mode 100644
index 00000000..940bc55a
--- /dev/null
+++ b/modules-available/locationinfo/templates/ajax-config-server.html
@@ -0,0 +1,99 @@
+<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">
+ <a class="btn btn-default" title="{{lang_nameTooltip}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ </div>
+ </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">
+ <a class="btn btn-default" id="help-type" title="{{lang_typeTooltip}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+
+{{#backendList}}
+<div id="formwrapper-{{backendtype}}" {{^active}}class="collapse"{{/active}}>
+ <form method="post" action="?do=locationinfo" id="form-{{backendtype}}">
+ <input type="hidden" name="token" value="{{token}}">
+ <input type="hidden" name="action" value="updateServerSettings">
+ <input type="hidden" name="id" value="{{id}}">
+ <input type="hidden" name="type" value="{{backendtype}}">
+ <input type="text" name="prevent_autofill" id="prevent_autofill" value="" style="position:absolute;top:-2000px" tabindex="-1">
+ <input type="password" name="password_fake" id="password_fake" value="" style="position:absolute;top:-2000px" tabindex="-1">
+
+
+ <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>
+ </div>
+ </form>
+</div>
+{{/backendList}}
+<script type="text/javascript">
+ var currentBackend = "{{currentbackend}}";
+
+ /**
+ * Show proper form after switching backend type
+ *
+ * @param {string} value The new currentBackend of the server which credentials needs to be loaded.
+ */
+ function servertype_changed(value) {
+ if (value === currentBackend)
+ return;
+ var newBackend = value;
+ var fadeIn = function() {
+ $('#formwrapper-' + newBackend).fadeIn('fast');
+ $('#myModalSubmitButton, #name-input').attr('form', 'form-' + newBackend);
+ };
+ if (currentBackend) {
+ $('#formwrapper-' + currentBackend).fadeOut('fast', fadeIn);
+ } else {
+ fadeIn();
+ }
+ currentBackend = value;
+ }
+
+ $('a.btn[title]').tooltip();
+ $('#myModalSubmitButton').attr('form', 'form-' + currentBackend);
+ $('.settings-bs-switch').bootstrapSwitch({size:'small'});
+
+</script>
diff --git a/modules-available/locationinfo/templates/frontend-default.html b/modules-available/locationinfo/templates/frontend-default.html
new file mode 100755
index 00000000..fc9c3eac
--- /dev/null
+++ b/modules-available/locationinfo/templates/frontend-default.html
@@ -0,0 +1,1763 @@
+<!DOCTYPE html>
+<!--
+
+parameter
+
+required:
+ uuid: [integer] panel id, see in admin panel
+
+optional:
+ mode:[1,2,3,4] sets the displaying
+ 1: Calendar & Room
+ 2: only Calendar
+ 3: only Room
+ 4: Calendar & Room alternately
+ daystoshow:[1,2,3,4,5,6,7] sets how many days the calendar shows
+ scale:[10-90] scales the calendar and Roomplan in mode 1
+ switchtime:[1-120] sets the time between switchen in mode 4 (in seconds)
+ calupdate: Time the calender querys for updates,in minutes.
+ roomupdate: Time the PCs in the room gets updated,in seconds.
+ rotation:[0-3] rotation of the roomplan
+ vertical:[true] only mode 1, sets the calendar above the roomplan
+ scaledaysauto: [true] if true it finds automatically the daystoshow parameter depending on display size
+
+-->
+<html lang="{{language}}">
+<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'/>
+
+ <style type='text/css'>
+
+ body {
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ height: 100%;
+ background-color: #cacaca;
+ overflow: hidden;
+ position: absolute;
+ display: table;
+ }
+
+ body, .wc-container {
+ font-family: "Lucida Grande", Helvetica, Arial, Verdana, sans-serif;
+ }
+
+ .row {
+ background-color: #444;
+ 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;
+ }
+
+ .pull-left {
+ float: left;
+ }
+
+ .clearfix {
+ clear: both;
+ }
+
+ .col {
+ padding: 0 4px;
+ color: white;
+ overflow: hidden;
+ flex: 1 1 auto;
+ text-overflow: ellipsis;
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ }
+
+ .col-square {
+ order: 1000;
+ float: right;
+ width: 70pt;
+ width: 6vw;
+ height: 70pt;
+ height: 6vw;
+ font-size: 56pt;
+ font-size: 4.25vw;
+ flex: 0 0 auto;
+ text-align: center;
+ padding: 0;
+ overflow: visible;
+ }
+
+ .count-1 .col-square {
+ width: 93pt;
+ width: 8vw;
+ height: 93pt;
+ height: 8vw;
+ font-size: 85pt;
+ font-size: 6vw;
+ }
+
+ .count-3 .col-square {
+ width: 46pt;
+ width: 4vw;
+ height: 46pt;
+ height: 4vw;
+ font-size: 35pt;
+ font-size: 2.5vw;
+ }
+
+ .progressbar {
+ width: 0;
+ height: 2px;
+ position: absolute;
+ background-color: red;
+ bottom: 0;
+ 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;
+ }
+
+ .count-1 .header-font {
+ font-size: 30pt;
+ font-size: 2.25vw;
+ }
+
+ .seats-counter {
+ color: white;
+ margin: auto;
+ font-weight: bold;
+ padding: 0;
+ text-shadow: #000 2px 2px;
+ }
+
+ .center {
+ text-align: center;
+ }
+
+ .room-layout {
+ position: relative;
+ float: left;
+ }
+
+ .location-container {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ border: 1px solid darkgrey;
+ background: linear-gradient(#ddd, white);
+ box-sizing: border-box;
+ }
+
+ .calendar {
+ float: left;
+ padding: 0;
+ box-sizing: border-box;
+ }
+
+ .free-busy-busy {
+ background: rgba(0, 0, 0, .25);
+ }
+
+ .ui-widget-content {
+ color: white;
+ }
+
+ .wc-header {
+ background-color: #444;
+ font-weight: bold;
+ }
+
+ .ui-state-default {
+ text-shadow: none;
+ }
+
+ .BROKEN {
+ opacity: 0.4;
+ }
+
+ .pc-container {
+ position: absolute;
+ left: 0;
+ bottom: 0;
+ display: inline-block;
+ padding: 0;
+ margin: 0;
+ overflow: hidden;
+ }
+
+ .pc-container div {
+ box-sizing: border-box;
+ }
+
+ .screen-frame {
+ position: relative;
+ background: black;
+ border-radius: 11%;
+ width: 100%;
+ height: 83%;
+ padding: 6%;
+ }
+
+ .screen-inner {
+ width: 100%;
+ height: 100%;
+ transition: background 2s;
+ border-radius: 5%;
+ padding-top: 4px;
+ overflow: hidden;
+ text-align: center;
+ color: #fff;
+ }
+
+ .BROKEN .screen-inner {
+ background: #000;
+ }
+
+ .OFF .screen-inner {
+ background: #332;
+ }
+
+ /*
+ .OFF .screen-inner:after {
+ content: "\01F4A4";
+ }
+ */
+
+ .IDLE .screen-inner {
+ background: #250;
+ }
+
+ .OCCUPIED .screen-inner {
+ background: #d23;
+ }
+
+ .OCCUPIED .screen-inner:after {
+ content: '\01F464';
+ font-weight: bold;
+ }
+
+ .screen-foot1 {
+ margin: 0 auto;
+ width: 10%;
+ height: 7%;
+ background: black;
+ }
+
+ .screen-foot2 {
+ margin: 0 auto;
+ width: 80%;
+ height: 7%;
+ background: black;
+ border-radius: 30% 30% 0 0;
+ }
+
+ .pc-overlay-container {
+ position: absolute;
+ left: 0;
+ bottom: 0;
+ width: 100%;
+ height: 100%;
+ display: table;
+ }
+
+ .pc-img {
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+
+ }
+
+ .overlay {
+ display: inline-block;
+ position: relative;
+ width: 50%;
+ height: 50%;
+ opacity: 0.5;
+ float: left;
+ z-index: 5;
+ }
+
+ .overlay-rollstuhl {
+ width: 25%;
+ height: 50%;
+ background-color: white;
+ opacity: 0.5;
+ float: left;
+ }
+
+ .ui-widget-content .ui-state-active {
+ font-weight: bold;
+ color: black;
+ }
+
+ .wc-today {
+ background-color: rgba(255, 255, 255, .66);
+ }
+
+ .wc-time-header-cell {
+ background-color: #eeeeee;
+ border: none;
+ }
+
+ .ui-corner-all {
+ border-radius: 0;
+ }
+
+ .wc-scrollable-grid {
+ transition: height 500ms;
+ background: rgba(0, 0, 0, 0);
+ }
+
+ .wc-grid-timeslot-header,
+ .wc-header .wc-time-column-header {
+ width: 50px;
+ }
+
+ #i18n {
+ display: none;
+ }
+
+ </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>
+
+</head>
+<body>
+<div id="i18n">
+ <span data-tag="room">{{lang_room}}</span>
+ <span data-tag="closed">{{lang_closed}}</span>
+ <span data-tag="free">{{lang_free}}</span>
+ <span data-tag="shortSun">{{lang_shortSun}}</span>
+ <span data-tag="shortMon">{{lang_shortMon}}</span>
+ <span data-tag="shortTue">{{lang_shortTue}}</span>
+ <span data-tag="shortWed">{{lang_shortWed}}</span>
+ <span data-tag="shortThu">{{lang_shortThu}}</span>
+ <span data-tag="shortFri">{{lang_shortFri}}</span>
+ <span data-tag="shortSat">{{lang_shortSat}}</span>
+ <span data-tag="longSun">{{lang_longSun}}</span>
+ <span data-tag="longMon">{{lang_longMon}}</span>
+ <span data-tag="longTue">{{lang_longTue}}</span>
+ <span data-tag="longWed">{{lang_longWed}}</span>
+ <span data-tag="longThu">{{lang_longThu}}</span>
+ <span data-tag="longFri">{{lang_longFri}}</span>
+ <span data-tag="longSat">{{lang_longSat}}</span>
+ <span data-tag="to">{{lang_to}}</span>
+</div>
+</body>
+
+<script type="text/javascript">
+ var rooms = {};
+ var lastRoomUpdate = 0;
+ var lastCalendarUpdate = 0;
+ var lastSwitchTime = 0;
+ var hasMode4 = false;
+ var globalConfig = {};
+ var roomIds = [];
+ var panelUuid = '{{{uuid}}}';
+ const IMG_FORMAT_LIST = (function() {
+ if (typeof(SVGRect) !== "undefined") {
+ return [".svg", ".png", ".jpg", ".gif"];
+ }
+ return [".png", ".jpg", ".gif"];
+ })();
+
+ $(document).ready(function () {
+ applyConfig({{{config}}});
+ });
+
+ /**
+ * Display given error message and try reloading page once a minute
+ */
+ function fatalError(message) {
+ $('body').empty().append($('<h1>').text(message));
+ window.setInterval(function () {
+ $.ajax('/').done(function () {
+ window.location.reload(true);
+ }).fail(function () {
+ $('body').append('...');
+ });
+ }, 60000);
+ }
+
+ function applyConfig(result) {
+ if (!result.locations || result.locations.constructor !== Array) {
+ fatalError("Requested panel doesn't contain locations / not array");
+ return;
+ }
+
+ var fetchedRooms = result.locations.filter(function (x) {
+ // filter out if no numeric id, or id already present, or already got 4 locations
+ if (typeof(x.id) !== 'number' || x.id <= 0 || roomIds.indexOf(x.id) !== -1 || roomIds.length >= 4)
+ return false;
+ roomIds.push(x.id);
+ return true;
+ });
+
+ if (roomIds.length === 0) {
+ fatalError("List of location ids is empty");
+ return;
+ }
+
+ var time = false;
+ var p = result.time.split('-');
+ if (p.length === 6) {
+ time = new Date(p[0], p[1], p[2], p[3], p[4], p[5]);
+ console.log(time);
+ }
+ if (time === false || isNaN(time.getTime()) || time.getYear() < 2010) {
+ time = new Date(result.time);
+ }
+ if (isNaN(time.getTime()) || time.getYear() < 2010) {
+ time = new Date();
+ }
+ SetUpDate(time);
+ delete result.time;
+ delete result.locations;
+
+ globalConfig = result;
+ sanitizeGlobalConfig();
+ lastRoomUpdate = MyDate().getTime();
+
+ for (var i = 0; i < fetchedRooms.length; ++i) {
+ addRoom(fetchedRooms[i]);
+ }
+ initRooms();
+ }
+
+ const PARAM_STRING = 1;
+ const PARAM_INT = 2;
+ const PARAM_BOOL = 3;
+
+ /**
+ * Read given parameter from URL, replacing it in the config object if present.
+ * @param config object config object
+ * @param property string name of property in object, URL param of same name is being checked
+ * @param paramType int one of PARAM_STRING, PARAM_INT, PARAM_BOOL
+ * @param intScaleFactor int optional scale factor that will be applied if paramType == PARAM_INT
+ */
+ function setRoomConfigFromUrl(config, property, paramType, intScaleFactor) {
+ var val = getUrlParameter(property);
+ if (val === true || val === false)
+ return;
+ if (paramType === PARAM_STRING) {
+ config[property] = val;
+ } else if (paramType === PARAM_INT) {
+ config[property] = parseInt(val);
+ if (intScaleFactor) {
+ config[property] *= intScaleFactor;
+ }
+ } else if (paramType === PARAM_BOOL) {
+ val = val.toLowerCase();
+ config[property] = val.length > 0 && val !== 'false' && val !== 'off' && val !== '0';
+ } else {
+ console.log('Invalid paramType: ' + paramType);
+ }
+ }
+
+ /**
+ * Put given numeric config property in range min..max (both inclusive),
+ * if not in range, set to default.
+ * @param config - object config object
+ * @param property - string config property
+ * @param min int - min allowed value (inclusive)
+ * @param max int - max allowed value (inclusive)
+ * @param defaultval - default value to use if out of range
+ * @param scaleFactor int - optional scale factor to apply
+ */
+ function putInRange(config, property, min, max, defaultval, scaleFactor) {
+ var v = config[property];
+ if (!scaleFactor) {
+ scaleFactor = 1;
+ }
+ if (!v || !isFinite(v) || isNaN(v) || v < min * scaleFactor || v > max * scaleFactor) {
+ config[property] = defaultval * scaleFactor;
+ }
+ }
+
+ /**
+ * gets Additional Parameters from the URL, and from the
+ * downloaded json.
+ * also makes sure parameters are in a given range
+ */
+ function sanitizeGlobalConfig() {
+ sanitizeConfig(globalConfig);
+ }
+
+ function sanitizeConfig(config) {
+ if (config) {
+ config.switchtime = config.switchtime * 1000;
+ config.calupdate = config.calupdate * 60 * 1000;
+ config.roomupdate = config.roomupdate * 1000;
+ }
+
+ setRoomConfigFromUrl(config, 'calupdate', PARAM_INT, 60 * 1000);
+ setRoomConfigFromUrl(config, 'roomupdate', PARAM_INT, 1000);
+ setRoomConfigFromUrl(config, 'daystoshow', PARAM_INT);
+ setRoomConfigFromUrl(config, 'scaledaysauto', PARAM_BOOL);
+ setRoomConfigFromUrl(config, 'vertical', PARAM_BOOL);
+ setRoomConfigFromUrl(config, 'eco', PARAM_BOOL);
+ setRoomConfigFromUrl(config, 'prettytime', PARAM_BOOL);
+
+ setRoomConfigFromUrl(config, 'scale', PARAM_INT);
+ setRoomConfigFromUrl(config, 'rotation', PARAM_INT);
+ setRoomConfigFromUrl(config, 'switchtime', PARAM_INT, 1000);
+
+ // parameter validation
+ putInRange(config, 'switchtime', 5, 120, 6, 1000);
+ putInRange(config, 'scale', 10, 90, 50);
+ putInRange(config, 'daystoshow', 1, 7, 7);
+ putInRange(config, 'roomupdate', 15, 5 * 60, 60, 1000);
+ putInRange(config, 'calupdate', 1, 60, 30, 60 * 1000);
+ putInRange(config, 'mode', 1, 4, 1);
+ putInRange(config, 'rotation', 0, 3, 0);
+ }
+
+ /**
+ * generates the Room divs and calls the needed functions depending on the rooms mode
+ */
+ function initRooms() {
+
+ var width = "100%";
+ var height = "100%";
+ var columns = 1;
+ var top, left;
+ hasMode4 = false;
+ if (roomIds.length === 2 || roomIds.length === 4) {
+ width = "50%";
+ columns = 2;
+ }
+ if (roomIds.length === 3) {
+ width = "33%";
+ columns = 3;
+ }
+ if (roomIds.length === 4) {
+ height = "50%";
+ }
+ for (var t = 0; t < roomIds.length; t++) {
+ var rid = roomIds[t];
+ var room = rooms[rid];
+ if (roomIds.length === 3) {
+ top = 0;
+ left = (t * 33) + '%';
+ } else {
+ top = (Math.floor(t / 2) * 50) + '%';
+ left = ((t % 2) * 50) + '%';
+ }
+
+ var $loc = $("<div>").addClass('location-container');
+ $loc.css({top: top, left: left, width: width, height: height});
+ $("body").append($loc);
+
+ room.$.container = $loc;
+ room.$.locationName = $('<div>').addClass('col').addClass('header-font').addClass('pull-left');
+ room.$.currentEvent = $("<span>").addClass('nowrap');
+ room.$.currentRemain = $("<span>").addClass('nowrap').addClass('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);
+ $header.append(room.$.seatsBackground);
+ $header.append($('<div>').addClass('col header-font center').append(room.$.currentEvent).append(' ').append(room.$.currentRemain));
+ room.$.header = $header;
+ $loc.append($header);
+ $header.append('<div class="clearfix">');
+
+ if (room.name !== null) {
+ room.$.locationName.text(room.name);
+ }
+
+ if (room.config.mode !== 3) {
+ setUpCalendar(room);
+ }
+ if (room.config.mode !== 2) {
+ initRoomLayout(room);
+ }
+ if (room.config.mode === 4) {
+ hasMode4 = true;
+ }
+ SetOpeningTimes(room);
+ UpdateRoomHeader(room);
+
+ (function (room) {
+ setTimeout(function () {
+ resizeIfRequired(room);
+ }, 800);
+ })(room);
+ }
+
+ if (hasMode4) {
+ generateProgressBar();
+ }
+
+ mainUpdateLoop();
+ setInterval(mainUpdateLoop, 10000);
+ setInterval(updateHeaders, globalConfig.eco ? 10000 : 1000);
+ }
+
+ var lastDate = false;
+ /**
+ * Main Update loop, this loop runs every 10 seconds
+ */
+ function mainUpdateLoop() {
+ var date = MyDate();
+ var now = date.getTime();
+
+ if (lastCalendarUpdate + globalConfig.calupdate < now) {
+ lastCalendarUpdate = now;
+ queryCalendars();
+ } else if (lastRoomUpdate + globalConfig.roomupdate < now) {
+ lastRoomUpdate = now;
+ queryRooms();
+ } else {
+ queryPanelChange();
+ }
+
+ $('.calendar').weekCalendar("scrollToHour");
+
+ // reload site at midnight
+ var today = date.getDate();
+ if (lastDate !== false) {
+ if (lastDate !== today) {
+ location.reload(true);
+ }
+ } else {
+ lastDate = today;
+ }
+ }
+
+ /**
+ * Update all location headers.
+ * Runs ever second (normal) or every 10 seconds (eco)
+ */
+ function updateHeaders() {
+ for (var property in rooms) {
+ if (rooms[property].state.end) {
+ // Updating All room Headers
+ UpdateRoomHeader(rooms[property]);
+ }
+ }
+
+ }
+
+ /**
+ * Generates a room Object and adds it to the rooms array
+ * @param roomData Config Json of the room
+ */
+ function addRoom(roomData) {
+ var mergedConfig = {};
+ if (roomData.config && typeof(roomData.config) === 'object') {
+ mergedConfig = roomData.config;
+ sanitizeConfig(mergedConfig);
+ }
+ for (var k in globalConfig) {
+ if (typeof mergedConfig[k] === 'undefined') {
+ mergedConfig[k] = globalConfig[k];
+ }
+ }
+ var now = MyDate().getTime();
+ var room = {
+ id: roomData.id,
+ name: roomData.name,
+ config: mergedConfig,
+ timetable: null,
+ currentEvent: null,
+ nextEventEnd: null,
+ timeTilFree: null,
+ state: null,
+ rawOpeningTimes: roomData.openingtime || null,
+ openingTimes: null,
+ openTimes: 24,
+ currentfreePcs: 0,
+ layout: roomData.machines || null,
+ freePcs: 0,
+ resizeRoom: true,
+ resizeCalendar: true,
+ lastCalendarUpdate: now,
+ lastRoomUpdate: now,
+ $: {},
+ getState: function () {
+ if (this.state === null) {
+ ComputeCurrentState(this);
+ return this.state;
+ }
+ if (this.state.end) {
+ if (this.state.end < MyDate()) {
+ ComputeCurrentState(this);
+ }
+ }
+ return this.state;
+ }
+
+
+ };
+ rooms[roomData.id] = room;
+ return room;
+ }
+
+ /**
+ * inilizes the Calendar for an room
+ * @param room Room Object
+ */
+ function setUpCalendar(room) {
+ var daysToShow = room.config.daystoshow;
+ generateCalendarDiv(room);
+ room.$.calendar.weekCalendar({
+ timeslotsPerHour: 1,
+ timeslotHeight: 30,
+ daysToShow: daysToShow,
+ height: function () {
+ if (room.config.mode === 1 && room.config.vertical && (!room.timetable || !room.timetable.length)) return 20;
+ var height = $(window).height();
+ if (roomIds.length === 4) {
+ height /= 2;
+ }
+
+ height -= room.$.header.height() - 5;
+ if (room.config.mode === 1 && room.config.vertical) {
+ height *= (room.config.scale / 100);
+ }
+ return height;
+ },
+ eventRender: function (calEvent, $event) {
+ if (calEvent.end.getTime() < MyDate().getTime()) {
+ $event.css("backgroundColor", "#aaa");
+ $event.find(".time").css({"backgroundColor": "#999", "border": "1px solid #888"});
+ } else if (calEvent.end.getTime() > MyDate().getTime() && calEvent.start.getTime() < MyDate().getTime()) {
+ $event.css("backgroundColor", "#25B002");
+ $event.find(".time").css({"backgroundColor": "#25B002", "border": "1px solid #888"});
+ }
+ },
+ date: MyDate(),
+ dateFormat: "j.n",
+ timeFormat: "G:i",
+ scrollToHourMillis: 500,
+ use24Hour: true,
+ readonly: true,
+ showHeader: false,
+ hourLine: true,
+ shortDays: [t("shortSun"), t("shortMon"), t("shortTue"), t("shortWed"), t("shortThu"), t("shortFri"), t("shortSat")],
+ longDays: [t("longSun"), t("longMon"), t("longTue"), t("longWed"), t("longThu"), t("longFri"), t("longSat")],
+ buttons: false,
+ timeSeparator: " - ",
+ startOnFirstDayOfWeek: false,
+ displayFreeBusys: true,
+ defaultFreeBusy: {free: false}
+ });
+ }
+
+ /**
+ * Generates the Calendar Div, depending on it's width
+ * @param room Room Object
+ */
+
+ function generateCalendarDiv(room) {
+ var width = 100;
+ if (room.config.mode === 1 && !room.config.vertical) {
+ width = room.config.scale;
+ }
+ var $cal = $('<div>').addClass('calendar');
+ if (room.config.mode === 1 && room.config.vertical) {
+ $cal.css('float', "none");
+ }
+ $cal.width(width + '%');
+ room.$.container.append($cal);
+ room.$.calendar = $cal;
+ }
+
+ const OT_DAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
+ const OT_KEYS = ['HourOpen', 'HourClose', 'MinutesOpen', 'MinutesClose'];
+
+ /**
+ * sets the opening Time in the calendar plugin and saves it in the room object
+ * @param room Room Object
+ */
+
+ function SetOpeningTimes(room) {
+ var opening = 24;
+ var close = 0;
+ var i;
+ if (room.rawOpeningTimes && typeof(room.rawOpeningTimes) === 'object') {
+ // TODO: wtf! we have three(!) formats for storing the opening times (DB, API, now this one) - WHY!?
+ var parsedOpenings = room.rawOpeningTimes;
+ room.state = null;
+ room.openingTimesCalendar = [];
+ room.openingTimes = [];
+ for (i = 0; i < OT_DAYS.length; ++i) {
+ room.openingTimes.push(filterOpeningTimesDay(parsedOpenings[OT_DAYS[i]]));
+ }
+ delete room.rawOpeningTimes;
+ }
+ if (!room.openingTimes) {
+ scaleCalendar(room);
+ return;
+ }
+ if (room.config.mode === 3) {
+ // Calendar is not displayed, don't need to do additional work
+ return;
+ }
+ var now = MyDate();
+ for (i = 0; i < 7; i++) {
+ var tmp = room.openingTimes[i];
+ for (var d = 0; d < tmp.length; d++) {
+ var day = getNextDayOfWeek(now, i);
+ if (room.openingTimesCalendar) {
+ room.openingTimesCalendar.push({
+ "start": new Date(day.getFullYear(), day.getMonth(), day.getDate(),
+ tmp[d]['HourOpen'], tmp[d]['MinutesOpen']),
+ "end": new Date(day.getFullYear(), day.getMonth(),
+ day.getDate(), tmp[d]['HourClose'], tmp[d]['MinutesClose']),
+ "free": true
+ });
+ }
+ if (tmp[d]['HourOpen'] < opening) {
+ opening = tmp[d]['HourOpen'];
+ }
+ if (tmp[d]['HourClose'] >= close) {
+ close = tmp[d]['HourClose'];
+ if (tmp[d]['MinutesClose'] !== 0) {
+ close++;
+ }
+ }
+ }
+ }
+ if (opening === 24 && close === 0) {
+ opening = 0;
+ close = 24;
+ }
+ room.openTimes = close - opening;
+ scaleCalendar(room);
+ room.$.calendar.weekCalendar("option", "businessHours", {
+ start: opening,
+ end: close,
+ limitDisplay: true
+ });
+ }
+
+ /**
+ * Filter out invalid opening time entries from given array,
+ * also make sure all the values are of type number (int)
+ *
+ * @param {Array} arr
+ * @return {Array} list of valid opening times
+ */
+ function filterOpeningTimesDay(arr) {
+ if (!arr || arr.constructor !== Array) return [];
+ return arr.map(function (el) {
+ if (!el || typeof el !== 'object') return null;
+ for (var i = 0; i < OT_KEYS.length; ++i) {
+ el[OT_KEYS[i]] = toInt(el[OT_KEYS[i]]);
+ if (isNaN(el[OT_KEYS[i]])) return null;
+ }
+ return el;
+ }).filter(function (el) {
+ if (!el) return false;
+ if (el.HourOpen < 0 || el.HourOpen > 23) return false;
+ if (el.HourClose < 0 || el.HourClose > 23) return false;
+ if (el.HourClose < el.HourOpen) return false;
+ if (el.MinutesOpen < 0 || el.MinutesOpen > 59) return false;
+ if (el.MinutesClose < 0 || el.MinutesClose > 59) return false;
+ if (el.HourOpen === el.HourClose && el.MinutesClose < el.MinutesOpen) return false;
+ return true;
+ });
+ }
+
+ /**
+ * querys the Calendar data
+ */
+ function queryCalendars() {
+ if (!panelUuid) return;
+ var url = "{{dirprefix}}api.php?do=locationinfo&get=calendar&uuid=" + panelUuid;
+ $.ajax({
+ url: url,
+ dataType: 'json',
+ cache: false,
+ timeout: 30000,
+ success: function (result) {
+ if (result && result.constructor === Array) {
+ var l = result.length;
+ for (var i = 0; i < l; i++) {
+ updateCalendar(result[i].calendar, rooms[result[i].id]);
+ }
+ }
+ }, error: function () {
+ // Retry in 5 minutes (300 seconds)
+ lastCalendarUpdate = MyDate().getTime() + globalConfig.calupdate + 300000;
+ }
+ });
+ }
+
+ const SEVEN_DAYS = 7 * 86400 * 1000;
+
+ /**
+ * applays new calendar data to the calendar plugin and also saves it to the room object
+ * @param {Array} json Calendar data
+ * @param room Room Object
+ */
+ function updateCalendar(json, room) {
+ if (!room) {
+ console.log("Error: No room for calendar data");
+ return;
+ }
+ if (!json || json.constructor !== Array) {
+ console.log("Error: Calendar data was empty or malformed.");
+ return;
+ }
+ if (json.length === 0) {
+ console.log("Notice: Calendar already empty from server");
+ }
+ var now = MyDate().getTime();
+ json = json.filter(function (el) {
+ if (!el.title || !el.start || !el.end) return false;
+ var s = new Date(el.start).getTime();
+ var e = new Date(el.end).getTime();
+ return !(isNaN(s) || isNaN(e) || Math.abs(s - now) > SEVEN_DAYS || Math.abs(e - now) > SEVEN_DAYS);
+ });
+ if (json.length === 0) {
+ console.log('Notice: Calendar has no current events for ' + room.name);
+ }
+ try {
+ room.timetable = json;
+ if (room.config.mode !== 3) {
+ // TODO: Check if they're the same
+ var cal = room.$.calendar;
+ cal.weekCalendar('option', 'data', {events: json});
+ cal.weekCalendar("refresh");
+ cal.weekCalendar("option", "defaultFreeBusy", {free: !room.openingTimesCalendar});
+ cal.weekCalendar("updateFreeBusy", room.openingTimesCalendar);
+ cal.weekCalendar("resizeCalendar");
+ cal.weekCalendar("option", "hourLine", true);
+ setTimeout(function() {
+ scaleRoom(room);
+ }, 550);
+ }
+ room.state = null;
+ UpdateRoomHeader(room);
+ } catch (e) {
+ console.log("Error: Couldnt add calendar data");
+ console.log(e);
+ }
+ }
+
+ /**
+ * scales calendar, called once on create and on window resize
+ * @param room Room Object
+ */
+ function scaleCalendar(room) {
+ if (room.config.mode === 3) {
+ return;
+ }
+ var $cal = room.$.calendar;
+ if (!$cal.is(':visible')) return;
+ room.resizeCalendar = false;
+ var columnWidth = $cal.find(".wc-day-1").width();
+
+ if (room.config.scaledaysauto) {
+ var result = ($cal.weekCalendar("option", "daysToShow") * columnWidth) / 100;
+ result = parseInt(Math.min(Math.max(Math.abs(result), 1), 7));
+ if (result !== $cal.weekCalendar("option", "daysToShow")) {
+ $cal.weekCalendar("option", "daysToShow", result);
+ }
+ }
+ if (((!room.config.scaledaysauto) || $cal.weekCalendar("option", "daysToShow") === 1) && columnWidth < 85) {
+ $cal.weekCalendar("option", "useShortDayNames", true);
+ } else {
+ $cal.weekCalendar("option", "useShortDayNames", false);
+ }
+ var clientHeight = $(window).height();
+ if (roomIds.length === 4) {
+ clientHeight = clientHeight / 2;
+ }
+
+ clientHeight = clientHeight - room.$.header.height()
+ - room.$.calendar.find(".wc-time-column-header").height() - 2;
+
+ if (room.config.mode === 1 && room.config.vertical) {
+
+ clientHeight = clientHeight * (room.config.scale / 100);
+ clientHeight -= 22;
+ }
+ clientHeight -= 6;
+ var height = clientHeight / (room.openTimes * $cal.weekCalendar("option", "timeslotsPerHour"));
+
+
+ if (height < 30) {
+ height = 30;
+ }
+ // Scale calendar font
+ if (height > 120) {
+ $cal.weekCalendar("option", "textSize", 28);
+ }
+ else if (height > 100) {
+ $cal.weekCalendar("option", "textSize", 24);
+ } else if (height > 80) {
+ $cal.weekCalendar("option", "textSize", 22);
+ } else if (height > 70) {
+ $cal.weekCalendar("option", "textSize", 20);
+ } else if (height > 60) {
+ $cal.weekCalendar("option", "textSize", 14);
+ } else {
+ $cal.weekCalendar("option", "textSize", 13);
+ }
+ $cal.weekCalendar("option", "timeslotHeight", height);
+ if (room.timetable) {
+ $cal.weekCalendar("option", "data", {events: room.timetable});
+ $cal.weekCalendar('refresh');
+ }
+ $cal.weekCalendar("option", "defaultFreeBusy", {free: !room.openingTimesCalendar});
+ if (room.openingTimesCalendar) {
+ $cal.weekCalendar("updateFreeBusy", room.openingTimesCalendar);
+ }
+ $cal.weekCalendar("resizeCalendar");
+ $cal.weekCalendar("option", "hourLine", true);
+ }
+
+ /**
+ * used for countdown
+ * computes the time difference between 2 Date objects
+ * @param {Date} a
+ * @param {Date} b
+ * @returns {string} printable time
+ */
+ function GetTimeDiferenceAsString(a, b) {
+ if (!a || !b) {
+ return "";
+ }
+ var milliseconds = a.getTime() - b.getTime();
+ var days = Math.floor((milliseconds / (1000 * 60 * 60 * 24)) % 31);
+ if (days !== 0) {
+ // don't show?
+ return "";
+ }
+ var seconds = Math.floor((milliseconds / 1000) % 60);
+ milliseconds -= seconds * 1000;
+ var minutes = Math.floor((milliseconds / (1000 * 60)) % 60);
+ milliseconds -= minutes * 1000 * 60;
+ var hours = Math.floor((milliseconds / (1000 * 60 * 60)) % 24);
+
+ if (globalConfig.prettytime) {
+ var str = '';
+ if (hours > 0) {
+ str += hours + 'muh';
+ }
+ }
+
+ if (minutes < 10) {
+ minutes = "0" + minutes;
+ }
+ if (globalConfig.eco) {
+ return hours + ":" + minutes;
+ }
+ if (seconds < 10) {
+ seconds = "0" + seconds;
+ }
+ return hours + ":" + minutes + ":" + seconds;
+ }
+
+ /**
+ * returns next closing time of a given room
+ * @param room
+ * @returns {Date} Object of next closing
+ */
+ function GetNextClosing(room) {
+ if (!room.openingTimes || room.openingTimes.length === 0) return null;
+ var now = MyDate();
+ var day = now.getDay();
+ var bestdate = null;
+ for (var a = 0; a < 7; a++) {
+ var tmp = room.openingTimes[(day + a) % 7];
+ if (!tmp) continue;
+ for (var i = 0; i < tmp.length; i++) {
+ var closeDate = getNextDayOfWeek(now, (day + a) % 7);
+ closeDate.setHours(tmp[i].HourClose);
+ closeDate.setMinutes(tmp[i].MinutesClose);
+ closeDate.setSeconds(0);
+ if (closeDate > now) {
+ if (!IsOpen(new Date(closeDate.getTime() + 1800000), room)) {
+ if (!bestdate || bestdate > closeDate) {
+ bestdate = closeDate;
+ }
+ }
+ }
+ }
+ if (bestdate) return bestdate;
+ }
+ return null;
+ }
+
+
+ /**
+ * checks if a room is on a given date/time open
+ * @param date Date Object
+ * @param room Room object
+ * @returns {Boolean} for open or not
+ */
+ function IsOpen(date, room) {
+ if (!room.openingTimes || room.openingTimes.length === 0) return true;
+ var tmp = room.openingTimes[date.getDay()];
+ if (!tmp) return false;
+ var openDate = new Date(date.getTime());
+ var closeDate = new Date(date.getTime());
+ for (var i = 0; i < tmp.length; i++) {
+ openDate.setHours(tmp[i].HourOpen);
+ openDate.setMinutes(tmp[i].MinutesOpen);
+ closeDate.setHours(tmp[i].HourClose);
+ closeDate.setMinutes(tmp[i].MinutesClose);
+ if (openDate < date && closeDate > date) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+
+ /**
+ * Returns next Opening
+ * @param room Room Object
+ * @returns {Date} Object of next opening
+ */
+ function GetNextOpening(room) {
+ if (!room.openingTimes) return null;
+ var now = MyDate();
+ var day = now.getDay();
+ var bestdate = null;
+ for (var dow = 0; dow < 7; dow++) {
+ var tmp = room.openingTimes[(day + dow) % 7];
+ if (!tmp) continue;
+ for (var i = 0; i < tmp.length; i++) {
+ var openDate = getNextDayOfWeek(now, (day + dow) % 7);
+ openDate.setHours(tmp[i].HourOpen);
+ openDate.setMinutes(tmp[i].MinutesOpen);
+ if (openDate > now) {
+ if (!IsOpen(new Date(openDate.getTime() - 1800000), room)) {
+ if (!bestdate || bestdate > openDate) {
+ bestdate = openDate;
+ }
+ }
+ }
+ }
+ if (bestdate) return bestdate;
+ }
+ return null;
+ }
+
+
+ /**
+ * Sets the free PCs number in the right corner and updates the square color accordingly
+ * @param room Room
+ * @param seats Number of free PC's in the room
+ */
+ function SetFreeSeats(room) {
+ room.$.seatsCounter.text(room.freePcs >= 0 ? room.freePcs : '');
+ if (room.freePcs > 0 && room.state && room.state.free) {
+ room.$.seatsBackground.css('background-color', '#250');
+ } else if (room.freePcs === -1) {
+ room.$.seatsBackground.css('background-color', 'red');
+ } else {
+ room.$.seatsBackground.css('background-color', 'red');
+ }
+ }
+
+ /**
+ * Updates the Header of an Room
+ * @param room Room Object
+ */
+ function UpdateRoomHeader(room) {
+ var tmp = room.getState();
+ var same = (tmp === room.lastHeaderState);
+ if (!same) {
+ room.lastHeaderState = tmp;
+ }
+ var newText = false, newTime = false;
+ var seats = room.freePcs;
+ if (tmp.state === 'closed' || tmp.state === 'CalendarEvent' || tmp.state === 'Free') {
+ newTime = GetTimeDiferenceAsString(tmp.end, MyDate());
+ } else if (!same) {
+ newTime = '';
+ }
+ if (tmp.state === "closed") {
+ if (!same) newText = t("closed");
+ } else if (tmp.state === "CalendarEvent") {
+ if (!same) newText = tmp.title;
+ seats = -1;
+ } else if (tmp.state === "Free") {
+ if (!same) newText = t("free");
+ } else if (tmp.state === "FreeNoEnd") {
+ if (!same) newText = t("free");
+ }
+ if (newText !== false) {
+ room.$.currentEvent.text(newText);
+ }
+ if (newTime !== false) {
+ room.$.currentRemain.text(newTime);
+ }
+ if (room.lastFreeSeats !== seats) {
+ SetFreeSeats(room);
+ room.lastFreeSeats = seats;
+ }
+ }
+
+ /**
+ * computes state of a room, states are:
+ * closed, FreeNoEnd, Free, CalendarEvent.
+ * @param room Object
+ */
+ function ComputeCurrentState(room) {
+ if (!IsOpen(MyDate(), room)) {
+ room.state = {state: "closed", end: GetNextOpening(room), title: "", next: ""};
+
+ return;
+ }
+ var closing = GetNextClosing(room);
+
+ var event = getNextEvent(room.timetable);
+
+ // no event and no closing
+ if (!closing && !event) {
+ room.state = {state: "FreeNoEnd", end: "", title: "", next: "", free: true};
+ return;
+ }
+
+ // no event so closing is next
+ if (!event) {
+ room.state = {state: "Free", end: closing, title: "", next: "closing", free: true};
+ return;
+ }
+
+ // event is at the moment
+ if ((!closing || event.start.getTime() < closing.getTime()) && event.start.getTime() < MyDate()) {
+ room.state = {
+ state: "CalendarEvent",
+ end: event.end,
+ title: event.title,
+ next: ""
+ };
+ return;
+ }
+
+ // no closing so event is next
+ if (!closing) {
+ room.state = {state: "Free", end: event.start, title: "", next: "event", free: true};
+ return;
+ }
+
+ // event sooner then closing
+ if (event.start.getTime() < closing) {
+ room.state = {state: "Free", end: event.start, title: "", next: "event", free: true};
+ } else {
+ room.state = {state: "Free", end: closing, title: "", next: "closing", free: true};
+ }
+
+ }
+
+
+ /**
+ * returns next event from a given json of events
+ * @param calEvents Json which contains the calendar data.
+ * @returns event next Calendar Event
+ */
+ function getNextEvent(calEvents) {
+ if (!calEvents) return null;
+ if (calEvents.constructor !== Array) {
+ console.log('getNextEvent called with something not array: ' + typeof(calEvents));
+ return null;
+ }
+ var event;
+ var now = MyDate();
+ for (var i = 0; i < calEvents.length; i++) {
+ //event is now active
+ if (calEvents[i].start.getTime() < now.getTime() && calEvents[i].end.getTime() > now.getTime()) {
+ return calEvents[i];
+ }
+ //first element to consider
+ if (!event) {
+ if (calEvents[i].start.getTime() > now.getTime()) {
+ event = calEvents[i];
+ }
+ } else if (calEvents[i].start.getTime() > now.getTime() && event.start.getTime() > calEvents[i].start.getTime()) {
+ event = calEvents[i];
+ }
+ }
+ return event;
+ }
+
+ /**
+ * Skip to next upcoming day matching the given day of week.
+ * @return {Date}
+ */
+ function getNextDayOfWeek(date, dayOfWeek) {
+ var resultDate = new Date(date.getTime());
+ resultDate.setDate(date.getDate() + (7 + dayOfWeek - date.getDay()) % 7);
+ return resultDate;
+ }
+ /*
+ /========================================== Room Layout =============================================
+ */
+
+
+ const picSizeX = 3.8;
+ const picSizeY = 3;
+
+ /**
+ * Generates the RoomLayout Div
+ * @param width The width the RoomLayout should have (in percent).
+ * @param room Room Object
+ */
+ function generateRoomLayoutDiv(width, room) {
+ if ((room.config.vertical && room.config.mode === 1) || (room.config.mode === 3) || (room.config.mode === 4)) {
+ width = 100 + "%";
+ }
+ var $div = $('<div>').prop('id', 'roomLayout_' + room.id).addClass("room-layout").css('width', width);
+
+ if (room.config.mode === 4) {
+ $div.hide();
+ }
+ room.$.container.append($div);
+ room.$.layout = $div;
+ }
+
+ /**
+ * Main function for generating the Room Layout
+ * @param room Room Object
+ */
+ function initRoomLayout(room) {
+ var maxX = false, maxY = false;
+ var minX = false, minY = false;
+ var xDifference, yDifference;
+ var x, y;
+
+ generateRoomLayoutDiv((100 - room.config.scale) + "%", room);
+ var layout = room.layout;
+ if (layout === null || !layout.length) {
+ return;
+ }
+
+ rotateRoom(room.config.rotation, layout);
+
+ for (var i = 0; i < layout.length; i++) {
+ x = layout[i].x = parseInt(layout[i].x);
+ y = layout[i].y = parseInt(layout[i].y);
+ if (isNaN(x) || isNaN(y)) continue;
+ if (minX === false || x < minX) {
+ minX = x;
+ }
+ if (minY === false || y < minY) {
+ minY = y;
+ }
+ if (maxX === false || x > maxX) {
+ maxX = x;
+ }
+ if (maxY === false || y > maxY) {
+ maxY = y;
+ }
+ }
+
+ xDifference = maxX - minX;
+ yDifference = maxY - minY;
+
+ room.xDifference = xDifference;
+ room.yDifference = yDifference;
+ room.minX = minX;
+ room.minY = minY;
+ room.maxX = maxX;
+ room.maxY = maxY;
+
+ setUpRoom(room, layout);
+ scaleRoom(room);
+ UpdatePc(layout, room);
+
+ }
+
+ /**
+ * Computes offsets and scaling's for the RoomLayout
+ * @param room Room Object
+ */
+ function generateOffsetAndScale(room) {
+ var clientHeight;
+
+ if (room.config.vertical && room.config.mode === 1) {
+ clientHeight = room.$.container.height() - (room.$.calendar.position().top + room.$.calendar.height());
+ } else {
+ clientHeight = room.$.container.height() - (room.$.header.height() + 5);
+ }
+
+ var clientWidth = room.$.layout.width();
+
+ var scaleX;
+ if (room.xDifference !== 0) {
+ scaleX = clientWidth / room.xDifference;
+ } else {
+ scaleX = clientWidth;
+ }
+ var scaleY;
+ if (room.yDifference !== 0) {
+ scaleY = clientHeight / room.yDifference;
+ } else {
+ scaleY = clientHeight;
+ }
+ var scaleYs = (clientHeight - (picSizeY * scaleY)) / room.yDifference;
+ var scaleXs = (clientWidth - (picSizeX * scaleX)) / room.xDifference;
+ if (scaleYs <= 0) {
+ scaleYs = 9999;
+ }
+ if (scaleXs <= 0) {
+ scaleXs = 9999;
+ }
+
+ room.scale = Math.min(scaleYs, scaleY, scaleXs, scaleX, (clientHeight * 0.9) / picSizeY, (clientWidth * 0.9) / picSizeX);
+ room.xOffset = 0 - room.minX;
+ room.yOffset = 0 - room.minY;
+ room.xOffset += ((1 / 2 * (clientWidth - (((room.maxX + room.xOffset) * room.scale) + picSizeX * room.scale))) / room.scale);
+ room.yOffset += ((1 / 2 * (clientHeight - (((room.maxY + room.yOffset) * room.scale) + picSizeY * room.scale))) / room.scale);
+ }
+
+
+ /**
+ * adds images for each pc to Room Layout
+ * @param room Room Object
+ * @param layout Layout json
+ */
+ function setUpRoom(room, layout) {
+ for (var i = 0; i < layout.length; i++) {
+ if (!isNaN(layout[i].y) && !isNaN(layout[i].x)) {
+ //var $img = $('<img>').prop('id', "pc-img_" + room.id + "_" + layout[i].id).addClass('pc-img');
+ var $overlays = $('<div>').addClass('pc-overlay-container');
+ layout[i].$div = $('<div>').prop('id', "pc_" + room.id + "_" + layout[i].id).addClass('pc-container');
+ layout[i].$div.append($('<div>').addClass('screen-frame').append($('<div>').addClass('screen-inner')));
+ layout[i].$div.append($('<div>').addClass('screen-foot1'));
+ layout[i].$div.append($('<div>').addClass('screen-foot2'));
+ //layout[i].$div.append($overlays).append($img);
+ room.$.layout.append(layout[i].$div);
+
+ if (layout[i].overlay && layout[i].overlay.constructor === Array) {
+ for (var a = 0; a < layout[i].overlay.length; a++) {
+ addOverlay($overlays, layout[i].overlay[a]);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Generate overlay with given image name.
+ * @param $container container to put overlay into
+ * @param overlayName name of the overlay (image name without ending)
+ */
+ function addOverlay($container, overlayName) {
+ var imgname;
+ for (var i = 0; i < IMG_FORMAT_LIST.length; ++i) {
+ if (imageExists("img/overlay/" + overlayName + IMG_FORMAT_LIST[i])) {
+ imgname = "img/overlay/" + overlayName + IMG_FORMAT_LIST[i];
+ break;
+ }
+
+ }
+ var $overlay;
+ if (!imgname) {
+ $overlay = $('<div>');
+ } else {
+ $overlay = $("<img>").attr('src', imgname);
+ }
+ $overlay.addClass('overlay').addClass("overlay-" + overlayName);
+ $container.append($overlay);
+ }
+
+
+ var imgExists = {};
+
+ /**
+ * checks if images exists on the web server.
+ * result will be cached after fist call.
+ *
+ * @param {String} image_url URL of image to check
+ * @return {Boolean} true iff image exists
+ */
+ function imageExists(image_url) {
+ if (!imgExists.hasOwnProperty(image_url)) {
+ var http = new XMLHttpRequest();
+ http.open('HEAD', image_url, false);
+ http.send();
+ imgExists[image_url] = http.status === 200;
+ }
+ return imgExists[image_url];
+
+ }
+
+ /**
+ * Checks whether the panel has been edited and reloads
+ * the entire page if so.
+ */
+ function queryPanelChange() {
+ $.ajax({
+ url: "{{dirprefix}}api.php?do=locationinfo&get=timestamp&uuid=" + panelUuid,
+ dataType: 'json',
+ cache: false,
+ timeout: 5000,
+ success: function (result) {
+ if (!result || !result.ts) {
+ console.log('Warning: get=timestamp didnt return json with ts field');
+ return;
+ }
+ if (globalConfig.ts && globalConfig.ts !== result.ts) {
+ // Change
+ window.location.reload(true);
+ }
+ globalConfig.ts = result.ts;
+ }
+ })
+ }
+
+ /**
+ * Queries Pc states
+ */
+ function queryRooms() {
+ $.ajax({
+ url: "{{dirprefix}}api.php?do=locationinfo&get=machines&uuid=" + panelUuid,
+ dataType: 'json',
+ cache: false,
+ timeout: 30000,
+ success: function (result) {
+ if (!result || result.constructor !== Array) {
+ console.log('Warning: get=machines didnt return array');
+ return;
+ }
+ for (var i = 0; i < result.length; i++) {
+ UpdatePc(result[i].machines, rooms[result[i].id]);
+ }
+ }
+ })
+ }
+
+ /**
+ * Updates the PC's (images) in the room layout. Also Updates how many pc's are free.
+ * @param update Update Json from query for one(!) room
+ * @param room Room object
+ */
+ function UpdatePc(update, room) {
+ if (!room) {
+ console.log('Got room update for unknown room, ignored.');
+ return;
+ }
+ if (!update || update.constructor !== Array) {
+ console.log('Update data is not array for room ' + room.name);
+ console.log(update);
+ return;
+ }
+ var freePcs = 0;
+ for (var i = 0; i < update.length; i++) {
+ var $div = $("#pc_" + room.id + "_" + update[i].id);
+ // Pc free
+ if (update[i].pcState === "IDLE" || update[i].pcState === "OFF") {
+ freePcs++;
+ }
+
+ $div.removeClass('BROKEN OFF IDLE OCCUPIED'.replace(update[i].pcState, '')).addClass(update[i].pcState);
+ }
+ room.freePcs = freePcs;
+ UpdateRoomHeader(room);
+ }
+
+ /**
+ * Adjust pc coordinate depending on room rotation
+ * @param r Rotation, from 0 - 3 (int)
+ * @param layout Layout json
+ */
+ function rotateRoom(r, layout) {
+ for (var z = 0; z < r; z++) {
+ for (var i = 0; i < layout.length; i++) {
+ var x = parseInt(layout[i].x);
+ var y = parseInt(layout[i].y);
+ layout[i].x = y;
+ layout[i].y = -x;
+ }
+ }
+ }
+
+ /**
+ * Positions the computer images in the roomLayout div according to their position and div size
+ * @param room Room object
+ */
+ function scaleRoom(room) {
+ if (!room.$.layout || !room.$.layout.is(':visible')) return;
+ room.resizeRoom = false;
+ generateOffsetAndScale(room);
+ room.$.layout.css('font-size', Math.floor(room.scale) + 'pt');
+ for (var i = 0; i < room.layout.length; i++) {
+ var pcWidth = (picSizeX * room.scale) + "px";
+ var pcHeight = (picSizeY * room.scale) + "px";
+ if (room.layout[i].$div && !isNaN(room.layout[i].y) && !isNaN(room.layout[i].x)) {
+ room.layout[i].$div.css({
+ width: pcWidth,
+ height: pcHeight,
+ top: ((room.layout[i].y + room.yOffset) * room.scale) + "px",
+ left: ((room.layout[i].x + room.xOffset) * room.scale) + "px"
+ });
+ }
+ }
+ }
+
+ /*
+ /========================================== Misc =============================================
+ */
+ var resizeTimeout = false;
+
+ // called when browser window changes size
+ // scales calendar and room layout accordingly
+
+ $(window).resize(function () {
+ if (resizeTimeout !== false) clearTimeout(resizeTimeout);
+ resizeTimeout = setTimeout(function () {
+ resizeTimeout = false;
+ for (var property in rooms) {
+ rooms[property].resizeCalendar = true;
+ rooms[property].resizeRoom = true;
+ scaleCalendar(rooms[property]);
+ scaleRoom(rooms[property]);
+ }
+ SetProgressBarSpeed();
+ }, 200);
+ });
+
+
+ /**
+ * returns parameter value from the url
+ * @param sParam
+ * @returns boolean|string for given parameter
+ */
+ function getUrlParameter(sParam) {
+ var sPageURL = decodeURIComponent(window.location.search.substring(1)),
+ sURLVariables = sPageURL.split('&'),
+ sParameterName,
+ i;
+
+ for (i = 0; i < sURLVariables.length; i++) {
+ sParameterName = sURLVariables[i].split('=', 2);
+
+ if (sParameterName[0] === sParam) {
+ if (sParameterName.length === 1) return true;
+ return sParameterName[1];
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Function for translation
+ * @param toTranslate key which we wan't to translate
+ * @returns r translated string
+ */
+ function t(toTranslate) {
+ if (tCache[toTranslate])
+ return tCache[toTranslate];
+ var r = $('#i18n').find('[data-tag="' + toTranslate + '"]');
+ return tCache[toTranslate] = (r.length === 0 ? toTranslate : r.text());
+ }
+ var tCache = {};
+
+ function resizeIfRequired(room) {
+ if (room.resizeCalendar) {
+ scaleCalendar(room);
+ }
+ if (room.resizeRoom) {
+ scaleRoom(room);
+ }
+ }
+
+
+ /**
+ * Used in Mode 4, switches given room from Timetable to room layout and vice versa
+ */
+ function switchLayouts() {
+ for (var roomKey in rooms) {
+ var room = rooms[roomKey];
+ if (room.config.mode !== 4) continue;
+ if (room.$.layout.is(':visible')) {
+ room.$.layout.hide();
+ room.$.calendar.show();
+ } else {
+ room.$.layout.show();
+ room.$.calendar.hide();
+ }
+ resizeIfRequired(room);
+ }
+ lastSwitchTime = MyDate().getTime();
+ }
+
+ var $pbar = false;
+ var pbarTimer = false;
+ const PX_PER_SEC_TARGET = 10;
+
+ /**
+ * adds a progressbar (id) used in mode 4
+ */
+ function generateProgressBar() {
+ if ($pbar) return;
+ $pbar = $('<div class="progressbar">');
+ $('body').append($pbar);
+ SetProgressBarSpeed();
+ }
+
+ function SetProgressBarSpeed() {
+ if (!$pbar || !globalConfig.switchtime) return;
+ if (pbarTimer) clearInterval(pbarTimer);
+ var interval = 1000;
+ if (!globalConfig.eco) {
+ var pxPerMSec = $('body').width() / globalConfig.switchtime;
+ interval = Math.max(1 / (pxPerMSec / PX_PER_SEC_TARGET), 100);
+ }
+ pbarTimer = setInterval(function () {
+ var width = ((MyDate().getTime() - lastSwitchTime) / globalConfig.switchtime) * 100;
+ if (width < 0) width = 0;
+ if (width >= 100) {
+ width = 100;
+ switchLayouts();
+ }
+ $pbar.width(width + '%');
+ }, interval);
+ }
+
+ /**
+ * Convert passed argument to integer if possible, return NaN otherwise.
+ * The difference to parseInt() is that leading zeros are ignored and not
+ * interpreted as octal representation.
+ *
+ * @param str string or already a number
+ * @return {number} str converted to number, or NaN
+ */
+ function toInt(str) {
+ var t = typeof str;
+ if (t === 'number') return str | 0;
+ if (t === 'string') return parseInt(str.replace(/^0+([^0])/, '$1'));
+ return NaN;
+ }
+
+</script>
+</html>
diff --git a/modules-available/locationinfo/templates/frontend-summary.html b/modules-available/locationinfo/templates/frontend-summary.html
new file mode 100644
index 00000000..dd5fc25d
--- /dev/null
+++ b/modules-available/locationinfo/templates/frontend-summary.html
@@ -0,0 +1,700 @@
+<!DOCTYPE html>
+<html lang="de">
+<meta name="viewport" content="width=device-width, initial-scale=1.0" charset="utf-8">
+<head>
+ <script type='text/javascript' src='../../../script/jquery.js'></script>
+
+ <style type='text/css'>
+ body {
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ background-color: lightgrey;
+ color: black;
+ }
+
+ #main {
+ display: flex;
+ flex-wrap: wrap;
+ }
+
+ .outermost {
+ font-size: 16pt;
+ }
+
+ .parent, .child {
+ padding: 5px;
+ float: left;
+ background-color: white;
+ font-size: 90%;
+ min-height: 7em;
+ flex-grow: 1;
+ align-items: stretch;
+ }
+
+ .parent .parent, .parent .child {
+ min-height: 5em;
+ }
+
+ .border {
+ flex-grow: 1;
+ display: inline-flex;
+ align-items: stretch;
+ padding: 5px;
+ }
+
+ .courseFont {
+ padding: 2px;
+ font-size: 90%;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-weight: bold;
+ overflow: hidden;
+ }
+
+ .headerFont {
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-weight: bold;
+ border: 0px;
+ border-bottom: 1px;
+ margin-bottom: 1px;
+ border-color: grey;
+ border-style: solid;
+ }
+
+ .pc-idle, .pc-occupied, .pc-off, .pc-broken {
+ padding: 2px 1px;
+ text-align: center;
+ font-size: 90%;
+ font-weight: 800;
+ overflow: hidden;
+ transition: width 2s;
+ width: 25%;
+ }
+
+ .pc-idle {
+ background-color: green;
+ }
+
+ .pc-occupied {
+ background-color: red;
+ border-radius: 3px 0px 0px 3px;
+ }
+
+ .pc-off {
+ background-color: darkgrey;
+ }
+
+ .pc-broken {
+ background-color: black;
+ color: white;
+ border-radius: 0px 3px 3px 0px;
+ }
+
+ .pc-state-wrapper {
+ display: flex;
+ }
+
+ .paperEffect {
+ margin: 0 auto;
+ background-color: #fff;
+ box-shadow: 0 0 0.2vmin rgba(0, 0, 0, 0.4), inset 0 0 1vmin rgba(0, 0, 0, 0.1);
+ border-radius: 1px;
+ }
+
+
+ </style>
+ <script type='text/javascript'>
+
+ var rooms = {};
+ var startdate;
+ var roomidsString = "";
+
+
+ $(document).ready(function () {
+ //temp
+ SetUpDate(new Date());
+ init();
+ });
+
+ function init() {
+ var ids = getUrlParameter("id");
+ $.getJSON("../../../api.php?do=locationinfo&action=locationtree&id=" + ids, function (result) {
+ generateLayout(result);
+
+ setTimeout(update, 1000);
+ });
+
+ }
+
+ function SetUpDate(d) {
+ startdate = d.getTime() - new Date().getTime();
+ }
+
+ function MyDate() {
+ return new Date(startdate + new Date().getTime());
+ }
+
+ function generateLayout(json) {
+ for (var i = 0; i < json.length; i++) {
+ console.log('Outermost for ' + json[i].locationid);
+ var el = generateObject(json[i], ($("#main")), true);
+ }
+ }
+
+ /**
+ * generates the divs, decidecs if parent or child
+ * @param json Room tree json
+ * @param myParent parent div
+ * @param outermost if the object is a root node
+ * @returns generated div
+ */
+ function generateObject(json, myParent, outermost) {
+ var obj;
+ if (!json.children || json.children.length == 0) {
+ obj = generateChild(myParent, json.locationid, json.locationname, outermost);
+ } else {
+ obj = generateParent(myParent, json.locationid, json.locationname, outermost);
+ for (var i = 0; i < json.children.length; i++) {
+ generateObject(json.children[i], $("#parent_" + json.locationid), false);
+ }
+ }
+ return obj;
+
+ }
+
+ /**
+ * Helper function to generate id string used in query functions
+ * @param list A string, wicht contains ids or not(for now)
+ * @param id An ID which should be added to the list
+ */
+ function addIdToUpdateList(list, id) {
+ if (list == "") {
+ list += id;
+ } else {
+ list += ("," + id);
+ }
+ return list;
+ }
+
+
+ const ROOMUPDATE_MS = 2*60*1000;
+ const CALUPDATE_MS = 20*60*1000;
+
+ function update() {
+ var calendarUpdateIds = "";
+ var rommUpdateIds = "";
+ var count = 0;
+ var nextUpdate = 15000;
+ for (var property in rooms) {
+ if (rooms[property].lastCalendarUpdate === null || rooms[property].lastCalendarUpdate + CALUPDATE_MS < MyDate().getTime()) {
+ calendarUpdateIds = addIdToUpdateList(calendarUpdateIds, rooms[property].id);
+ count++;
+ rooms[property].lastCalendarUpdate = MyDate().getTime();
+ }
+ if (rooms[property].lastRoomUpdate === null || rooms[property].lastRoomUpdate + ROOMUPDATE_MS < MyDate().getTime()) {
+ rommUpdateIds = addIdToUpdateList(rommUpdateIds, rooms[property].id);
+ count++;
+ rooms[property].lastRoomUpdate = MyDate().getTime();
+ }
+ if (count > 7) break;
+ }
+ if (calendarUpdateIds !== "") {
+ queryCalendars(calendarUpdateIds);
+ nextUpdate = 1000;
+ }
+ if (rommUpdateIds !== "") {
+ queryRooms(rommUpdateIds);
+ nextUpdate = 1000;
+ }
+ for (var property in rooms) {
+ upDateRoomState(rooms[property]);
+ }
+ setTimeout(update, nextUpdate);
+ }
+
+
+ function UpdateTimeTables(json) {
+ var l = json.length;
+ for (var i = 0; i < l; i++) {
+ rooms[json[i].id].timetable = json[i].calendar;
+ for (var property in rooms[json[i].id].timetable) {
+ rooms[json[i].id].timetable[property].start = new Date(rooms[json[i].id].timetable[property].start);
+ rooms[json[i].id].timetable[property].end = new Date(rooms[json[i].id].timetable[property].end);
+ }
+ ComputeCurrentState(rooms[json[i].id]);
+ }
+ }
+
+ /**
+ * Querys Pc states
+ * @param ids Room ID's which should be queried. Format for e.g.: "20,5,6"
+ */
+ function queryRooms(ids) {
+ $.ajax({
+ url: "../../../api.php?do=locationinfo&action=pcstates&id=" + ids,
+ dataType: 'json',
+ cache: false,
+ timeout: 30000,
+ success: function (result) {
+ var l = result.length;
+ if (result[0] == null) {
+ console.log("Error: Backend reported null back for RoomUpdate, this might happend if the room isn't" +
+ "configurated.");
+ return;
+ }
+ updatePcStates(result);
+ }, error: function () {
+
+ }
+ })
+ }
+
+ /**
+ * Updates a room visualy
+ * @param room A room to update
+ */
+ function upDateRoomState(room) {
+ if (room === undefined || room.lastRoomUpdate === null) {
+ return;
+ }
+
+ var state = room.getState();
+
+ if (state.state == "CalendarEvent") {
+ updateCourseText(room.id, state.titel);
+ updateCoursTimer(room.id, GetTimeDiferenceAsString(state.end, MyDate()));
+ } else if (state.state == "Free") {
+ updateCourseText(room.id, "Frei");
+ updateCoursTimer(room.id, GetTimeDiferenceAsString(state.end, MyDate()));
+ } else if (state.state == "FreeNoEnd") {
+ updateCourseText(room.id, "Frei");
+ updateCoursTimer(room.id, "");
+ }
+ else if (state.state == "closed") {
+ updateCourseText(room.id, "Geschlossen");
+ updateCoursTimer(room.id, "");
+ }
+
+ }
+
+ /**
+ * Updates for all rooms the PC's states
+ * @param json Json with information about the PC's states
+ */
+ function updatePcStates(json) {
+ var l = json.length;
+ for (var i = 0; i < l; i++) {
+ updateRoomUsage(json[i].id, json[i].idle, json[i].occupied, json[i].off, json[i].broken)
+ }
+
+ }
+ /**
+ * Generates a room Object and adds it to the rooms array
+ * @param id ID of the room
+ * @param name Name of the room
+ * @param config Config Json of the room
+ */
+ function addRoom(id, name) {
+ var room = {
+ id: id,
+ name: name,
+ timetable: null,
+ currentEvent: null,
+ nextEventEnd: null,
+ timeTilFree: null,
+ state: null,
+ openingTimes: null,
+ lastCalendarUpdate: null,
+ lastRoomUpdate: null,
+ getState: function () {
+ if (!this.state) {
+ ComputeCurrentState(this);
+ return this.state;
+ }
+ if (this.state.end != "") {
+ if (this.state.end < new MyDate()) {
+ ComputeCurrentState(this);
+ }
+ }
+ return this.state;
+ }
+
+
+ };
+
+ rooms[id] = room;
+
+ if (roomidsString == "") {
+ roomidsString = id;
+ } else {
+ roomidsString = roomidsString + "," + id;
+ }
+ }
+
+
+ /**
+ * computes state of a room, states are:
+ * closed, FreeNoEnd, Free, ClaendarEvent.
+ * @param Room Object
+ */
+ function ComputeCurrentState(room) {
+ if (room.lastRoomUpdate === null) {
+ room.state = {state: 'unknown'};
+ return;
+ }
+ if (!IsOpenNow(room)) {
+ room.state = {state: "closed", end: GetNextOpening(room), titel: "", next: ""};
+
+ return;
+ }
+ var closing = GetNextClosing(room);
+
+ var event = getNextEvent(room.timetable);
+ // no event and no closing
+ if (closing == null && event == null) {
+ room.state = {state: "FreeNoEnd", end: "", titel: "", next: ""};
+ return;
+ }
+
+ // no event so closing is next
+ if (event == null) {
+ room.state = {state: "Free", end: closing, titel: "", next: "closing"};
+ return;
+ }
+
+ // event is at the moment
+ if ((closing == null || event.start.getTime() < closing.getTime()) && event.start.getTime() < new MyDate()) {
+ room.state = {state: "CalendarEvent", end: event.end, titel: event.title, next: ""};
+ return;
+ }
+
+ // no closing so event is next
+ if (closing == null) {
+ room.state = {state: "Free", end: event.start, titel: "", next: "event"};
+ return;
+ }
+
+ // event sooner then closing
+ if (event.start.getTime() < closing) {
+ room.state = {state: "Free", end: event.start, titel: "", next: "event"};
+ } else if (event.start.getTime() > closing) {
+ room.state = {state: "Free", end: closing, titel: "", next: "closing"};
+ }
+ }
+ /**
+ * checks if a room is open
+ * @param room Room object
+ * @returns bool for open or not
+ */
+ function IsOpenNow(room) {
+ var now = new MyDate();
+ if (room.openingTimes == null) {
+
+ // changes from falls needs testing
+ return true;
+ }
+ var tmp = room.openingTimes[now.getDay()];
+ if (tmp == null) {
+ return false;
+ }
+ for (var i = 0; i < tmp.length; i++) {
+ var openDate = new MyDate();
+ openDate.setHours(tmp[i].HourOpen);
+ openDate.setMinutes(tmp[i].MinutesOpen);
+ var closeDate = new MyDate();
+ closeDate.setHours(tmp[i].HourClose);
+ closeDate.setMinutes(tmp[i].MinutesClose);
+ if (openDate < now && closeDate > now) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * returns next event from a given json of events
+ * @param json Json which contains the calendar data.
+ * @returns event next Carlendar Event
+ */
+ function getNextEvent(json) {
+ if (json == null) {
+ return;
+ }
+ var event;
+ var now = new MyDate();
+ for (var i = 0; i < json.length; i++) {
+ //event is now active
+ if (json[i].start.getTime() < now.getTime() && json[i].end.getTime() > now.getTime()) {
+ return json[i];
+ }
+ //first element to consider
+ if (event == null) {
+ if (json[i].start.getTime() > now.getTime()) {
+ event = json[i];
+ }
+ }
+ if (json[i].start.getTime() > now.getTime() && event.start.getTime() > json[i].start.getTime()) {
+ event = json[i];
+ }
+ }
+ return event;
+ }
+
+ /**
+ * Retruns next Opening
+ * @param room Room Object
+ * @returns bestdate Date Object of next opening
+ */
+ function GetNextOpening(room) {
+ var now = new MyDate();
+ var day = now.getDay();
+ var offset = 0;
+ var bestdate;
+ for (var a = 0; a < 7; a++) {
+ if (room.openingTimes == null) {
+ return null;
+ }
+ var tmp = room.openingTimes[day];
+ if (tmp != null) {
+ for (var i = 0; i < tmp.length; i++) {
+ var openDate = new MyDate();
+ openDate.setDate(now.getDate() + offset);
+ openDate.setHours(tmp[i].HourOpen);
+ openDate.setMinutes(tmp[i].MinutesOpen);
+ if (openDate > now) {
+ if (!IsOpen(new Date(openDate.getTime() - 60000))) {
+ if (bestdate == null || bestdate > openDate) {
+ bestdate = openDate;
+ }
+ }
+ }
+ }
+ }
+ offset++;
+ day++;
+ if (day > 6) {
+ day = 0;
+ }
+ }
+ return bestdate;
+ }
+
+ /**
+ * returns next closing time of a given room
+ * @param room
+ * @returns Date Object of next closing
+ */
+ function GetNextClosing(room) {
+ var now = new MyDate();
+ var day = now.getDay();
+ var offset = 0;
+ var bestdate;
+ for (var a = 0; a < 7; a++) {
+ //Test
+ if (room.openingTimes === null) {
+ return null;
+ }
+ var tmp = room.openingTimes[day];
+ if (tmp != null) {
+ for (var i = 0; i < tmp.length; i++) {
+ var closeDate = new MyDate();
+ closeDate.setDate(now.getDate() + offset);
+ closeDate.setHours(tmp[i].HourClose);
+ closeDate.setMinutes(tmp[i].MinutesClose);
+ if (closeDate > now) {
+ if (!IsOpen(new Date(closeDate.getTime() + 60000))) {
+ if (bestdate == null || bestdate > closeDate) {
+ bestdate = closeDate;
+ }
+ }
+ }
+ }
+ }
+ offset++;
+ day++;
+ if (day > 6) {
+ day = 0;
+ }
+ }
+ return bestdate;
+ }
+
+ /**
+ * Updates the Course Text of a child
+ * @param id of the child
+ * @param idle PC's on
+ * @param occupied PC's used
+ * @param off PC's that are off
+ * @param broken PC's that are broken
+ */
+ function updateRoomUsage(id, idle, occupied, off, broken) {
+ if (idle == 0 && occupied == 0 && off == 0) {
+ $('#parent_' + id).parent().hide();
+ return;
+ }
+ $('#parent_' + id).parent().show();
+ var total = parseInt(idle) + parseInt(occupied) + parseInt(off) + parseInt(broken);
+ $("#pc_Idle_" + id).text(idle).width((idle / total) * 100 + '%');
+ $("#pc_Occupied_" + id).text(occupied).width((occupied / total) * 100 + '%');
+ $("#pc_Off_" + id).text(off).width((off / total) * 100 + '%');
+ $("#pc_Broken_" + id).text(broken).width((broken / total) * 100 + '%');
+ }
+
+ /**
+ * Updates the Course Text of a child
+ * @param id of the child
+ * @param text Text
+ */
+ function updateCourseText(id, text) {
+ $("#div_course" + id).text(text);
+ }
+
+ /**
+ * Updates the Course time of a child
+ * @param id of the child
+ * @param time Time value
+ */
+ function updateCoursTimer(id, time) {
+ $("#div_Time_" + id).text(time);
+ }
+
+ /**
+ * generates a Div, used for a child node
+ * @param target Div it should be inserted
+ * @param id ID of the Object it represents
+ * @param name Name of the Object it represents
+ * @param outermost if the object is a root node
+ * @returns generated div
+ */
+ function generateChild(target, id, name, outermost) {
+
+ var c = "";
+ if (outermost) {
+ c = "outermost";
+ }
+
+ var text = "<div class='border " + c + "'>" +
+ "<div class='child paperEffect' id='parent_" + id + "'>" +
+ "<div class='headerFont'>" + name + "</div>" +
+ "<div class='pc-state-wrapper'>" +
+ "<div id = 'pc_Occupied_" + id + "' class='pc-occupied'>?</div>" +
+ "<div id = 'pc_Idle_" + id + "' class='pc-idle'>?</div>" +
+ "<div id = 'pc_Off_" + id + "' class='pc-off'>?</div>" +
+ "<div id = 'pc_Broken_" + id + "' class='pc-broken'>?</div>" +
+ "</div>" +
+ "<div class='aroundCourse'>" +
+ "<div id = 'div_course" + id + "'class='courseFont'>?</div>" +
+ "<div id = 'div_Time_" + id + "'class='courseFont'></div></div></div></div>";
+ var obj = $(target).append(text);
+ addRoom(id, name);
+ return obj
+
+ }
+
+ /**
+ * generates a Div, used for a parent node
+ * @param target Div it should be inserted
+ * @param id ID of the Object it represents
+ * @param name Name of the Object it represents
+ * @param outermost if the object is a root node
+ * @returns generated div
+ */
+ function generateParent(target, id, name, outermost) {
+ var c = "";
+ if (outermost) {
+ c = "outermost";
+ }
+
+ var text = "<div class='border " + c + "'>" +
+ "<div class='parent paperEffect'>" +
+ "<div class='headerFont'>" + name + "</div>" +
+ "<div id='parent_" + id + "'></div>" +
+ "</div></div>";
+ return $(target).append(text);
+ }
+
+ /**
+ * returns parameter value from the url
+ * @param sParam
+ * @returns value for given parameter
+ */
+ var getUrlParameter = function getUrlParameter(sParam) {
+ var sPageURL = decodeURIComponent(window.location.search.substring(1)),
+ sURLVariables = sPageURL.split('&'),
+ sParameterName,
+ i;
+
+ for (i = 0; i < sURLVariables.length; i++) {
+ sParameterName = sURLVariables[i].split('=');
+
+ if (sParameterName[0] === sParam) {
+ return sParameterName[1] === undefined ? true : sParameterName[1];
+ }
+ }
+ };
+
+
+ /**
+ * querys the Calendar data
+ * @param ids ID'S of rooms to query as string, for e.g.: "5,17,8" or "5"
+ */
+ function queryCalendars(ids) {
+ var url = "../../../api.php?do=locationinfo&action=calendar&id=" + ids;
+
+ // Todo reimplement Frontend methode if needed
+ /*
+ if(!(room.config.calendarqueryurl === undefined)) {
+ url = room.config.calendarqueryurl;
+ }
+ */
+ $.ajax({
+ url: url,
+ dataType: 'json',
+ cache: false,
+ timeout: 30000,
+ success: function (result) {
+ UpdateTimeTables(result);
+
+
+ }, error: function () {
+
+ }
+ });
+ }
+
+
+ /**
+ * used for countdown
+ * computes the time difference between 2 Date objects
+ * @param a Date Object
+ * @param b Date Object
+ * @returns time string
+ */
+ function GetTimeDiferenceAsString(a, b) {
+ if (a == null || b == null) {
+ return "";
+ }
+ var milliseconds = a.getTime() - b.getTime();
+ var seconds = Math.floor((milliseconds / 1000) % 60);
+ milliseconds -= seconds * 1000;
+ var minutes = Math.floor((milliseconds / (1000 * 60)) % 60);
+ milliseconds -= minutes * 1000 * 60;
+ var hours = Math.floor((milliseconds / (1000 * 60 * 60)) % 24);
+
+ var days = Math.floor((milliseconds / (1000 * 60 * 60 * 24)) % 31);
+ if (seconds < 10) {
+ seconds = "0" + seconds;
+ }
+ if (minutes < 10) {
+ minutes = "0" + minutes;
+ }
+ if (days != 0) {
+ // dont show?
+ return "";
+ }
+ return hours + ":" + minutes + ":" + seconds;
+ }
+ </script>
+</head>
+<body>
+<div id="main"></div>
+</body>
+</html>
diff --git a/modules-available/locationinfo/templates/page-config-panel-default.html b/modules-available/locationinfo/templates/page-config-panel-default.html
new file mode 100644
index 00000000..b55e3d4d
--- /dev/null
+++ b/modules-available/locationinfo/templates/page-config-panel-default.html
@@ -0,0 +1,418 @@
+<h2>
+ {{#new}}{{lang_createPanel}}{{/new}}
+ {{^new}}{{lang_editPanel}}{{/new}}
+</h2>
+
+<p>{{lang_editDefaultPanelHints}}</p>
+
+<form method="post" action="?do=locationinfo" id="config-form">
+ <input type="hidden" name="token" value="{{token}}">
+ <input type="hidden" name="action" value="writePanelConfig">
+ <input type="hidden" name="ptype" value="DEFAULT">
+ <input type="hidden" name="uuid" value="{{uuid}}">
+
+ <div class="row">
+
+ <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-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">
+ <a class="btn btn-default helptext" title="{{lang_displayNameTooltip}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ </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">
+ <a class="btn btn-default helptext" title="{{lang_languageTooltip}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ </div>
+ </div>
+ </div>
+
+ <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()">
+ <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">
+ <a class="btn btn-default helptext" title="{{lang_modeTooltip}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ </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">
+ <input id="input-eco" type="checkbox" name="eco" {{eco_checked}}>
+ </div>
+ <div class="col-sm-2">
+ <a class="btn btn-default helptext" title="{{lang_ecoTooltip}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ </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>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <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-item">
+ <div class="row">
+ <div class="col-sm-3">
+ <label for="input-calupdate">{{lang_calendar}}</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">
+ <a class="btn btn-default helptext" title="{{lang_calupdateTooltip}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ </div>
+ </div>
+ </div>
+
+ <div class="list-group-item">
+ <div class="row">
+ <div class="col-sm-3">
+ <label for="form-roomupdate">{{lang_room}}</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">
+ <a class="btn btn-default helptext" title="{{lang_roomupdateTooltip}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ </div>
+ </div>
+ </div>
+
+ </div>
+ </div>
+ </div>
+ </div>
+
+ </div>
+
+ <div class="modify-inputs">
+ <div class="row">
+ <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">
+
+ <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" type="checkbox" name="vertical" {{vertical_checked}}>
+ </div>
+ <div class="col-sm-2">
+ <a class="btn btn-default helptext" title="{{lang_verticalTooltip}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ </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">
+ <input id="scaledaysauto" type="checkbox" name="scaledaysauto" {{scaledaysauto_checked}}>
+ </div>
+ <div class="col-sm-2">
+ <a class="btn btn-default helptext" title="{{lang_autoscaleTooltip}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ </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">
+ <a class="btn btn-default helptext" title="{{lang_daysToShowTooltip}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ </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">
+ <a class="btn btn-default helptext" title="{{lang_rotationTooltip}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ </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">
+ <a class="btn btn-default helptext" title="{{lang_scaleTooltip}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ </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">
+ <a class="btn btn-default helptext" title="{{lang_switchTimeTooltip}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="col-md-6">
+ <div class="panel panel-default">
+ <div class="panel-heading">{{lang_locations}}</div>
+ <div class="panel-body">
+ <input type="hidden" name="locationids" value="{{locationids}}" id="locationids">
+ <p>{{lang_fourLocsHint}}</p>
+ <ul id="selected-locations" class="list-unstyled">
+
+ </ul>
+ <div class="dropdown pull-right">
+ <button type="button" class="btn btn-success dropdown-toggle" data-toggle="dropdown">
+ <span class="glyphicon glyphicon-plus"></span>
+ </button>
+ <ul class="dropdown-menu" id="location-list">
+ {{#locations}}
+ <li><a href="#" data-lid="{{locationid}}">{{locationpad}} <span class="name">{{locationname}}</span></a></li>
+ {{/locations}}
+ </ul>
+ </div>
+ <div class="clearfix"></div>
+ </div>
+ </div>
+ </div>
+
+ </div>
+ </div>
+ <button type="submit" class="btn btn-primary">{{lang_save}}</button>
+ <a href="?do=locationinfo&amp;show=panels" class="btn btn-default">{{lang_cancel}}</a>
+</form>
+
+<div class="modal fade" id="no-locations-message" tabindex="-1" role="dialog">
+ <div class="modal-dialog"> <!--style="min-width:600px;width:70%"-->
+
+ <div class="modal-content">
+ <div class="modal-header">{{lang_error}}</div>
+ <div class="modal-body">
+ {{lang_noLocationsWarning}}
+ </div>
+ <div class="modal-footer">
+ <a class="btn btn-primary pull-right" data-dismiss="modal">{{lang_close}}</a>
+ <div class="clearfix"></div>
+ </div>
+ </div>
+
+ </div>
+</div>
+
+<script type="text/javascript"><!--
+
+document.addEventListener("DOMContentLoaded", function () {
+ var $selLocs = $('#selected-locations');
+ var $locList = $('#location-list');
+ var $locInput = $('#locationids');
+
+ // Initialize fancy tooltips
+ $('a.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'})
+ .on('switchChange.bootstrapSwitch', function () {
+ if (this.name === 'scaledaysauto') {
+ $('#daystoshow').prop('disabled', this.checked);
+ }
+ });
+
+ var lids = $locInput.val().split(',');
+ $selLocs.empty();
+ for (var i = 0; i < lids.length; ++i) {
+ var $name = $locList.find('a[data-lid="' + lids[i] + '"] .name');
+ if ($name.length === 0) continue;
+ addLocation(lids[i], $name.text());
+ }
+
+ $('#daystoshow option[value="{{daystoshow}}"]').attr("selected", "selected");
+ $('#rotation option[value="{{rotation}}"]').attr("selected", "selected");
+ $('#mode option[value="{{mode}}"]').attr("selected", "selected");
+
+ $('#daystoshow').prop('disabled', document.getElementById('scaledaysauto').checked);
+
+ $('input[type="range"]').change();
+ modeChange();
+
+ // Adding/removing locations
+ $locList.find('a').click(function(ev) {
+ ev.preventDefault();
+ var $this = $(this);
+ var name = $this.find('.name').text();
+ var id = $this.data('lid');
+ addLocation(id, name);
+ serializeLocs();
+ });
+
+ $('#config-form').submit(function(ev) {
+ if ($locInput.val().length > 0)
+ return;
+ ev.preventDefault();
+ $('#no-locations-message').modal('show');
+ });
+
+ function addLocation(id, name) {
+ $selLocs.find('li[data-lid="' + id + '"]').remove();
+ var delButton = $('<button class="btn btn-danger btn-xs" type="button">').append($('<span class="glyphicon glyphicon-remove">')).click(delParent);
+ $selLocs.append($('<li>').attr('data-lid', id).text(name).prepend(delButton));
+ }
+
+ function delParent() {
+ $(this).parent().remove();
+ serializeLocs();
+ }
+
+ function serializeLocs() {
+ var res = $selLocs.find('li[data-lid]').map( function() {
+ return $(this).data('lid');
+ }).get().join(',');
+ $locInput.val(res);
+ }
+
+});
+
+/**
+ * If the mode was changed the mode settings have to be adjusted.
+ */
+function modeChange() {
+ var value = $('#mode').val();
+ $('.m' + value + '-h').hide();
+ $('.m' + value + '-s').show();
+}
+
+//--></script>
diff --git a/modules-available/locationinfo/templates/page-config-panel-url.html b/modules-available/locationinfo/templates/page-config-panel-url.html
new file mode 100644
index 00000000..401214bd
--- /dev/null
+++ b/modules-available/locationinfo/templates/page-config-panel-url.html
@@ -0,0 +1,81 @@
+<h2>
+ {{#new}}{{lang_createPanel}}{{/new}}
+ {{^new}}{{lang_editPanel}}{{/new}}
+</h2>
+
+<p>{{lang_editUrlPanelHints}}</p>
+
+<form method="post" action="?do=locationinfo" id="config-form">
+ <input type="hidden" name="token" value="{{token}}">
+ <input type="hidden" name="action" value="writePanelConfig">
+ <input type="hidden" name="ptype" value="URL">
+ <input type="hidden" name="uuid" value="{{uuid}}">
+
+ <div class="panel panel-default">
+ <div class="panel-heading">{{lang_display}}</div>
+ <div class="panel-body">
+ <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">
+ <a class="btn btn-default helptext" title="{{lang_displayNameTooltip}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ </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">
+ <a class="btn btn-default helptext" title="{{lang_urlTooltip}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ </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">
+ <input id="input-ssl" type="checkbox" name="insecure-ssl" {{ssl_checked}} value="1">
+ </div>
+ <div class="col-sm-2">
+ <a class="btn btn-default helptext" title="{{lang_ignoreSslTooltip}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <button type="submit" class="btn btn-primary">{{lang_save}}</button>
+ <a href="?do=locationinfo&amp;show=panels" class="btn btn-default">{{lang_cancel}}</a>
+</form>
+
+<script type="text/javascript"><!--
+
+document.addEventListener("DOMContentLoaded", function () {
+
+});
+
+//--></script>
diff --git a/modules-available/locationinfo/templates/page-locations.html b/modules-available/locationinfo/templates/page-locations.html
new file mode 100644
index 00000000..3eafa7bf
--- /dev/null
+++ b/modules-available/locationinfo/templates/page-locations.html
@@ -0,0 +1,100 @@
+<h2>{{lang_locationsTable}}</h2>
+
+<p>{{lang_locationsTableHints}}</p>
+
+<table class="table table-condensed table-hover">
+ <thead>
+ <tr>
+ <th>{{lang_locationName}}</th>
+ <th>{{lang_backend}}</th>
+ <th>{{lang_openingtimes}}</th>
+ </tr>
+ </thead>
+
+ <tbody>
+ {{#list}}
+ <tr>
+ <td>
+ <div style="display:inline-block;width:{{depth}}em"></div>
+ <a href="#" class="loc-name {{^depth}}slx-bold{{/depth}}" data-locationid="{{locationid}}">
+ {{locationname}}
+ <span class="glyphicon glyphicon-edit"></span>
+ </a>
+ </td>
+ <td>
+ {{backend}}
+ </td>
+ <td>
+ <span class="glyphicon glyphicon-{{openingGlyph}}"></span>
+ </td>
+ </tr>
+ {{/list}}
+ </tbody>
+</table>
+
+<div class="modal fade" id="location-modal" tabindex="-1" role="dialog">
+ <div class="modal-dialog"> <!--style="min-width:600px;width:70%"-->
+
+ <div class="modal-content">
+ <form method="post" action="?do=locationinfo" id="settings-form">
+ <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-body"></div>
+ <div class="modal-footer">
+ <button type="submit" class="btn btn-primary">{{lang_save}}</button>
+ <a class="btn btn-primary" data-dismiss="modal">{{lang_close}}</a>
+ </div>
+ </form>
+ </div>
+
+ </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-9 days-box">
+ <label><input type="checkbox" class="i-Monday">{{lang_shortMonday}}</label> |
+ <label><input type="checkbox" class="i-Tuesday">{{lang_shortTuesday}}</label> |
+ <label><input type="checkbox" class="i-Wednesday">{{lang_shortWednesday}}</label> |
+ <label><input type="checkbox" class="i-Thursday">{{lang_shortThursday}}</label> |
+ <label><input type="checkbox" class="i-Friday">{{lang_shortFriday}}</label> |
+ <label><input type="checkbox" class="i-Saturday">{{lang_shortSaturday}}</label> |
+ <label><input type="checkbox" class="i-Sunday">{{lang_shortSunday}}</label>
+ </div>
+ <div class="col-xs-3 text-right">
+ <label><input type="checkbox" class="i-delete"><span class="glyphicon glyphicon-trash"></span></label>
+ </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"><!--
+
+document.addEventListener("DOMContentLoaded", function () {
+ /**
+ * Load a opening time modal of a location.
+ */
+ $('.loc-name').click(function (e) {
+ e.preventDefault();
+ 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);
+ });
+ $('#settings-form').submit(submitLocationSettings);
+});
+
+//--></script> \ No newline at end of file
diff --git a/modules-available/locationinfo/templates/page-panels.html b/modules-available/locationinfo/templates/page-panels.html
new file mode 100644
index 00000000..f374e456
--- /dev/null
+++ b/modules-available/locationinfo/templates/page-panels.html
@@ -0,0 +1,74 @@
+<h2>{{lang_panelsTable}}</h2>
+
+<p>{{lang_panelsTableHints}}</p>
+
+<table class="table table-hover">
+ <thead>
+ <tr>
+ <th>{{lang_panel}}</th>
+ <th>{{lang_panelType}}</th>
+ <th>{{lang_locations}}</th>
+ {{#hasRunmode}}
+ <th class="slx-smallcol">{{lang_runmodeTHead}}</th>
+ {{/hasRunmode}}
+ <th class="slx-smallcol"></th>
+ <th class="slx-smallcol"></th>
+ </tr>
+ </thead>
+ <tbody>
+ <form method="post" action="?do=locationinfo" onsubmit="return confirm('{{lang_areYouSure}}')">
+ <input type="hidden" name="token" value="{{token}}">
+ <input type="hidden" name="action" value="deletePanel">
+ {{#panels}}
+ <tr>
+ <td>
+ <a href="/panel/{{paneluuid}}" target="_blank">{{panelname}}</a>
+ </td>
+ <td>
+ {{paneltype}}
+ </td>
+ <td>
+ {{#locationurl}}<a href="{{locationurl}}" target="_blank">{{/locationurl}}
+ {{locations}}
+ {{#locationurl}}</a>{{/locationurl}}
+ </td>
+ {{#hasRunmode}}
+ <td>
+ <a class="btn btn-default btn-xs" href="?do=runmode&amp;module=locationinfo&amp;modeid={{paneluuid}}&amp;redirect=?do=locationinfo">
+ <span class="glyphicon glyphicon-edit"></span>
+ </a>
+ {{assignedMachineCount}}
+ </td>
+ {{/hasRunmode}}
+ <td>
+ <a class="btn btn-default btn-xs" href="?do=locationinfo&amp;show=edit-panel&amp;uuid={{paneluuid}}">
+ <span class="glyphicon glyphicon-cog"></span>
+ {{lang_edit}}
+ </a>
+ </td>
+ <td>
+ <button type="submit" name="uuid" value="{{paneluuid}}" class="btn btn-danger btn-xs">
+ <span class="glyphicon glyphicon-trash"></span>
+ {{lang_delete}}
+ </button>
+ </td>
+ </tr>
+ {{/panels}}
+ </form>
+ </tbody>
+</table>
+
+<div>
+ <a class="btn btn-sm btn-success" href="?do=locationinfo&amp;show=edit-panel&amp;uuid=new-default">
+ <span class="glyphicon glyphicon-plus"></span>
+ {{lang_defaultPanel}}
+ </a>
+ <a class="btn btn-sm btn-success" href="?do=locationinfo&amp;show=edit-panel&amp;uuid=new-summary">
+ <span class="glyphicon glyphicon-plus"></span>
+ {{lang_summaryPanel}}
+ </a>
+ <a class="btn btn-sm btn-success" href="?do=locationinfo&amp;show=edit-panel&amp;uuid=new-url">
+ <span class="glyphicon glyphicon-plus"></span>
+ {{lang_urlPanel}}
+ </a>
+</div> \ No newline at end of file
diff --git a/modules-available/locationinfo/templates/page-servers.html b/modules-available/locationinfo/templates/page-servers.html
new file mode 100644
index 00000000..eefb1ed5
--- /dev/null
+++ b/modules-available/locationinfo/templates/page-servers.html
@@ -0,0 +1,116 @@
+<h2>{{lang_serverTable}}</h2>
+
+<p>{{lang_serverTableHints}}</p>
+
+<table class="table table-hover">
+ <thead>
+ <tr>
+ <th width="1">{{lang_serverType}}</th>
+ <th>{{lang_locationName}}</th>
+ <th width="1"></th>
+ <th width="1"></th>
+ </tr>
+ </thead>
+ <tbody>
+ {{#serverlist}}
+ <form method="post" action="?do=locationinfo">
+ <input type="hidden" name="token" value="{{token}}">
+ <input type="hidden" name="serverid" value="{{serverid}}">
+ <tr>
+ <td nowrap>{{typename}}</td>
+ <td nowrap>{{servername}}</td>
+
+ <td align="center" nowrap>
+ <button class="btn btn-xs {{^autherror}}btn-default{{/autherror}}{{#autherror}}btn-danger{{/autherror}}"
+ data-server-edit="{{serverid}}" {{disabled}} type="button">
+ <span class="glyphicon glyphicon-cog"></span>
+ {{lang_edit}}
+ </button>
+ <button class="btn btn-xs btn-primary server-check" {{disabled}} name="action" value="checkConnection"
+ type="submit">
+ <span class="glyphicon glyphicon-refresh"></span>
+ {{lang_checkConnection}}
+ </button>
+ </td>
+ <td align="center" nowrap>
+ <button class="btn btn-xs btn-danger server-delete" type="submit" name="action" value="deleteServer">
+ <span class="glyphicon glyphicon-trash"></span>
+ {{lang_delete}}
+ </button>
+ </td>
+ </tr>
+ </form>
+ {{/serverlist}}
+ </tbody>
+</table>
+
+<div>
+ <button class="btn btn-sm btn-success" id="addServerButton" onclick="addServer()">
+ <span class="glyphicon glyphicon-plus"></span>
+ {{lang_addServer}}
+ </button>
+</div>
+
+<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-body" id="myModalBody"></div>
+ <div class="modal-footer">
+ <button type="submit" id="myModalSubmitButton" class="btn btn-primary" form="">{{lang_save}}</button>
+
+ <a class="btn btn-primary" data-dismiss="modal">{{lang_close}}</a>
+ </div>
+ </div>
+
+ </div>
+</div>
+
+<script type="text/javascript"><!--
+
+ document.addEventListener("DOMContentLoaded", function () {
+
+ /**
+ * Confirm deleting a server.
+ */
+ $('.server-delete').click(function(ev) {
+ var del = confirm("{{lang_deleteConfirmation}}");
+ if (!del) ev.preventDefault();
+ });
+
+ /**
+ * Animate refresh icon while page is loading
+ */
+ $('.server-check').click(function() {
+ $(this).find('.glyphicon').addClass('slx-rotation');
+ });
+
+ $('button[data-server-edit]').click(function() {
+ var id = $(this).data('server-edit');
+ loadServerSettingsModal(id);
+ });
+
+ });
+
+ /**
+ * Loads the settings modal of a server.
+ *
+ * @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);
+ }
+
+ // ########### Server Table ###########
+
+ /**
+ * Loads a new / empty server settings modal.
+ */
+ function addServer() {
+ loadServerSettingsModal(0);
+ }
+//--></script>
diff --git a/modules-available/locationinfo/templates/page-tabs.html b/modules-available/locationinfo/templates/page-tabs.html
new file mode 100644
index 00000000..ed3f01fd
--- /dev/null
+++ b/modules-available/locationinfo/templates/page-tabs.html
@@ -0,0 +1,6 @@
+<ul class="nav nav-tabs">
+ <li class="{{class-}}"><a href="?do=locationinfo">{{lang_panels}}</a></li>
+ <li class="{{class-locations}}"><a href="?do=locationinfo&amp;show=locations">{{lang_locationSettings}}</a></li>
+ <li class="{{class-backends}}"><a href="?do=locationinfo&amp;show=backends">{{lang_backends}}</a></li>
+</ul>
+<br> \ No newline at end of file
diff --git a/modules-available/locationinfo/templates/server-prop-bool.html b/modules-available/locationinfo/templates/server-prop-bool.html
new file mode 100644
index 00000000..f430d02c
--- /dev/null
+++ b/modules-available/locationinfo/templates/server-prop-bool.html
@@ -0,0 +1,16 @@
+<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">
+ <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}}
+ <a class="btn btn-default" title="{{helptext}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ {{/helptext}}
+ </div>
+ </div>
+</div> \ No newline at end of file
diff --git a/modules-available/locationinfo/templates/server-prop-dropdown.html b/modules-available/locationinfo/templates/server-prop-dropdown.html
new file mode 100644
index 00000000..80667766
--- /dev/null
+++ b/modules-available/locationinfo/templates/server-prop-dropdown.html
@@ -0,0 +1,19 @@
+<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">
+ <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}}
+ <a class="btn btn-default" title="{{helptext}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ {{/helptext}}
+ </div>
+ </div>
+</div> \ No newline at end of file
diff --git a/modules-available/locationinfo/templates/server-prop-generic.html b/modules-available/locationinfo/templates/server-prop-generic.html
new file mode 100644
index 00000000..9e94c23d
--- /dev/null
+++ b/modules-available/locationinfo/templates/server-prop-generic.html
@@ -0,0 +1,16 @@
+<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">
+ <input class="form-control" id="prop-{{property}}" type="{{inputtype}}" name="prop-{{property}}"
+ value="{{currentvalue}}">
+ </div>
+ <div class="col-md-2">
+ {{#helptext}}
+ <a class="btn btn-default" title="{{helptext}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ {{/helptext}}
+ </div>
+ </div>
+</div> \ No newline at end of file
diff --git a/modules-available/locations/baseconfig/getconfig.inc.php b/modules-available/locations/baseconfig/getconfig.inc.php
index 3a0c918f..f21503f1 100644
--- a/modules-available/locations/baseconfig/getconfig.inc.php
+++ b/modules-available/locations/baseconfig/getconfig.inc.php
@@ -17,7 +17,7 @@ $matchingLocations = array();
if ($locationId !== false) {
// Get all parents
$matchingLocations = Location::getLocationRootChain($locationId);
- $configVars["SLX_LOCATIONS"] = implode(' ', $matchingLocations);
+ ConfigHolder::add("SLX_LOCATIONS", implode(' ', $matchingLocations), 100);
}
// Query location specific settings (from bottom to top)
@@ -31,14 +31,15 @@ if (!empty($matchingLocations)) {
}
// $matchingLocations contains the location ids sorted from closest to furthest, so we use it to make sure the order
// in which they override is correct (closest setting wins, e.g. room setting beats department setting)
+ $prio = count($matchingLocations) + 1;
foreach ($matchingLocations as $lid) {
if (!isset($tmp[$lid]))
continue;
+ ConfigHolder::setContext('location-' . $lid);
foreach ($tmp[$lid] as $setting => $value) {
- if (!isset($configVars[$setting])) {
- $configVars[$setting] = $value;
- }
+ ConfigHolder::add($setting, $value, $prio);
}
+ $prio -= 1;
}
unset($tmp);
}
diff --git a/modules-available/locations/baseconfig/hook.json b/modules-available/locations/baseconfig/hook.json
index c295e0f6..b7b3581b 100644
--- a/modules-available/locations/baseconfig/hook.json
+++ b/modules-available/locations/baseconfig/hook.json
@@ -1,5 +1,6 @@
{
"table": "setting_location",
"field": "locationid",
- "tostring": "Location::getName"
+ "tostring": "Location::getName",
+ "getfallback": "Location::getBaseconfigParent"
} \ No newline at end of file
diff --git a/modules-available/locations/inc/location.inc.php b/modules-available/locations/inc/location.inc.php
index 3c9775ef..8b2ef262 100644
--- a/modules-available/locations/inc/location.inc.php
+++ b/modules-available/locations/inc/location.inc.php
@@ -27,6 +27,11 @@ class Location
return $rows;
}
+ /**
+ * Return row from location table for $locationId.
+ * @param $locationId
+ * @return array|bool row from DB, false if not found
+ */
public static function get($locationId)
{
return Database::queryFirst("SELECT * FROM location WHERE locationid = :locationId", compact('locationId'));
@@ -61,12 +66,18 @@ class Location
'locationid' => (int)$node['locationid'],
'parentlocationid' => (int)$node['parentlocationid'],
'parents' => $parents,
+ 'children' => empty($node['children']) ? array() : array_map(function ($item) { return $item['locationid']; }, $node['children']),
'locationname' => $node['locationname'],
'depth' => $depth,
'isleaf' => true,
);
if (!empty($node['children'])) {
- $output += self::flattenTreeAssoc($node['children'], array_merge($parents, array((int)$node['locationid'])), $depth + 1);
+ $childNodes = self::flattenTreeAssoc($node['children'], array_merge($parents, array((int)$node['locationid'])), $depth + 1);
+ $output[(int)$node['locationid']]['children'] = array_merge($output[(int)$node['locationid']]['children'],
+ array_reduce($childNodes, function ($carry, $item) {
+ return array_merge($carry, $item['children']);
+ }, array()));
+ $output += $childNodes;
}
}
foreach ($output as &$entry) {
@@ -119,6 +130,47 @@ class Location
return array_values($rows);
}
+ /**
+ * Get nested array of all the locations and children of given locationid(s).
+ *
+ * @param int[]|int $idList List of location ids
+ * @param bool $locationTree used in recursive calls, don't pass
+ * @return array list of passed locations plus their children
+ */
+ public static function getRecursive($idList, $locationTree = false)
+ {
+ if (!is_array($idList)) {
+ $idList = array($idList);
+ }
+ if ($locationTree === false) {
+ $locationTree = self::getTree();
+ }
+ $ret = array();
+ foreach ($locationTree as $location) {
+ if (in_array($location['locationid'], $idList)) {
+ $ret[] = $location;
+ } elseif (!empty($location['children'])) {
+ $ret = array_merge($ret, self::getRecursive($idList, $location['children']));
+ }
+ }
+ return $ret;
+ }
+
+ /**
+ * Get flat array of all the locations and children of given locationid(s).
+ *
+ * @param int[]|int $idList List of location ids
+ * @return array list of passed locations plus their children
+ */
+ public static function getRecursiveFlat($idList)
+ {
+ $ret = self::getRecursive($idList);
+ if (!empty($ret)) {
+ $ret = self::flattenTree($ret);
+ }
+ return $ret;
+ }
+
public static function buildTree($elements, $parentId = 0)
{
$branch = array();
@@ -184,7 +236,7 @@ class Location
/**
* Get location id for given machine (by uuid)
- * @param $uuid machine uuid
+ * @param string $uuid machine uuid
* @return bool|int locationid, false if no match
*/
public static function getFromMachineUuid($uuid)
@@ -225,8 +277,8 @@ class Location
* client. We can't trust the UUID too much as it is provided by the
* client, so if it seems too fishy, the UUID will be ignored.
*
- * @param $ip IP address of client
- * @param $uuid System-UUID of client
+ * @param string $ip IP address of client
+ * @param string $uuid System-UUID of client
* @return int|bool location id, or false if none matches
*/
public static function getFromIpAndUuid($ip, $uuid)
@@ -243,8 +295,8 @@ class Location
// location determined by the uuid
$uuidLocations = self::getLocationRootChain($uuidLoc);
$ipLocations = self::getLocationRootChain($ipLoc);
- if (in_array($uuidLoc, $ipLocations)
- || (in_array($ipLoc, $uuidLocations) && count($ipLocations) + 1 >= count($uuidLocations))
+ if (in_array($uuidLoc, $ipLocations) // UUID loc is further up, OK
+ || (in_array($ipLoc, $uuidLocations) && count($ipLocations) + 1 >= count($uuidLocations)) // UUID is max one level deeper than IP loc, accept as well
) {
// Close enough, allow
$locationId = $uuidLoc;
@@ -280,6 +332,22 @@ class Location
}
/**
+ * @param $locationId
+ * @return bool|array ('value' => x, 'display' => y), false if no parent or unknown id
+ */
+ public static function getBaseconfigParent($locationId)
+ {
+ settype($locationId, 'integer');
+ $locations = Location::getLocationsAssoc();
+ if (!isset($locations[$locationId]))
+ return false;
+ $locationId = (int)$locations[$locationId]['parentlocationid'];
+ if (!isset($locations[$locationId]))
+ return false;
+ return array('value' => $locationId, 'display' => $locations[$locationId]['locationname']);
+ }
+
+ /**
* @return array list of subnets as numeric array
*/
public static function getSubnets()
@@ -385,7 +453,7 @@ class Location
* If two+ subnets match and have the same depth and size, a
* random one will be returned.
*
- * @param $ip IP to look up
+ * @param string $ip IP to look up
* @return bool|int locationid ip matches, false = no match
*/
public static function mapIpToLocation($ip)
diff --git a/modules-available/locations/templates/location-subnets.html b/modules-available/locations/templates/location-subnets.html
index 4eadeffd..b8a2b091 100644
--- a/modules-available/locations/templates/location-subnets.html
+++ b/modules-available/locations/templates/location-subnets.html
@@ -61,9 +61,8 @@
<br>
<div class="btn-group">
{{#roomplanner}}
- <a class="btn btn-default" href="?do=roomplanner&amp;locationid={{locationid}}"
- target="_blank"
- >
+ <a class="btn btn-default" href="?do=roomplanner&amp;locationid={{locationid}}" target="_blank"
+ onclick="window.open(this.href, '_blank', 'toolbar=0,scrollbars,resizable');return false">
<span class="glyphicon glyphicon-move"></span>{{lang_editRoomplan}}
</a>
{{/roomplanner}}
diff --git a/modules-available/main/category-icons.json b/modules-available/main/category-icons.json
index 08cb2a64..f790089e 100644
--- a/modules-available/main/category-icons.json
+++ b/modules-available/main/category-icons.json
@@ -4,5 +4,6 @@
"settings-client":"cog",
"settings-server":"cog",
"status":"tasks",
- "users":"user"
+ "users":"user",
+ "beta":"flash"
}
diff --git a/modules-available/main/lang/de/categories.json b/modules-available/main/lang/de/categories.json
index 8fd292ec..71f149ec 100644
--- a/modules-available/main/lang/de/categories.json
+++ b/modules-available/main/lang/de/categories.json
@@ -1,4 +1,5 @@
{
+ "beta": "Beta",
"content": "Inhalt",
"settings-client": "Einstellungen (Client)",
"settings-server": "Einstellungen (Server)",
diff --git a/modules-available/main/lang/de/global-tags.json b/modules-available/main/lang/de/global-tags.json
index 6664caac..451e016e 100644
--- a/modules-available/main/lang/de/global-tags.json
+++ b/modules-available/main/lang/de/global-tags.json
@@ -8,8 +8,10 @@
"lang_hint": "Hinweis",
"lang_hours": "Stunde(n)",
"lang_next": "Weiter",
+ "lang_no": "Nein",
"lang_reset": "Zur\u00fccksetzen",
"lang_save": "Speichern",
"lang_today": "Heute",
+ "lang_yes": "Ja",
"lang_yesterday": "Gestern"
} \ No newline at end of file
diff --git a/modules-available/main/lang/en/categories.json b/modules-available/main/lang/en/categories.json
index defbd652..9dfa0404 100644
--- a/modules-available/main/lang/en/categories.json
+++ b/modules-available/main/lang/en/categories.json
@@ -1,4 +1,5 @@
{
+ "beta": "Beta",
"content": "Content",
"settings-client": "Settings (Client)",
"settings-server": "Settings (Server)",
diff --git a/modules-available/main/lang/en/global-tags.json b/modules-available/main/lang/en/global-tags.json
index 26cbdfde..bfadf2f2 100644
--- a/modules-available/main/lang/en/global-tags.json
+++ b/modules-available/main/lang/en/global-tags.json
@@ -8,8 +8,10 @@
"lang_hint": "Hint",
"lang_hours": "hour(s)",
"lang_next": "Next",
+ "lang_no": "No",
"lang_reset": "Reset",
"lang_save": "Save",
"lang_today": "Today",
+ "lang_yes": "Yes",
"lang_yesterday": "Yesterday"
} \ No newline at end of file
diff --git a/modules-available/main/templates/main-menu.html b/modules-available/main/templates/main-menu.html
index 2ede4f87..55b19833 100644
--- a/modules-available/main/templates/main-menu.html
+++ b/modules-available/main/templates/main-menu.html
@@ -20,6 +20,7 @@
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="?do=Main">OpenSLX</a>
+ <div id="navbar-sub" class="gray small"></div>
</div>
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse" id="bs-sidebar-navbar-collapse-1">
diff --git a/modules-available/news/lang/de/template-tags.json b/modules-available/news/lang/de/template-tags.json
index f13a5733..d8ad974f 100644
--- a/modules-available/news/lang/de/template-tags.json
+++ b/modules-available/news/lang/de/template-tags.json
@@ -9,5 +9,5 @@
"lang_oldHelp": "Alte Hilfetexte",
"lang_show": "Ansehen",
"lang_title": "Titel",
- "lang_vmChooser_title": "VMChooser-Einstellungen"
+ "lang_vmChooser_title": "vmChooser-Einstellungen"
} \ No newline at end of file
diff --git a/modules-available/news/lang/en/module.json b/modules-available/news/lang/en/module.json
index b13eca27..f44a64e5 100644
--- a/modules-available/news/lang/en/module.json
+++ b/modules-available/news/lang/en/module.json
@@ -1,3 +1,3 @@
{
- "module_name": "News"
+ "module_name": "vmChooser News"
} \ No newline at end of file
diff --git a/modules-available/news/lang/en/template-tags.json b/modules-available/news/lang/en/template-tags.json
index 58005e5b..270fed44 100644
--- a/modules-available/news/lang/en/template-tags.json
+++ b/modules-available/news/lang/en/template-tags.json
@@ -9,5 +9,5 @@
"lang_oldHelp": "Old Help Texts",
"lang_show": "Show",
"lang_title": "Title",
- "lang_vmChooser_title": "VMChooser-Settings"
+ "lang_vmChooser_title": "vmChooser-Settings"
} \ No newline at end of file
diff --git a/modules-available/rebootcontrol/api.inc.php b/modules-available/rebootcontrol/api.inc.php
index 77687f8e..dad25375 100644
--- a/modules-available/rebootcontrol/api.inc.php
+++ b/modules-available/rebootcontrol/api.inc.php
@@ -1,4 +1,13 @@
<?php
+
+if (Request::any('action') === 'rebuild' && isLocalExecution()) {
+ if (Module::isAvailable('sysconfig')) {
+ SSHKey::getPublicKey();
+ ConfigTgz::rebuildAllConfigs();
+ echo "OK";
+ }
+ exit(0);
+}
/*
Needed POST-Parameters:
'token' -- for authentification
@@ -15,7 +24,8 @@ foreach ($ips as $client) {
$clients[] = array("ip" => $client);
}
-if (Request::post('token') == Property::get("rebootcontrol_APIPOSTKEY")) {
+$apikey = Property::get("rebootcontrol_APIPOSTKEY", 'not-set');
+if (!empty($apikey) && Request::post('token') === $apikey) {
if (Request::isPost()) {
if (Request::post('action') == 'shutdown') {
$shutdown = true;
diff --git a/modules-available/rebootcontrol/config.json b/modules-available/rebootcontrol/config.json
index 2cc05822..d8ab5868 100644
--- a/modules-available/rebootcontrol/config.json
+++ b/modules-available/rebootcontrol/config.json
@@ -1,4 +1,4 @@
{
- "category":"main.content",
+ "category":"main.beta",
"dependencies": [ "locations", "js_stupidtable" ]
}
diff --git a/modules-available/rebootcontrol/inc/rebootqueries.inc.php b/modules-available/rebootcontrol/inc/rebootqueries.inc.php
index df3c13d8..8f65b756 100644
--- a/modules-available/rebootcontrol/inc/rebootqueries.inc.php
+++ b/modules-available/rebootcontrol/inc/rebootqueries.inc.php
@@ -5,10 +5,12 @@ class RebootQueries
// Get Client+IP+CurrentVM+CurrentUser+Location to fill the table
public static function getMachineTable($locationId) {
+ $queryArgs = array('cutoff' => strtotime('-30 days'));
if ($locationId === 0) {
$where = 'machine.locationid IS NULL';
} else {
$where = 'machine.locationid = :locationid';
+ $queryArgs['locationid'] = $locationId;
}
$leftJoin = '';
$sessionField = 'machine.currentsession';
@@ -19,12 +21,25 @@ class RebootQueries
}
$res = Database::simpleQuery("
SELECT machine.machineuuid, machine.hostname, machine.clientip,
- IF(machine.lastboot = 0 OR UNIX_TIMESTAMP() - machine.lastseen >= 600, 0, 1) AS status,
+ machine.lastboot, machine.lastseen, machine.logintime,
$sessionField, machine.currentuser, machine.locationid
FROM machine
$leftJoin
- WHERE " . $where, array('locationid' => $locationId));
- return $res->fetchAll(PDO::FETCH_ASSOC);
+ WHERE $where AND machine.lastseen > :cutoff", $queryArgs);
+ $ret = $res->fetchAll(PDO::FETCH_ASSOC);
+ $NOW = time();
+ foreach ($ret as &$row) {
+ if ($row['lastboot'] == 0 || $NOW - $row['lastseen'] > 600) {
+ $row['status'] = 0;
+ } else {
+ $row['status'] = 1;
+ }
+ if ($row['status'] === 0 || $row['logintime'] == 0) {
+ $row['currentuser'] = '';
+ $row['currentsession'] = '';
+ }
+ }
+ return $ret;
}
/**
@@ -36,8 +51,8 @@ class RebootQueries
{
if (empty($list))
return array();
- $qs = '?' . str_repeat(',?', count($list) - 1);
- $res = Database::simpleQuery("SELECT machineuuid, clientip, locationid FROM machine WHERE machineuuid IN ($qs)", $list);
+ $res = Database::simpleQuery("SELECT machineuuid, clientip, locationid FROM machine
+ WHERE machineuuid IN (:list)", compact('list'));
return $res->fetchAll(PDO::FETCH_ASSOC);
}
diff --git a/modules-available/rebootcontrol/lang/de/template-tags.json b/modules-available/rebootcontrol/lang/de/template-tags.json
index 2a04e746..57164f02 100644
--- a/modules-available/rebootcontrol/lang/de/template-tags.json
+++ b/modules-available/rebootcontrol/lang/de/template-tags.json
@@ -1,5 +1,5 @@
{
- "lang_authFail": "Athentifizierung fehlgeschlagen",
+ "lang_authFail": "Authentifizierung fehlgeschlagen",
"lang_client": "Client",
"lang_connecting": "Verbinde...",
"lang_error": "Nicht erreichbar",
@@ -14,7 +14,7 @@
"lang_reboot": "Neustarten",
"lang_rebootAt": "Neustart um:",
"lang_rebootButton": "Neustarten",
- "lang_rebootCheck": "Wollen Sie wirklich die ausgew\u00e4hlten Rechner neustarten?",
+ "lang_rebootCheck": "Wollen Sie die ausgew\u00e4hlten Rechner wirklich neustarten?",
"lang_rebooting": "Neustart...",
"lang_selectall": "Alle ausw\u00e4hlen",
"lang_selected": "Ausgew\u00e4hlt",
@@ -23,7 +23,7 @@
"lang_shutdown": "Herunterfahren",
"lang_shutdownAt": "Herunterfahren um: ",
"lang_shutdownButton": "Herunterfahren",
- "lang_shutdownCheck": "Wollen Sie wirklich die ausgew\u00e4hlten Rechner herunterfahren?",
+ "lang_shutdownCheck": "Wollen Sie die ausgew\u00e4hlten Rechner wirklich herunterfahren?",
"lang_shutdownIn": "Herunterfahren in: ",
"lang_status": "Status",
"lang_unselectall": "Alle abw\u00e4hlen",
diff --git a/modules-available/rebootcontrol/templates/_page.html b/modules-available/rebootcontrol/templates/_page.html
index 0fc3c166..4d2bf60e 100644
--- a/modules-available/rebootcontrol/templates/_page.html
+++ b/modules-available/rebootcontrol/templates/_page.html
@@ -40,10 +40,10 @@
<td>{{clientip}}</td>
<td class="statusColumn">
{{#status}}
- {{lang_on}}
+ <span class="text-success">{{lang_on}}</span>
{{/status}}
{{^status}}
- {{lang_off}}
+ <span class="text-danger">{{lang_off}}</span>
{{/status}}
</td>
<td>{{#status}}{{currentsession}}{{/status}}</td>
diff --git a/modules-available/roomplanner/baseconfig/getconfig.inc.php b/modules-available/roomplanner/baseconfig/getconfig.inc.php
index 8518f751..92e7a8d3 100644
--- a/modules-available/roomplanner/baseconfig/getconfig.inc.php
+++ b/modules-available/roomplanner/baseconfig/getconfig.inc.php
@@ -1,12 +1,12 @@
<?php
-$configVars["SLX_PVS_CONFIG_URL"] = 'http://' . $_SERVER['SERVER_ADDR'] . $_SERVER['SCRIPT_NAME'] . '?do=roomplanner';
+ConfigHolder::add("SLX_PVS_CONFIG_URL", 'http://' . $_SERVER['SERVER_ADDR'] . $_SERVER['SCRIPT_NAME'] . '?do=roomplanner');
$res = Database::queryFirst('SELECT dedicatedmgr FROM location_roomplan WHERE managerip = :ip LIMIT 1', ['ip' => $ip]);
if ($res !== false) {
if ((int)$res['dedicatedmgr'] !== 0) {
- $configVars["SLX_PVS_DEDICATED"] = 'yes';
+ ConfigHolder::add("SLX_PVS_DEDICATED", 'yes');
} else {
- $configVars["SLX_PVS_HYBRID"] = 'yes';
+ ConfigHolder::add("SLX_PVS_HYBRID", 'yes');
}
} \ No newline at end of file
diff --git a/modules-available/roomplanner/clientscript.js b/modules-available/roomplanner/clientscript.js
index 53e0164a..1cd65132 100644
--- a/modules-available/roomplanner/clientscript.js
+++ b/modules-available/roomplanner/clientscript.js
@@ -14,7 +14,7 @@ var placedMachines = [];
function makeCombinedField(machineArray)
{
machineArray.forEach(function (v,i,a){
- machineArray[i].combined = (v.machineuuid + " " + v.hostname + " " + v.clientip + " " + v.macaddr).toLocaleLowerCase();
+ machineArray[i].combined = (v.machineuuid + " " + v.hostname + " " + v.clientip + " " + v.macaddr + " " + v.macaddr.replace(/-/g, ':')).toLocaleLowerCase();
});
return machineArray;
}
@@ -48,6 +48,10 @@ function renderMachineEntry(item, escape) {
+ '</div>';
}
+function renderMachineSelected(item, escape) {
+ return '<div>' + escape(item.hostname) + '</div>';
+}
+
var queryCache = {};
function filterCache(key, query) {
@@ -113,7 +117,7 @@ function initSelectize() {
searchField: "combined",
openOnFocus: false,
create: false,
- render : { option : renderMachineEntry, item: renderMachineEntry},
+ render : { option : renderMachineEntry, item: renderMachineSelected},
load: loadMachines,
maxItems: 1,
sortField: 'hostname',
@@ -130,7 +134,7 @@ function initSelectize() {
searchField: "combined",
openOnFocus: true,
create: false,
- render : { option : renderMachineEntry, item: renderMachineEntry},
+ render : { option : renderMachineEntry, item: renderMachineSelected},
maxItems: 1,
sortField: 'hostname',
sortDirection: 'asc',
diff --git a/modules-available/roomplanner/page.inc.php b/modules-available/roomplanner/page.inc.php
index a35023b9..4e36d3ba 100644
--- a/modules-available/roomplanner/page.inc.php
+++ b/modules-available/roomplanner/page.inc.php
@@ -88,14 +88,15 @@ class Page_Roomplanner extends Page
if ($this->action === 'getmachines') {
$query = Request::get('query', false, 'string');
+ $aquery = preg_replace('/[^\x01-\x7f]+/', '%', $query);
$result = Database::simpleQuery('SELECT machineuuid, macaddr, clientip, hostname '
. 'FROM machine '
- . 'WHERE machineuuid LIKE :query '
- . ' OR macaddr LIKE :query '
- . ' OR clientip LIKE :query '
+ . 'WHERE machineuuid LIKE :aquery '
+ . ' OR macaddr LIKE :aquery '
+ . ' OR clientip LIKE :aquery '
. ' OR hostname LIKE :query '
- . ' LIMIT 100', ['query' => "%$query%"]);
+ . ' LIMIT 100', ['query' => "%$query%", 'aquery' => "%$aquery%"]);
$returnObject = ['machines' => []];
diff --git a/modules-available/runmode/baseconfig/getconfig.inc.php b/modules-available/runmode/baseconfig/getconfig.inc.php
new file mode 100644
index 00000000..fe04b5ef
--- /dev/null
+++ b/modules-available/runmode/baseconfig/getconfig.inc.php
@@ -0,0 +1,29 @@
+<?php
+
+$foofoo = function($machineUuid) {
+ $res = Database::queryFirst('SELECT module, modeid, modedata FROM runmode WHERE machineuuid = :uuid',
+ array('uuid' => $machineUuid));
+ if ($res === false)
+ return;
+ $config = RunMode::getModuleConfig($res['module']);
+ if ($config === false || $config->configHook === false)
+ return;
+ if (!Module::isAvailable($res['module']))
+ return; // Not really possible because getModuleConfig would have failed but we should make sure
+ call_user_func($config->configHook, $machineUuid, $res['modeid'], $res['modedata']);
+ if ($config->systemdDefaultTarget !== false) {
+ ConfigHolder::add('SLX_SYSTEMD_TARGET', $config->systemdDefaultTarget, 10000);
+ }
+ if ($config->noSysconfig) {
+ ConfigHolder::add('SLX_NO_CONFIG_TGZ', '1', 10000);
+ }
+ // Disable exam mode - not sure if this is generally a good idea; for now, all modes we can think of would
+ // not make sense that way so do this for now
+ if (ConfigHolder::get('SLX_EXAM') !== false) {
+ ConfigHolder::add('SLX_EXAM', '', 100001);
+ ConfigHolder::add('SLX_EXAM_START', '', 100001);
+ ConfigHolder::add('SLX_AUTOLOGIN', '', 100001);
+ }
+};
+
+$foofoo($uuid); \ No newline at end of file
diff --git a/modules-available/runmode/config.json b/modules-available/runmode/config.json
new file mode 100644
index 00000000..e3c07d48
--- /dev/null
+++ b/modules-available/runmode/config.json
@@ -0,0 +1,4 @@
+{
+ "dependencies": [ "statistics", "js_selectize" ],
+ "permission":"0"
+}
diff --git a/modules-available/runmode/inc/runmode.inc.php b/modules-available/runmode/inc/runmode.inc.php
new file mode 100644
index 00000000..0f4994f4
--- /dev/null
+++ b/modules-available/runmode/inc/runmode.inc.php
@@ -0,0 +1,208 @@
+<?php
+
+class RunMode
+{
+
+ private static $moduleConfigs = array();
+
+ /**
+ * Get runmode config for a specific module
+ *
+ * @param string $module name of module
+ * @return \RunModeModuleConfig|false config, false if moudles doesn't support run modes
+ */
+ public static function getModuleConfig($module)
+ {
+ if (isset(self::$moduleConfigs[$module]))
+ return self::$moduleConfigs[$module];
+ if (Module::get($module) === false)
+ return false;
+ $file = 'modules/' . $module . '/hooks/runmode/config.json';
+ if (!file_exists($file))
+ return false;
+ return (self::$moduleConfigs[$module] = new RunModeModuleConfig($file));
+ }
+
+ /**
+ * @param string $machineuuid
+ * @param string $moduleId
+ * @param string|null $modeId an ID specific to the module to further specify the run mode, NULL to delete the run mode entry
+ * @param string|null $modeData optional, additional data for the run mode
+ * @param bool|null $isClient whether to count the machine as a client (in statistics etc.) NULL for looking at module's general runmode config
+ * @return bool whether it was set
+ */
+ public static function setRunMode($machineuuid, $moduleId, $modeId, $modeData, $isClient)
+ {
+ // - Check if module provides runmode config at all
+ $config = self::getModuleConfig($moduleId);
+ if ($config === false)
+ return false;
+ // - Check if machine exists
+ $machine = Statistics::getMachine($machineuuid, Machine::NO_DATA);
+ if ($machine === false)
+ return false;
+ // - Add/replace entry in runmode table
+ if (is_null($modeId)) {
+ Database::exec('DELETE FROM runmode WHERE machineuuid = :machineuuid', compact('machineuuid'));
+ } else {
+ if ($isClient === null) {
+ $isClient = $config->isClient;
+ }
+ Database::exec('INSERT INTO runmode (machineuuid, module, modeid, modedata, isclient)'
+ . ' VALUES (:uuid, :module, :modeid, :modedata, :isclient)'
+ . ' ON DUPLICATE KEY'
+ . ' UPDATE module = VALUES(module), modeid = VALUES(modeid), modedata = VALUES(modedata), isclient = VALUES(isclient)', array(
+ 'uuid' => $machineuuid,
+ 'module' => $moduleId,
+ 'modeid' => $modeId,
+ 'modedata' => $modeData,
+ 'isclient' => ($isClient ? 1 : 0),
+ ));
+ }
+ return true;
+ }
+
+ /**
+ * @param string|\Module $module
+ * @return array
+ */
+ public static function getForModule($module, $groupByModeId = false)
+ {
+ if (is_object($module)) {
+ $module = $module->getIdentifier();
+ }
+ $res = Database::simpleQuery('SELECT machineuuid, modeid, modedata FROM runmode WHERE module = :module',
+ compact('module'));
+ $ret = array();
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ if ($groupByModeId) {
+ if (!isset($ret[$row['modeid']])) {
+ $ret[$row['modeid']] = array();
+ }
+ $ret[$row['modeid']][] = $row;
+ } else {
+ $ret[$row['machineuuid']] = $row;
+ }
+ }
+ return $ret;
+ }
+
+ /**
+ * @param string|\Module $module
+ * @param string $modeId
+ * @param bool $detailed whether to return meta data about machine, not just machineuuid
+ * @return array
+ */
+ public static function getForMode($module, $modeId, $detailed = false)
+ {
+ if (is_object($module)) {
+ $module = $module->getIdentifier();
+ }
+ if ($detailed) {
+ $sel = ', m.hostname, m.clientip, m.macaddr, m.locationid';
+ $join = 'INNER JOIN machine m USING (machineuuid)';
+ } else {
+ $join = $sel = '';
+ }
+ $res = Database::simpleQuery(
+ "SELECT r.machineuuid, r.modedata $sel
+ FROM runmode r $join
+ WHERE module = :module AND modeid = :modeId",
+ compact('module', 'modeId'));
+ $ret = array();
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ if ($detailed && empty($row['hostname'])) {
+ $row['hostname'] = $row['clientip'];
+ }
+ $ret[] = $row;
+ }
+ return $ret;
+ }
+
+ /**
+ * Get display name of a module's mode. If the module doesn't have a getModeName
+ * method configured, the modeId is simply returned. Otherwise the return value of
+ * that method is passed through. getModeName by contract should return false if
+ * the module doesn't think the given modeId exists.
+ *
+ * @param string|\Module $module
+ * @param string $modeId
+ * @return string|bool mode name if known, modeId as fallback, or false if mode is not known by module
+ */
+ public static function getModeName($module, $modeId)
+ {
+ if (is_object($module)) {
+ $module = $module->getIdentifier();
+ }
+ $conf = self::getModuleConfig($module);
+ if ($conf === false || $conf->getModeName === false || !Module::isAvailable($module))
+ return $modeId;
+ return call_user_func($conf->getModeName, $modeId);
+ }
+
+}
+
+/* *\
+|* Helper classes *|
+\* */
+
+/**
+ * Class RunModeModuleConfig represents desired config of a runmode
+ */
+class RunModeModuleConfig
+{
+ /**
+ * @var string|false
+ */
+ public $systemdDefaultTarget = false;
+ /**
+ * @var string[]
+ */
+ public $systemdDisableTargets = [];
+ /**
+ * @var string[]
+ */
+ public $systemdEnableTargets = [];
+ /**
+ * @var string Name of function that turns a modeId into a string
+ */
+ public $getModeName = false;
+ /**
+ * @var string Name of function that is called to add additional config entries
+ */
+ public $configHook = false;
+ /**
+ * @var bool Consider this a normal client that should e.g. be shown in client statistics by default
+ */
+ public $isClient = false;
+ /**
+ * @var bool If true, config.tgz should not be downloaded by the client
+ */
+ public $noSysconfig = false;
+
+ public function __construct($file)
+ {
+ $data = json_decode(file_get_contents($file), true);
+ if (!is_array($data))
+ return;
+ $this->loadType($data, 'systemdDefaultTarget', 'string');
+ $this->loadType($data, 'systemdDisableTargets', 'array');
+ $this->loadType($data, 'systemdEnableTargets', 'array');
+ $this->loadType($data, 'getModeName', 'string');
+ $this->loadType($data, 'configHook', 'string');
+ $this->loadType($data, 'isClient', 'boolean');
+ $this->loadType($data, 'noSysconfig', 'boolean');
+ }
+
+ private function loadType($data, $key, $type)
+ {
+ if (!isset($data[$key]))
+ return false;
+ if (is_string($type) && gettype($data[$key]) !== $type)
+ return false;
+ if (is_array($type) && !in_array(gettype($data[$key]), $type))
+ return false;
+ $this->{$key} = $data[$key];
+ return true;
+ }
+}
diff --git a/modules-available/runmode/install.inc.php b/modules-available/runmode/install.inc.php
new file mode 100644
index 00000000..e2b1ed0f
--- /dev/null
+++ b/modules-available/runmode/install.inc.php
@@ -0,0 +1,46 @@
+<?php
+
+$res = array();
+
+$res[] = tableCreate('runmode', "
+ `machineuuid` char(36) CHARACTER SET ascii NOT NULL,
+ `module` varchar(30) CHARACTER SET ascii NOT NULL,
+ `modeid` varchar(60) CHARACTER SET ascii NOT NULL,
+ `modedata` blob DEFAULT NULL,
+ `isclient` bool DEFAULT '1',
+ PRIMARY KEY (`machineuuid`),
+ KEY `module` (`module`,`modeid`)
+");
+
+if (!tableExists('machine')) {
+ // Cannot add constraint yet
+ $res[] = UPDATE_RETRY;
+} else {
+ $c = tableGetContraints('runmode', 'machineuuid', 'machine', 'machineuuid');
+ if ($c === false)
+ finalResponse(UPDATE_FAILED, 'Cannot get constraints of runmode table: ' . Database::lastError());
+ if (empty($c)) {
+ $alter = Database::exec('ALTER TABLE runmode ADD FOREIGN KEY (machineuuid) REFERENCES machine (machineuuid)
+ ON DELETE CASCADE ON UPDATE CASCADE');
+ if ($alter === false)
+ finalResponse(UPDATE_FAILED, 'Cannot add machineuuid constraint to runmode table: ' . Database::lastError());
+ $res[] = UPDATE_DONE;
+ }
+}
+
+if (!tableHasColumn('runmode', 'isclient')) {
+ $ret = Database::exec("ALTER TABLE runmode ADD COLUMN isclient bool DEFAULT '1'");
+ if ($ret === false) {
+ finalResponse(UPDATE_FAILED, 'Could not add lastchange field');
+ } elseif ($ret > 0) {
+ $ret[] = UPDATE_DONE;
+ }
+}
+
+// Create response for browser
+
+if (in_array(UPDATE_DONE, $res)) {
+ finalResponse(UPDATE_DONE, 'Tables created successfully');
+}
+
+finalResponse(UPDATE_NOOP, 'Everything already up to date'); \ No newline at end of file
diff --git a/modules-available/runmode/page.inc.php b/modules-available/runmode/page.inc.php
new file mode 100644
index 00000000..05f32f81
--- /dev/null
+++ b/modules-available/runmode/page.inc.php
@@ -0,0 +1,176 @@
+<?php
+
+class Page_RunMode extends Page
+{
+
+ /**
+ * Called before any page rendering happens - early hook to check parameters etc.
+ */
+ protected function doPreprocess()
+ {
+ User::load();
+ if (!User::isLoggedIn()) {
+ Message::addError('main.no-permission');
+ Util::redirect('?do=main');
+ }
+ $action = Request::post('action', false, 'string');
+ if ($action !== false) {
+ $this->handleAction($action);
+ Util::redirect('?do=runmode');
+ }
+ }
+
+ private function handleAction($action)
+ {
+ if ($action === 'save-mode') {
+ $machines = array_filter(Request::post('machines', [], 'array'), 'is_string');
+ $module = Request::post('module', false, 'string');
+ $modeId = Request::post('modeid', false, 'string');
+ // TODO Validate
+ $active = 0;
+ foreach ($machines as $machine) {
+ $ret = RunMode::setRunMode($machine, $module, $modeId, null, null);
+ if ($ret) {
+ $active++;
+ } else {
+ Message::addError('invalid-module-or-machine', $module, $machine);
+ }
+ }
+ $deleted = Database::exec('DELETE FROM runmode
+ WHERE module = :module AND modeid = :modeId AND machineuuid NOT IN (:machines)',
+ compact('module', 'modeId', 'machines'));
+ Message::addError('enabled-removed-save', $active, $deleted);
+ $redirect = Request::post('redirect', false, 'string');
+ if ($redirect !== false) {
+ Util::redirect($redirect);
+ }
+ Util::redirect('?do=runmode&module=' . $module . '&modeid=' . $modeId);
+ } elseif ($action === 'delete-machine') {
+ $machineuuid = Request::post('machineuuid', false, 'string');
+ if ($machineuuid === false) {
+
+ }
+ }
+ }
+
+ protected function doRender()
+ {
+ $moduleId = Request::get('module', false, 'string');
+ if ($moduleId !== false) {
+ $this->renderModule($moduleId);
+ return;
+ }
+ $this->renderClientList(false);
+ }
+
+ private function renderModule($moduleId)
+ {
+ $module = Module::get($moduleId);
+ if ($module === false) {
+ Message::addError('main.no-such-module', $moduleId);
+ Util::redirect('?do=runmode');
+ }
+ $module->activate();
+ $config = RunMode::getModuleConfig($moduleId);
+ if ($config === false) {
+ Message::addError('module-hasnt-runmode', $moduleId);
+ Util::redirect('?do=runmode');
+ }
+ // Given modeId?
+ $modeId = Request::get('modeid', false, 'string');
+ if ($modeId !== false) {
+ // Show edit page for specific module-mode combo
+ $this->renderModuleMode($module, $modeId);
+ return;
+ }
+ // Show list of machines with assigned mode for this module
+ $this->renderClientList($moduleId);
+ Render::setTitle(Page::getModule()->getDisplayName() . ' – ' . $module->getDisplayName());
+ }
+
+ private function renderClientList($onlyModule)
+ {
+ if ($onlyModule === false) {
+ $where = '';
+ } else {
+ $where = ' AND r.module = :moduleId ';
+ }
+ $res = Database::simpleQuery("SELECT m.machineuuid, m.hostname, m.clientip, r.module, r.modeid, r.isclient"
+ . " FROM runmode r"
+ . " INNER JOIN machine m ON (m.machineuuid = r.machineuuid $where )"
+ . " ORDER BY m.hostname ASC, m.clientip ASC", array('moduleId' => $onlyModule));
+ $modules = array();
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ if (!isset($modules[$row['module']])) {
+ if (!Module::isAvailable($row['module']))
+ continue;
+ $modules[$row['module']] = array('config' => RunMode::getModuleConfig($row['module']), 'list' => array());
+ }
+ if (empty($row['hostname'])) {
+ $row['hostname'] = $row['clientip'];
+ }
+ if ($modules[$row['module']]['config']->getModeName !== false) {
+ $row['mode_name'] = call_user_func($modules[$row['module']]['config']->getModeName, $row['modeid']);
+ }
+ $modules[$row['module']]['list'][] = $row;
+ }
+ foreach ($modules as $moduleId => $rows) {
+ $module = Module::get($moduleId);
+ if ($module === false)
+ continue;
+ Render::addTemplate('module-machine-list', array(
+ 'list' => $rows['list'],
+ 'modulename' => $module->getDisplayName(),
+ 'module' => $moduleId,
+ ));
+ }
+ }
+
+ /**
+ * @param \Module $module
+ * @param string $modeId
+ */
+ private function renderModuleMode($module, $modeId)
+ {
+ $moduleId = $module->getIdentifier();
+ $modeName = RunMode::getModeName($moduleId, $modeId);
+ if ($modeName === false) {
+ Message::addError('invalid-modeid', $modeId);
+ Util::redirect('?do=runmode');
+ }
+ Render::addTemplate('machine-selector', [
+ 'module' => $moduleId,
+ 'modeid' => $modeId,
+ 'moduleName' => $module->getDisplayName(),
+ 'modeName' => $modeName,
+ 'machines' => json_encode(RunMode::getForMode($module, $modeId, true)),
+ 'redirect' => Request::get('redirect', '', 'string'),
+ ]);
+ }
+
+ protected function doAjax()
+ {
+ $action = Request::any('action', false, 'string');
+
+ if ($action === 'getmachines') {
+ $query = Request::get('query', false, 'string');
+
+ $result = Database::simpleQuery('SELECT m.machineuuid, m.macaddr, m.clientip, m.hostname, m.locationid, '
+ . 'r.module, r.modeid '
+ . 'FROM machine m '
+ . 'LEFT JOIN runmode r USING (machineuuid) '
+ . 'WHERE machineuuid LIKE :query '
+ . ' OR macaddr LIKE :query '
+ . ' OR clientip LIKE :query '
+ . ' OR hostname LIKE :query '
+ . ' LIMIT 100', ['query' => "%$query%"]);
+
+ $returnObject = [
+ 'machines' => $result->fetchAll(PDO::FETCH_ASSOC)
+ ];
+
+ echo json_encode($returnObject);
+ }
+ }
+
+} \ No newline at end of file
diff --git a/modules-available/runmode/style.css b/modules-available/runmode/style.css
new file mode 100644
index 00000000..655292db
--- /dev/null
+++ b/modules-available/runmode/style.css
@@ -0,0 +1,36 @@
+/* select popup */
+.machine-entry {
+ width: 99%;
+ width: calc(100% - 5px);
+ border: 1px solid #999;
+ border-radius: 5px;
+ margin: 2px;
+ padding: 2px 4px;
+}
+
+/* in case it is already in the sketchboard */
+.machine-entry.used {
+ color: #666;
+}
+
+.machine-entry table {
+ font-size: 12px;
+ margin-bottom: -5px;
+ width: 100%;
+}
+.machine-entry table tr {
+ border-top: 1px solid #bbb;
+}
+
+.machine-entry-header {
+ font-weight: bolder;
+ font-size: 18px;
+}
+
+.used .mode {
+ color: #f00;
+}
+
+.selectize-dropdown-content {
+ max-height : 600px;
+}
diff --git a/modules-available/runmode/templates/machine-selector.html b/modules-available/runmode/templates/machine-selector.html
new file mode 100644
index 00000000..d3ff7378
--- /dev/null
+++ b/modules-available/runmode/templates/machine-selector.html
@@ -0,0 +1,128 @@
+<h1>{{lang_assignRunmodeToMachine}}</h1>
+<h2>{{moduleName}} // {{modeName}}</h2>
+<p>{{lang_assignMachineIntroText}}</p>
+
+<div class="hidden">
+ {{#machines}}
+ <div id="qex-{{machineuuid}}">{{hostname}}</div>
+ {{/machines}}
+</div>
+
+<h4>{{lang_addNewMachines}}</h4>
+<form method="post" action="?do=runmode">
+ <input type="hidden" name="token" value="{{token}}">
+ <input type="hidden" name="action" value="save-mode">
+ <input type="hidden" name="module" value="{{module}}" id="i-module">
+ <input type="hidden" name="modeid" value="{{modeid}}" id="i-modeid">
+ <input type="hidden" name="redirect" value="{{redirect}}">
+ <select id="machine-sel" name="machines[]" multiple>
+ </select>
+ <div class="buttonbar">
+ <button type="submit" class="btn btn-primary">{{lang_save}}</button>
+ </div>
+</form>
+
+<script type="application/javascript"><!--
+
+const MODULE = document.getElementById('i-module').value;
+const MODE_ID = document.getElementById('i-modeid').value;
+
+function makeCombinedField(machineArray) {
+ machineArray.forEach(function (v, i, a) {
+ machineArray[i].combined = (v.machineuuid + " " + v.hostname + " " + v.clientip + " " + v.macaddr + " " + v.macaddr.replace(/-/g, ':')).toLocaleLowerCase();
+ machineArray[i].isUsed = v.module && v.module.length && (v.module !== MODULE || v.modeid !== MODE_ID);
+ });
+ return machineArray;
+}
+
+var queryCache = {};
+
+
+function filterCache(key, query) {
+ return queryCache[key].filter(function (el) {
+ return -1 !== el.combined.indexOf(query);
+ });
+}
+
+function loadMachines(query, callback) {
+ if (query.length < 2) {
+ callback();
+ return;
+ }
+ query = query.toLocaleLowerCase();
+ // See if we have a previous query in our cache that is a superset for this one
+ for (var k in queryCache) {
+ if (query.indexOf(k) !== -1) {
+ callback(filterCache(k, query));
+ return;
+ }
+ }
+ $.ajax({
+ url: '?do=runmode&action=getmachines&query=' + encodeURIComponent(query),
+ type: 'GET',
+ dataType: 'json',
+ error: function () {
+ callback();
+ },
+ success: function (json) {
+ var machines = makeCombinedField(json.machines);
+ // Server cuts off at 100, so only cache if it contains less entries, as
+ // the new, more specific query could return previously removed results.
+ if (machines.length < 100) {
+ queryCache[query] = machines;
+ }
+ callback(machines);
+ }
+ });
+}
+
+function renderMachineOption(item, escape) {
+ var extraClass = '';
+ var usedRow = '';
+ if (item.isUsed) {
+ usedRow = '<tr class="mode"><td>Mode:</td><td>' + escape(item.module + ' // ' + item.modeid) + '</td></tr>';
+ extraClass = 'used';
+ }
+ item.hostname || (item.hostname = item.clientip);
+ return '<div class="machine-entry ' + extraClass +'">'
+ + ' <div class="machine-body">'
+ + ' <div class="machine-entry-header"> ' + escape(item.hostname) + '</div>'
+ + ' <table>'
+ + '<tr><td>UUID:</td> <td>' + escape(item.machineuuid) + '</td></tr>'
+ + '<tr><td>MAC: </td> <td>' + escape(item.macaddr) + '</td></tr>'
+ + '<tr><td>IP: </td> <td>' + escape(item.clientip) + '</td></tr>'
+ + usedRow
+ + ' </table>'
+ + ' </div>'
+ + '</div>';
+}
+
+function renderMachineSelected(item, escape) {
+ item.hostname || (item.hostname = item.clientip);
+ var extra = '';
+ if (item.isUsed) {
+ extra = '<span class="glyphicon glyphicon-warning-sign text-danger"></span> '
+ }
+ return '<div>' + extra + escape(item.hostname) + '<div class="small">' + escape(item.clientip + ' - ' + item.macaddr) + '</div>'
+ + '</div>';
+}
+
+document.addEventListener('DOMContentLoaded', function () {
+ var old = {{{machines}}} || [];
+ var $box = $('#machine-sel').selectize({
+ options: old,
+ items: old.map(function(x) { return x.machineuuid; }),
+ plugins: ["remove_button"],
+ valueField: 'machineuuid',
+ searchField: "combined",
+ openOnFocus: false,
+ create: false,
+ render: {option: renderMachineOption, item: renderMachineSelected},
+ load: loadMachines,
+ maxItems: null,
+ sortField: 'hostname',
+ sortDirection: 'asc'
+ });
+});
+
+//--></script> \ No newline at end of file
diff --git a/modules-available/runmode/templates/module-machine-list.html b/modules-available/runmode/templates/module-machine-list.html
new file mode 100644
index 00000000..a749a4a7
--- /dev/null
+++ b/modules-available/runmode/templates/module-machine-list.html
@@ -0,0 +1,44 @@
+<h2>
+ {{lang_specialMachinesForMode}}:
+ <a href="?do={{module}}">{{modulename}}</a>
+</h2>
+
+<form method="post" action="?do=runmode" onclick="return confirm('{{lang_confirmDelete}}')">
+ <input type="hidden" name="token" value="{{token}}">
+ <input type="hidden" name="action" value="delete-machine">
+ <table class="table">
+ <tr>
+ <th>{{lang_machine}}</th>
+ <th>{{lang_mode}}</th>
+ <th class="slx-smallcol">{{lang_isclient}}</th>
+ <th class="slx-smallcol">{{lang_delete}}</th>
+ </tr>
+ {{#list}}
+ <tr>
+ <td>
+ <div class="slx-bold" title="{{clientip}}"><a
+ href="?do=statistics&amp;uuid={{machineuuid}}">{{hostname}}</a></div>
+ <div class="small">{{machineuuid}}</div>
+ </td>
+ <td>
+ {{#mode_name}}
+ <div>{{mode_name}}</div>
+ <div class="small">{{modeid}}</div>
+ {{/mode_name}}
+ {{^mode_name}}
+ <div>{{modeid}}</div>
+ {{/mode_name}}
+ </td>
+ <td class="text-center">
+ {{#isclient}}
+ <span class="glyphicon glyphicon-check text-success"></span>
+ {{/isclient}}
+ </td>
+ <td class="text-center">
+ <button class="btn btn-danger btn-sm" name="machineuuid" value="{{machineuuid}}"><span
+ class="glyphicon glyphicon-remove"></span></button>
+ </td>
+ </tr>
+ {{/list}}
+ </table>
+</form> \ No newline at end of file
diff --git a/modules-available/statistics/hooks/config-tgz.inc.php b/modules-available/statistics/hooks/config-tgz.inc.php
index 1272a94f..8dffbff6 100644
--- a/modules-available/statistics/hooks/config-tgz.inc.php
+++ b/modules-available/statistics/hooks/config-tgz.inc.php
@@ -7,26 +7,28 @@ $res = Database::simpleQuery('SELECT h.hwname FROM statistic_hw h'
'screen' => DeviceType::SCREEN,
));
-$content = '';
-while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
- $content .= $row['hwname'] . "=beamer\n";
-}
+if ($res !== false) { // CHeck this in case we're running on old DB during update
+ $content = '';
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ $content .= $row['hwname'] . "=beamer\n";
+ }
-if (!empty($content)) {
- $tmpfile = '/tmp/bwlp-' . md5($content) . '.tar';
- if (!is_file($tmpfile) || !is_readable($tmpfile) || filemtime($tmpfile) + 86400 < time()) {
- if (file_exists($tmpfile)) {
- unlink($tmpfile);
- }
- try {
- $a = new PharData($tmpfile);
- $a->addFromString("/opt/openslx/beamergui/beamer.conf", $content);
+ if (!empty($content)) {
+ $tmpfile = '/tmp/bwlp-' . md5($content) . '.tar';
+ if (!is_file($tmpfile) || !is_readable($tmpfile) || filemtime($tmpfile) + 86400 < time()) {
+ if (file_exists($tmpfile)) {
+ unlink($tmpfile);
+ }
+ try {
+ $a = new PharData($tmpfile);
+ $a->addFromString("/opt/openslx/beamergui/beamer.conf", $content);
+ $file = $tmpfile;
+ } catch (Exception $e) {
+ EventLog::failure('Could not include beamer.conf in config.tgz', (string)$e);
+ unlink($tmpfile);
+ }
+ } elseif (is_file($tmpfile) && is_readable($tmpfile)) {
$file = $tmpfile;
- } catch (Exception $e) {
- EventLog::failure('Could not include beamer.conf in config.tgz', (string)$e);
- unlink($tmpfile);
}
- } elseif (is_file($tmpfile) && is_readable($tmpfile)) {
- $file = $tmpfile;
}
-}
+} \ No newline at end of file
diff --git a/modules-available/statistics/inc/filter.inc.php b/modules-available/statistics/inc/filter.inc.php
index 6af6eed1..0afce572 100644
--- a/modules-available/statistics/inc/filter.inc.php
+++ b/modules-available/statistics/inc/filter.inc.php
@@ -91,7 +91,6 @@ class Filter
if ($lhs == 'gbram') {
$filters[] = new RamGbFilter($operator, $rhs);
} elseif ($lhs == 'state') {
- error_log('new state filter with ' . $rhs);
$filters[] = new StateFilter($operator, $rhs);
} elseif ($lhs == 'hddgb') {
$filters[] = new Id44Filter($operator, $rhs);
diff --git a/modules-available/statistics/inc/machine.inc.php b/modules-available/statistics/inc/machine.inc.php
new file mode 100644
index 00000000..8cb5e884
--- /dev/null
+++ b/modules-available/statistics/inc/machine.inc.php
@@ -0,0 +1,73 @@
+<?php
+
+class Machine
+{
+ const NO_DATA = 0;
+ const RAW_DATA = 1;
+
+ /**
+ * @var string UUID
+ */
+ public $machineuuid;
+
+ /**
+ * @var int|null locationid machine belongs to
+ */
+ public $locationid;
+
+ /**
+ * @var string mac address
+ */
+ public $macaddr;
+
+ /**
+ * @var string client's ip address
+ */
+ public $clientip;
+
+ /**
+ * @var string client's host name
+ */
+ public $hostname;
+
+ /**
+ * @var int timestamp of when this machine booted from this server for the first time
+ */
+ public $firstseen;
+
+ /**
+ * @var int last time this machine was seen active
+ */
+ public $lastseen;
+
+ /**
+ * @var int timestamp of when the machine was booted, 0 if machine is powered off
+ */
+ public $lastboot;
+
+ /**
+ * @var int timestamp of when the current user logged in, 0 if machine is idle
+ */
+ public $logintime;
+
+ /**
+ * @var string json data of position inside room (if any), null/empty otherwise
+ */
+ public $position;
+
+ /**
+ * @var string|null UUID or name of currently running lecture/session
+ */
+ public $currentsession;
+
+ /**
+ * @var string|null name of currently logged in user
+ */
+ public $currentuser;
+
+ /**
+ * @var string|null raw data of machine, if requested
+ */
+ public $data;
+
+}
diff --git a/modules-available/statistics/inc/statistics.inc.php b/modules-available/statistics/inc/statistics.inc.php
new file mode 100644
index 00000000..1c9ebf07
--- /dev/null
+++ b/modules-available/statistics/inc/statistics.inc.php
@@ -0,0 +1,40 @@
+<?php
+
+
+
+class Statistics
+{
+
+ private static $machineFields = false;
+
+ /**
+ * @param string $machineuuid
+ * @param int $returnData
+ * @return \Machine|false
+ */
+ public static function getMachine($machineuuid, $returnData)
+ {
+ if (self::$machineFields === false) {
+ $r = new ReflectionClass('Machine');
+ $props = $r->getProperties(ReflectionProperty::IS_PUBLIC);
+ self::$machineFields = array_flip(array_map(function($e) { return $e->getName(); }, $props));
+ }
+ if ($returnData === Machine::NO_DATA) {
+ unset(self::$machineFields['data']);
+ } elseif ($returnData === Machine::RAW_DATA) {
+ self::$machineFields['data'] = true;
+ } else {
+ Util::traceError('Invalid $returnData option passed');
+ }
+ $fields = implode(',', array_keys(self::$machineFields));
+ $row = Database::queryFirst("SELECT * FROM machine WHERE machineuuid = :machineuuid", compact('machineuuid'));
+ if ($row === false)
+ return false;
+ $m = new Machine();
+ foreach ($row as $key => $val) {
+ $m->{$key} = $val;
+ }
+ return $m;
+ }
+
+}
diff --git a/modules-available/statistics/install.inc.php b/modules-available/statistics/install.inc.php
index 79346f99..bfa342c4 100644
--- a/modules-available/statistics/install.inc.php
+++ b/modules-available/statistics/install.inc.php
@@ -27,7 +27,7 @@ $res[] = tableCreate('statistic', "
$res[] = $machineCreate = tableCreate('machine', "
`machineuuid` char(36) CHARACTER SET ascii NOT NULL,
`fixedlocationid` int(11) DEFAULT NULL COMMENT 'Manually set location (e.g. roomplanner)',
- `subnetlocationid` int(11) DEFAULT NULL COMMENT 'Automatically determined location (e.g. from subnet match),
+ `subnetlocationid` int(11) DEFAULT NULL COMMENT 'Automatically determined location (e.g. from subnet match)',
`locationid` int(11) DEFAULT NULL COMMENT 'Will be automatically set to fixedlocationid if not null, subnetlocationid otherwise',
`macaddr` char(17) CHARACTER SET ascii NOT NULL,
`clientip` varchar(45) CHARACTER SET ascii NOT NULL,
diff --git a/modules-available/statistics/lang/de/template-tags.json b/modules-available/statistics/lang/de/template-tags.json
index 7274aef4..e0be0d48 100644
--- a/modules-available/statistics/lang/de/template-tags.json
+++ b/modules-available/statistics/lang/de/template-tags.json
@@ -47,6 +47,7 @@
"lang_modelStats": "PC-Modelle",
"lang_more": "Mehr",
"lang_newMachines": "Neue Ger\u00e4te",
+ "lang_noEdid": "Kein EDID",
"lang_noProjectorsDefined": "Keine Beamer-Overrides definiert",
"lang_notes": "Anmerkungen",
"lang_onlineMachines": "Gestartete Clients",
diff --git a/modules-available/statistics/lang/en/template-tags.json b/modules-available/statistics/lang/en/template-tags.json
index 4e135388..4a31a5ee 100644
--- a/modules-available/statistics/lang/en/template-tags.json
+++ b/modules-available/statistics/lang/en/template-tags.json
@@ -47,6 +47,7 @@
"lang_modelStats": "PC models",
"lang_more": "More",
"lang_newMachines": "New machines",
+ "lang_noEdid": "No EDID",
"lang_noProjectorsDefined": "No projector overrides defined",
"lang_notes": "Notes",
"lang_onlineMachines": "Online clients",
diff --git a/modules-available/statistics/page.inc.php b/modules-available/statistics/page.inc.php
index 5ad8bc20..2b12c69f 100644
--- a/modules-available/statistics/page.inc.php
+++ b/modules-available/statistics/page.inc.php
@@ -404,7 +404,7 @@ class Page_Statistics extends Page
$res = Database::simpleQuery("SELECT mbram, Count(*) AS `count` FROM machine $join WHERE $where GROUP BY mbram", $args);
$lines = array();
while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
- $gb = ceil($row['mbram'] / 1024);
+ $gb = (int)ceil($row['mbram'] / 1024);
for ($i = 1; $i < count($SIZE_RAM); ++$i) {
if ($SIZE_RAM[$i] < $gb) {
continue;
@@ -476,7 +476,7 @@ class Page_Statistics extends Page
$total = 0;
while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
$total += $row['count'];
- $gb = ceil($row['id44mb'] / 1024);
+ $gb = (int)ceil($row['id44mb'] / 1024);
for ($i = 1; $i < count($SIZE_ID44); ++$i) {
if ($SIZE_ID44[$i] < $gb) {
continue;
diff --git a/modules-available/statistics/templates/machine-main.html b/modules-available/statistics/templates/machine-main.html
index bdc51167..74df80c4 100644
--- a/modules-available/statistics/templates/machine-main.html
+++ b/modules-available/statistics/templates/machine-main.html
@@ -143,6 +143,7 @@
</form>
{{#screens}}
<div class="small">
+ {{#hwname}}
<div class="pull-right btn-group btn-group-xs">
{{#projector}}
<a href="?do=statistics&amp;show=projectors" class="btn btn-default">{{lang_projector}}</a>
@@ -154,7 +155,8 @@
class="btn btn-success"><span class="glyphicon glyphicon-plus"></span> {{lang_projector}}</button>
{{/projector}}
</div>
- {{connector}}: <b>{{hwname}}</b> {{resolution}}
+ {{/hwname}}
+ {{connector}}: <b>{{hwname}}</b> {{^hwname}}<i>{{lang_noEdid}}</i>{{/hwname}} {{resolution}}
<div class="clearfix"></div>
</div>
{{/screens}}
@@ -187,4 +189,4 @@ document.addEventListener("DOMContentLoaded", function () {
$(this).load('?do=statistics&lookup=' + $(this).text());
});
}, false);
-// --></script> \ No newline at end of file
+// --></script>
diff --git a/modules-available/statistics_reporting/config.json b/modules-available/statistics_reporting/config.json
index f9627cdb..78ca35ba 100644
--- a/modules-available/statistics_reporting/config.json
+++ b/modules-available/statistics_reporting/config.json
@@ -1,4 +1,4 @@
{
- "category": "main.content",
+ "category": "main.status",
"dependencies": [ "statistics", "locations", "js_stupidtable", "js_jqueryui" ]
}
diff --git a/modules-available/statistics_reporting/hooks/cron.inc.php b/modules-available/statistics_reporting/hooks/cron.inc.php
index afb18a23..14597f7d 100644
--- a/modules-available/statistics_reporting/hooks/cron.inc.php
+++ b/modules-available/statistics_reporting/hooks/cron.inc.php
@@ -7,10 +7,9 @@ if (RemoteReport::isReportingEnabled()) {
while ($nextReporting <= time()) {
RemoteReport::writeNextReportingTimestamp();
- $from = strtotime("-7 days", $nextReporting);
$to = $nextReporting;
- $statisticsReport = json_encode(RemoteReport::generateReport($from, $to));
+ $statisticsReport = json_encode(RemoteReport::generateReport($to));
$params = array("action" => "statistics", "data" => $statisticsReport);
diff --git a/modules-available/statistics_reporting/inc/getdata.inc.php b/modules-available/statistics_reporting/inc/getdata.inc.php
index da3a9a26..ae4d5aa4 100644
--- a/modules-available/statistics_reporting/inc/getdata.inc.php
+++ b/modules-available/statistics_reporting/inc/getdata.inc.php
@@ -29,6 +29,7 @@ class GetData
$data["medianSessionLength_s"] = self::formatSeconds($data["medianSessionLength"]);
$data["totalOffTime_s"] = self::formatSeconds($data["totalOffTime"]);
}
+ $data['uniqueUsers'] = Queries::getUniqueUserCount(self::$from, self::$to);
return $data;
}
diff --git a/modules-available/statistics_reporting/inc/queries.inc.php b/modules-available/statistics_reporting/inc/queries.inc.php
index 2269e764..1a125c9e 100644
--- a/modules-available/statistics_reporting/inc/queries.inc.php
+++ b/modules-available/statistics_reporting/inc/queries.inc.php
@@ -213,5 +213,68 @@ class Queries
";
return "(".$queryString.")";
}
+
+ public static function getDozmodStats($from, $to)
+ {
+ if (!Module::isAvailable('dozmod'))
+ return array('disabled' => true);
+
+ $return = array();
+ $return['vms'] = Database::queryFirst("SELECT Count(*) AS `total`, Sum(If(createtime >= $from, 1, 0)) AS `new`,
+ Sum(If(updatetime >= $from, 1, 0)) AS `updated`, Sum(If(latestversionid IS NOT NULL, 1, 0)) AS `valid`
+ FROM sat.imagebase
+ WHERE createtime <= $to");
+ $return['lectures'] = Database::queryFirst("SELECT Count(*) AS `total`, Sum(If(createtime >= $from, 1, 0)) AS `new`,
+ Sum(If(updatetime >= $from, 1, 0)) AS `updated`,
+ Sum(If((($from BETWEEN starttime AND endtime) OR ($to BETWEEN starttime AND endtime)) AND isenabled <> 0, 1, 0)) AS `valid`
+ FROM sat.lecture
+ WHERE createtime <= $to");
+ $return['users'] = Database::queryFirst("SELECT Count(*) AS `total`, Count(DISTINCT organizationid) AS `organizations`
+ FROM sat.user
+ WHERE lastlogin >= $from");
+ return $return;
+ }
+
+ public static function getAggregatedMachineStats($from)
+ {
+ $return = array();
+ $return['location'] = Database::queryAll("SELECT MD5(CONCAT(locationid, :salt)) AS `location`, Count(*) AS `count`
+ FROM machine
+ WHERE lastseen >= $from
+ GROUP BY locationid",
+ array('salt' => GetData::$salt));
+ $prev = 0;
+ $str = ' ';
+ foreach (array(0.5, 1, 1.5, 2, 3, 4, 6, 8, 10, 12, 16, 20, 24, 28, 32, 40, 48, 64, 72, 80, 88, 96, 128, 192, 256) as $val) {
+ $str .= 'WHEN mbram < ' . round(($val + $prev) * 512) . " THEN '" . $prev . "' ";
+ $prev = $val;
+ }
+ $return['ram'] = Database::queryAll("SELECT CASE $str ELSE 'OVER 9000' END AS `gbram`, Count(*) AS `total`
+ FROM machine
+ WHERE lastseen >= $from
+ GROUP BY gbram");
+ foreach (array('cpumodel', 'systemmodel', 'realcores', 'kvmstate') as $key) {
+ $return[$key] = Database::queryAll("SELECT $key, Count(*) AS `total`
+ FROM machine
+ WHERE lastseen >= $from
+ GROUP BY $key");
+ }
+ return $return;
+ }
+
+ /**
+ * @param int $from start timestamp
+ * @param int $to end timestamp
+ * @return int count of user active in timespan
+ */
+ public static function getUniqueUserCount($from, $to)
+ {
+ $res = Database::queryFirst("SELECT Count(DISTINCT username) as `total`
+ FROM statistic
+ WHERE (dateline BETWEEN $from AND $to) AND typeid = '.vmchooser-session-name'
+ GROUP BY username");
+ return (int)$res['total'];
+ }
+
}
diff --git a/modules-available/statistics_reporting/inc/remotereport.inc.php b/modules-available/statistics_reporting/inc/remotereport.inc.php
index 4c5f604b..e94e9b93 100644
--- a/modules-available/statistics_reporting/inc/remotereport.inc.php
+++ b/modules-available/statistics_reporting/inc/remotereport.inc.php
@@ -13,18 +13,19 @@ class RemoteReport
*/
public static function setReportingEnabled($isEnabled)
{
- $value = ($isEnabled === true || $isEnabled === 'on') ? 'on' : '';
- Property::set(self::ENABLED_ID, $value, 60 * 24 * 14);
+ $value = ($isEnabled === true || $isEnabled === 'on') ? 'on' : 'off';
+ Property::set(self::ENABLED_ID, $value);
}
/**
* Returns whether remote reporting is enabled or not.
+ * Defaults to on.
*
* @return bool true if reporting is on, false if off
*/
public static function isReportingEnabled()
{
- return Property::get(self::ENABLED_ID, false) === 'on';
+ return Property::get(self::ENABLED_ID, 'on') === 'on';
}
/**
@@ -42,9 +43,9 @@ class RemoteReport
// schedule for next time
self::writeNextReportingTimestamp();
$ts = Property::get(self::NEXT_SUBMIT_ID, 0);
- } elseif ($ts < strtotime('last monday')) {
- // Too long ago, move forward to last monday
- $ts = strtotime('last monday');
+ } elseif ($ts < strtotime('last monday - 14 days')) {
+ // Too long ago, move forward
+ $ts = strtotime('last monday - 14 days');
}
return $ts;
}
@@ -63,19 +64,63 @@ class RemoteReport
* Generate the multi-dimensional array containing the anonymized
* (weekly) statistics to report.
*
- * @param int $from start timestamp
* @param int $to end timestamp
+ * @param int[] $days list of days to generate aggregated stats for
* @return array wrapped up statistics, ready for reporting
*/
- public static function generateReport($from, $to) {
- GetData::$from = $from;
- GetData::$to = $to;
+ public static function generateReport($to, $days = false) {
+ if ($days === false) {
+ $days = [7, 30, 90];
+ }
GetData::$salt = bin2hex(Util::randomBytes(20, false));
- $data = GetData::total(GETDATA_ANONYMOUS);
- $data['perLocation'] = GetData::perLocation(GETDATA_ANONYMOUS);
- $data['perVM'] = GetData::perVM(GETDATA_ANONYMOUS);
- $data['tsFrom'] = $from;
- $data['tsTo'] = $to;
+ GetData::$lowerTimeBound = 7;
+ GetData::$upperTimeBound = 20;
+ $result = array();
+ foreach ($days as $day) {
+ if (isset($result['days' . $day]))
+ continue;
+ $from = strtotime("-{$day} days", $to);
+ GetData::$from = $from;
+ GetData::$to = $to;
+ $data = array('total' => GetData::total(GETDATA_ANONYMOUS));
+ $data['perLocation'] = GetData::perLocation(GETDATA_ANONYMOUS);
+ $data['perVM'] = GetData::perVM(GETDATA_ANONYMOUS);
+ $data['tsFrom'] = $from;
+ $data['tsTo'] = $to;
+ $data['dozmod'] = Queries::getDozmodStats($from, $to);
+ $data['machines'] = Queries::getAggregatedMachineStats($from);
+ $result['days' . $day] = $data;
+ }
+ $result['server'] = self::getLocalHardware();
+ $result['version'] = CONFIG_FOOTER;
+ return $result;
+ }
+
+ private function getLocalHardware()
+ {
+ $cpuInfo = file_get_contents('/proc/cpuinfo');
+ $uptime = file_get_contents('/proc/uptime');
+ $memInfo = file_get_contents('/proc/meminfo');
+ preg_match_all('/\b(\w+):\s+(\d+)\s/s', $memInfo, $out, PREG_SET_ORDER);
+ $mem = array();
+ foreach ($out as $e) {
+ $mem[$e[1]] = $e[2];
+ }
+ //
+ $data = array();
+ $data['cpuCount'] = preg_match_all('/\bprocessor\s+:\s+(.*)$/m', $cpuInfo, $out);
+ if ($data['cpuCount'] > 0) {
+ $data['cpuModel'] = $out[1][0];
+ }
+ if (preg_match('/^(\d+)\D/', $uptime, $out)) {
+ $data['uptime'] = $out[1];
+ }
+ if (isset($mem['MemTotal']) && isset($mem['MemFree']) && isset($mem['SwapTotal'])) {
+ $data['memTotal'] = $mem['MemTotal'];
+ $data['memFree'] = ($mem['MemFree'] + $mem['Buffers'] + $mem['Cached']);
+ $data['swapTotal'] = $mem['SwapTotal'];
+ $data['swapUsed'] = ($mem['SwapTotal'] - $mem['SwapFree']);
+ }
return $data;
}
diff --git a/modules-available/statistics_reporting/lang/de/template-tags.json b/modules-available/statistics_reporting/lang/de/template-tags.json
index 3801f3a6..fb624fad 100644
--- a/modules-available/statistics_reporting/lang/de/template-tags.json
+++ b/modules-available/statistics_reporting/lang/de/template-tags.json
@@ -10,7 +10,8 @@
"lang_location": "Ort",
"lang_longSessions": "Sitzungen \u2265 60s",
"lang_medianSessionLength": "Sitzungsdauer Median",
- "lang_reportingDescription": "Helfen Sie uns bwLehrpool durch das w\u00f6chentliche Verschicken eines anonymisierten Statistikberichts zu verbessern. Wenn Sie den Inhalt eines solchen Reports genauer inspizieren wollen, k\u00f6nnen Sie \u00fcber den untenstehenden Button einen aktuellen Report ihres Servers herunterladen.",
+ "lang_reportMoreInfo": "Weitere Informationen...",
+ "lang_reportingDescription": "Helfen Sie uns bwLehrpool durch das w\u00f6chentliche Verschicken eines anonymisierten Statistikberichts zu verbessern. Wenn Sie den Inhalt eines solchen Reports genauer inspizieren wollen, k\u00f6nnen Sie \u00fcber den untenstehenden Button einen aktuellen Report Ihres Servers herunterladen.",
"lang_reportingLabel": "Anonymisierte Nutzungsstatistiken \u00fcbermitteln",
"lang_sessions": "Sitzungen",
"lang_shortSessions": "Sitzungen < 60s",
diff --git a/modules-available/statistics_reporting/lang/en/template-tags.json b/modules-available/statistics_reporting/lang/en/template-tags.json
index a4865931..4e33ffed 100644
--- a/modules-available/statistics_reporting/lang/en/template-tags.json
+++ b/modules-available/statistics_reporting/lang/en/template-tags.json
@@ -10,6 +10,7 @@
"lang_location": "Location",
"lang_longSessions": "Sessions \u2265 60s",
"lang_medianSessionLength": "Median Session Length",
+ "lang_reportMoreInfo": "More information...",
"lang_reportingDescription": "Help us improve bwLehrpool by automatically sending an anonymized statistics report once a week. If you want to check what data the report contains, you can download such a report for reference below.",
"lang_reportingLabel": "Send anonymized usage statistics",
"lang_sessions": "Sessions",
diff --git a/modules-available/statistics_reporting/templates/columnChooser.html b/modules-available/statistics_reporting/templates/columnChooser.html
index 6776da95..d0408b6f 100644
--- a/modules-available/statistics_reporting/templates/columnChooser.html
+++ b/modules-available/statistics_reporting/templates/columnChooser.html
@@ -76,6 +76,7 @@
</div>
<div>
<p>{{lang_reportingDescription}}</p>
+ <p><a href="https://www.bwlehrpool.de/doku.php/satellite/statistics_reporting" target="_blank">{{lang_reportMoreInfo}}</a></p>
<a class="btn btn-success" href="?do=statistics_reporting&amp;action=getreport">{{lang_downloadReport}}</a>
</div>
</div>
diff --git a/modules-available/summernote/config.json b/modules-available/summernote/config.json
index d1261653..5a0c7960 100644
--- a/modules-available/summernote/config.json
+++ b/modules-available/summernote/config.json
@@ -1,3 +1,9 @@
{
- "dependencies" : []
+ "dependencies" : [],
+ "css": {
+ "style.css": true
+ },
+ "scripts": {
+ "clientscript.js": true
+ }
}
diff --git a/modules-available/sysconfig/addmodule_adauth.inc.php b/modules-available/sysconfig/addmodule_adauth.inc.php
index 82df5bad..6e4463ae 100644
--- a/modules-available/sysconfig/addmodule_adauth.inc.php
+++ b/modules-available/sysconfig/addmodule_adauth.inc.php
@@ -13,13 +13,16 @@ class AdAuth_Start extends AddModule_Base
protected function renderInternal()
{
- $ADAUTH_COMMON_FIELDS = array('title', 'server', 'searchbase', 'binddn', 'bindpw', 'home', 'homeattr', 'ssl', 'certificate');
+ $ADAUTH_COMMON_FIELDS = array('title', 'server', 'searchbase', 'binddn', 'bindpw', 'home', 'homeattr', 'ssl', 'fixnumeric', 'certificate');
$data = array();
if ($this->edit !== false) {
moduleToArray($this->edit, $data, $ADAUTH_COMMON_FIELDS);
$data['title'] = $this->edit->title();
$data['edit'] = $this->edit->id();
}
+ if ($data['fixnumeric'] === false) {
+ $data['fixnumeric'] = 's';
+ }
postToArray($data, $ADAUTH_COMMON_FIELDS, true);
$obdn = Request::post('originalbinddn');
if (!empty($obdn)) {
@@ -92,6 +95,7 @@ class AdAuth_CheckConnection extends AddModule_Base
'home' => Request::post('home'),
'homeattr' => Request::post('homeattr'),
'ssl' => Request::post('ssl'),
+ 'fixnumeric' => Request::post('fixnumeric'),
'certificate' => Request::post('certificate', ''),
'taskid' => $this->scanTask['id']
);
@@ -190,6 +194,7 @@ class AdAuth_SelfSearch extends AddModule_Base
'home' => Request::post('home'),
'homeattr' => Request::post('homeattr'),
'ssl' => Request::post('ssl') === 'on',
+ 'fixnumeric' => Request::post('fixnumeric'),
'fingerprint' => Request::post('fingerprint'),
'certificate' => Request::post('certificate', ''),
'originalbinddn' => $this->originalBindDn,
@@ -265,6 +270,7 @@ class AdAuth_HomeAttrCheck extends AddModule_Base
'home' => Request::post('home'),
'homeattr' => Request::post('homeattr'),
'ssl' => Request::post('ssl') === 'on',
+ 'fixnumeric' => Request::post('fixnumeric'),
'fingerprint' => Request::post('fingerprint'),
'certificate' => Request::post('certificate', ''),
'originalbinddn' => Request::post('originalbinddn'),
@@ -335,6 +341,7 @@ class AdAuth_CheckCredentials extends AddModule_Base
'home' => Request::post('home'),
'homeattr' => Request::post('homeattr'),
'ssl' => Request::post('ssl') === 'on',
+ 'fixnumeric' => Request::post('fixnumeric'),
'fingerprint' => Request::post('fingerprint'),
'certificate' => Request::post('certificate', ''),
'originalbinddn' => Request::post('originalbinddn'),
@@ -397,6 +404,7 @@ class AdAuth_HomeDir extends AddModule_Base
'home' => Request::post('home'),
'homeattr' => Request::post('homeattr'),
'ssl' => Request::post('ssl') === 'on',
+ 'fixnumeric' => Request::post('fixnumeric'),
'fingerprint' => Request::post('fingerprint'),
'certificate' => Request::post('certificate', ''),
'originalbinddn' => Request::post('originalbinddn'),
@@ -410,6 +418,7 @@ class AdAuth_HomeDir extends AddModule_Base
}
}
$data['shareRemapMode_' . $this->edit->getData('shareRemapMode')] = 'selected="selected"';
+ $data['shareDomain'] = $this->edit->getData('shareDomain');
$letter = $this->edit->getData('shareHomeDrive');
} else {
$data['shareDownloads_c'] = $data['shareMedia_c'] = $data['shareDocuments_c'] = $data['shareRemapCreate_c'] = 'checked="checked"';
@@ -457,6 +466,7 @@ class AdAuth_Finish extends AddModule_Base
$module->setData('homeattr', Request::post('homeattr'));
$module->setData('certificate', Request::post('certificate'));
$module->setData('ssl', $ssl);
+ $module->setData('fixnumeric', Request::post('fixnumeric', '', 'string'));
foreach (AdAuth_HomeDir::getAttributes() as $key) {
$value = Request::post($key);
if (is_numeric($value)) {
diff --git a/modules-available/sysconfig/addmodule_ldapauth.inc.php b/modules-available/sysconfig/addmodule_ldapauth.inc.php
index c61c710c..07c494cc 100644
--- a/modules-available/sysconfig/addmodule_ldapauth.inc.php
+++ b/modules-available/sysconfig/addmodule_ldapauth.inc.php
@@ -9,13 +9,16 @@ class LdapAuth_Start extends AddModule_Base
protected function renderInternal()
{
- $LDAPAUTH_COMMON_FIELDS = array('title', 'server', 'searchbase', 'binddn', 'bindpw', 'home', 'ssl', 'certificate');
+ $LDAPAUTH_COMMON_FIELDS = array('title', 'server', 'searchbase', 'binddn', 'bindpw', 'home', 'ssl', 'fixnumeric', 'certificate');
$data = array();
if ($this->edit !== false) {
moduleToArray($this->edit, $data, $LDAPAUTH_COMMON_FIELDS);
$data['title'] = $this->edit->title();
$data['edit'] = $this->edit->id();
}
+ if ($data['fixnumeric'] === false) {
+ $data['fixnumeric'] = 's';
+ }
postToArray($data, $LDAPAUTH_COMMON_FIELDS, true);
if (preg_match('/^(.*)\:(636|389)$/', $data['server'], $out)) {
$data['server'] = $out[1];
@@ -72,6 +75,7 @@ class LdapAuth_CheckConnection extends AddModule_Base
'bindpw' => Request::post('bindpw'),
'home' => Request::post('home'),
'ssl' => Request::post('ssl'),
+ 'fixnumeric' => Request::post('fixnumeric'),
'certificate' => Request::post('certificate', ''),
'taskid' => $this->scanTask['id']
);
@@ -142,6 +146,7 @@ class LdapAuth_CheckCredentials extends AddModule_Base
'bindpw' => Request::post('bindpw'),
'home' => Request::post('home'),
'ssl' => Request::post('ssl') === 'on',
+ 'fixnumeric' => Request::post('fixnumeric'),
'fingerprint' => Request::post('fingerprint'),
'certificate' => Request::post('certificate', ''),
'prev' => 'LdapAuth_Start',
@@ -181,6 +186,7 @@ class LdapAuth_HomeDir extends AddModule_Base
'home' => Request::post('home'),
'homeattr' => Request::post('homeattr'),
'ssl' => Request::post('ssl') === 'on',
+ 'fixnumeric' => Request::post('fixnumeric'),
'fingerprint' => Request::post('fingerprint'),
'certificate' => Request::post('certificate', ''),
'originalbinddn' => Request::post('originalbinddn'),
@@ -194,6 +200,7 @@ class LdapAuth_HomeDir extends AddModule_Base
}
}
$data['shareRemapMode_' . $this->edit->getData('shareRemapMode')] = 'selected="selected"';
+ $data['shareDomain'] = $this->edit->getData('shareDomain');
$letter = $this->edit->getData('shareHomeDrive');
} else {
$data['shareDownloads_c'] = $data['shareMedia_c'] = $data['shareDocuments_c'] = $data['shareRemapCreate_c'] = 'checked="checked"';
@@ -242,6 +249,7 @@ class LdapAuth_Finish extends AddModule_Base
$module->setData('home', Request::post('home'));
$module->setData('certificate', Request::post('certificate'));
$module->setData('ssl', $ssl);
+ $module->setData('fixnumeric', Request::post('fixnumeric', '', 'string'));
foreach (LdapAuth_HomeDir::getAttributes() as $key) {
$value = Request::post($key);
if (is_numeric($value)) {
diff --git a/modules-available/sysconfig/api.inc.php b/modules-available/sysconfig/api.inc.php
index 556e99f3..d6cdc0e6 100644
--- a/modules-available/sysconfig/api.inc.php
+++ b/modules-available/sysconfig/api.inc.php
@@ -57,12 +57,7 @@ while ($r = $res->fetch(PDO::FETCH_ASSOC)) {
if ($row === false) {
// TODO Not found in DB
- deliverEmpty("No config.tgz for location $locationId found");
-}
-
-if (!file_exists($row['filepath'])) {
- // TODO Does not exist
- deliverEmpty();
+ deliverEmpty("No config.tgz for location $locationId found (src $ip)");
}
Header('Content-Type: application/gzip');
diff --git a/modules-available/sysconfig/inc/configmodulebaseldap.inc.php b/modules-available/sysconfig/inc/configmodulebaseldap.inc.php
index 8e42478e..686bcbc0 100644
--- a/modules-available/sysconfig/inc/configmodulebaseldap.inc.php
+++ b/modules-available/sysconfig/inc/configmodulebaseldap.inc.php
@@ -6,7 +6,7 @@ abstract class ConfigModuleBaseLdap extends ConfigModule
const VERSION = 2;
private static $REQUIRED_FIELDS = array('server', 'searchbase');
- private static $OPTIONAL_FIELDS = array('binddn', 'bindpw', 'home', 'ssl', 'fingerprint', 'certificate', 'homeattr',
+ private static $OPTIONAL_FIELDS = array('binddn', 'bindpw', 'home', 'ssl', 'fixnumeric', 'fingerprint', 'certificate', 'homeattr',
'shareRemapMode', 'shareRemapCreate', 'shareDocuments', 'shareDownloads', 'shareDesktop', 'shareMedia',
'shareOther', 'shareHomeDrive', 'shareDomain', 'credentialPassthrough');
@@ -39,6 +39,9 @@ abstract class ConfigModuleBaseLdap extends ConfigModule
if (!isset($config['shareHomeDrive'])) {
$config['shareHomeDrive'] = 'H:';
}
+ if (!isset($config['fixnumeric'])) {
+ $config['fixnumeric'] = 's';
+ }
$this->preTaskmanagerHook($config);
return Taskmanager::submit('CreateLdapConfig', $config);
}
diff --git a/modules-available/sysconfig/lang/de/template-tags.json b/modules-available/sysconfig/lang/de/template-tags.json
index 4618aa08..d3b0bb9f 100644
--- a/modules-available/sysconfig/lang/de/template-tags.json
+++ b/modules-available/sysconfig/lang/de/template-tags.json
@@ -41,6 +41,8 @@
"lang_driveLetterNote": "WICHTIG: Bitte w\u00e4hlen Sie einen Laufwerksbuchstaben, der in den eingesetzten VMs verf\u00fcgbar ist, da ansonsten auf einen anderen Buchstaben ausgewichen werden muss.",
"lang_editLong": "Modul oder Konfiguration bearbeiten.",
"lang_editingLocationInfo": "Sie setzen die Konfiguration eines bestimmten Raums\/Orts, nicht die globale Konfiguration",
+ "lang_fixNumeric": "Nummerischen Account-Namen muss ein 's' vorangestellt werden",
+ "lang_fixNumericDescription": "Wenn Sie diese Option aktivieren, m\u00fcssen Benutzer, deren Account-Name nur aus Ziffern besteht, diesem ein 's' voranstellen beim Login. Diese Option ist beim alten Login-Manager (KDM) zwingend erforderlich, da sonst der Loginvorgang fehlschl\u00e4gt. Mit dem neuen lightdm-basierten Login-Screen lassen sich numerische Account-Namen jedoch direkt verwenden. Wenn Sie an Ihrer Einrichtung keine numerischen Account-Namen verwenden, hat diese Option keine Auswirkung.",
"lang_folderRedirection": "Folder Redirection",
"lang_generateModule": "Modul erzeugen",
"lang_handlingNotes": "Hier k\u00f6nnen Sie festlegen, wie Netzwerk-Shares (inkl. des Home-Verzeichnisses) an Virtuelle Maschinen durchgereicht werden. In \u00e4lteren Versionen von bwLehrpool wurden die VMware Shared Folders genutzt, was mit bestimmten file servern Probleme verursachen konnte. Der neue native Modus funktioniert deutlich besser, ist aber bei Windows-G\u00e4sten darauf angewiesen, dass (1) der file server smb\/cifs spricht (z.B. Windows Server, Samba unter Linux) und (2) die openslx.exe im Autostart eingebunden ist (bei den bwLehrpool Vorlagen bereits der Fall).",
diff --git a/modules-available/sysconfig/lang/en/template-tags.json b/modules-available/sysconfig/lang/en/template-tags.json
index 9b399f04..1374d87f 100644
--- a/modules-available/sysconfig/lang/en/template-tags.json
+++ b/modules-available/sysconfig/lang/en/template-tags.json
@@ -41,6 +41,8 @@
"lang_driveLetterNote": "IMPORTANT: Pick a drive letter for the home directory that will be free in the Virtual Machines. Otherwise, a random letter will be assigned.",
"lang_editLong": "Edit module or configuration.",
"lang_editingLocationInfo": "You're setting the configuration for a specific location, not the global one",
+ "lang_fixNumeric": "Numeric account names have to be prefixed by 's'",
+ "lang_fixNumericDescription": "If enabled, users with account names that consist entirely of digits have to prefix their user id by 's' when logging in. This is required with the old login manager (KDM) to prevent crashes. The new lightdm-based login manager will accept numeric account names, so you can leave this option disabled. If your organization doesn't have any numeric account names, this option will have no effect.",
"lang_folderRedirection": "Folder Redirection",
"lang_generateModule": "Generating module",
"lang_handlingNotes": "Here you can configure how network shares (like the user's home directory) are mapped inside the VM. Old Versions of bwLehrpool used the VMware Shared Folder technique, which could cause problems with certain file servers. The new \"native mode\" works much better, but on Windows guests, it requires that you (1) use an smb\/cifs file server (Windows Server, Linux with Samba) and (2) have openslx.exe setup to autorun in the VM (this is already configured for bwLehrpool templates).",
diff --git a/modules-available/sysconfig/templates/ad-selfsearch.html b/modules-available/sysconfig/templates/ad-selfsearch.html
index 39ef3e59..6c5bcb8c 100644
--- a/modules-available/sysconfig/templates/ad-selfsearch.html
+++ b/modules-available/sysconfig/templates/ad-selfsearch.html
@@ -39,6 +39,7 @@
<input name="ssl" value="on" type="hidden">
<input type="hidden" name="certificate" value="{{certificate}}">
{{/ssl}}
+ <input name="fixnumeric" value="{{fixnumeric}}" type="hidden">
<button type="submit" class="btn btn-primary">&laquo; {{lang_back}}</button>
</form>
</div>
@@ -59,6 +60,7 @@
<input name="ssl" value="on" type="hidden">
<input type="hidden" name="certificate" value="{{certificate}}">
{{/ssl}}
+ <input name="fixnumeric" value="{{fixnumeric}}" type="hidden">
<input name="fingerprint" value="{{fingerprint}}" type="hidden">
<button id="nextbutton" type="submit" class="btn btn-primary" style="display:none">{{lang_skip}} &raquo;</button>
</form>
diff --git a/modules-available/sysconfig/templates/ad-start.html b/modules-available/sysconfig/templates/ad-start.html
index efe7e64e..1f8e1e01 100644
--- a/modules-available/sysconfig/templates/ad-start.html
+++ b/modules-available/sysconfig/templates/ad-start.html
@@ -55,10 +55,21 @@
<br>
<div>
<label>
+ <input type="checkbox" name="fixnumeric" {{#fixnumeric}}checked{{/fixnumeric}}> {{lang_fixNumeric}}
+ </label>
+ <div>
+ <i>{{lang_fixNumericDescription}}</i>
+ </div>
+ </div>
+ <br>
+ <div>
+ <label>
<input type="checkbox" name="ssl" onchange="$('#cert-box').css('display', this.checked ? '' : 'none')" {{#ssl}}checked{{/ssl}}> {{lang_ssl}}
</label>
+ <div>
+ <i>{{lang_sslDescription}}</i>
+ </div>
</div>
- <i>{{lang_sslDescription}}</i>
<br/>
<hr>
<div class="btn-group">
diff --git a/modules-available/sysconfig/templates/ad_ldap-checkconnection.html b/modules-available/sysconfig/templates/ad_ldap-checkconnection.html
index 0ee596ab..f3194308 100644
--- a/modules-available/sysconfig/templates/ad_ldap-checkconnection.html
+++ b/modules-available/sysconfig/templates/ad_ldap-checkconnection.html
@@ -26,6 +26,7 @@
<input name="ssl" value="on" type="hidden">
<input type="hidden" name="certificate" value="{{certificate}}">
{{/ssl}}
+ <input name="fixnumeric" value="{{fixnumeric}}" type="hidden">
<button type="submit" class="btn btn-primary">&laquo; {{lang_back}}</button>
</form>
</div>
@@ -46,6 +47,7 @@
<input id="fingerprint" name="fingerprint" value="" type="hidden">
<input id="certificate" type="hidden" name="certificate" value="{{certificate}}">
{{/ssl}}
+ <input name="fixnumeric" value="{{fixnumeric}}" type="hidden">
<input name="originalbinddn" value="{{binddn}}" type="hidden">
<button id="nextbutton" type="submit" class="btn btn-primary" style="display:none">{{lang_next}} &raquo;</button>
</form>
diff --git a/modules-available/sysconfig/templates/ad_ldap-checkcredentials.html b/modules-available/sysconfig/templates/ad_ldap-checkcredentials.html
index 0586209b..bf151da3 100644
--- a/modules-available/sysconfig/templates/ad_ldap-checkcredentials.html
+++ b/modules-available/sysconfig/templates/ad_ldap-checkcredentials.html
@@ -22,6 +22,7 @@
<input name="ssl" value="on" type="hidden">
<input type="hidden" name="certificate" value="{{certificate}}">
{{/ssl}}
+ <input name="fixnumeric" value="{{fixnumeric}}" type="hidden">
<button type="submit" class="btn btn-primary">&laquo; {{lang_back}}</button>
</form>
</div>
@@ -41,6 +42,7 @@
<input name="ssl" value="on" type="hidden">
<input type="hidden" name="certificate" value="{{certificate}}">
{{/ssl}}
+ <input name="fixnumeric" value="{{fixnumeric}}" type="hidden">
<input name="fingerprint" value="{{fingerprint}}" type="hidden">
<input name="originalbinddn" value="{{binddn}}" type="hidden">
<button id="nextbutton" type="submit" class="btn btn-primary" style="display:none">{{lang_skip}} &raquo;</button>
diff --git a/modules-available/sysconfig/templates/ad_ldap-homedir.html b/modules-available/sysconfig/templates/ad_ldap-homedir.html
index 2ced563d..10a43030 100644
--- a/modules-available/sysconfig/templates/ad_ldap-homedir.html
+++ b/modules-available/sysconfig/templates/ad_ldap-homedir.html
@@ -14,11 +14,12 @@
<input name="ssl" value="on" type="hidden">
<input type="hidden" name="certificate" value="{{certificate}}">
{{/ssl}}
+ <input name="fixnumeric" value="{{fixnumeric}}" type="hidden">
<input name="fingerprint" value="{{fingerprint}}" type="hidden">
<div class="slx-bold">{{lang_credentialPassing}}</div>
<div class="checkbox">
- <input type="checkbox" class="form-control" id="inputcredentialPassthrough" name="credentialPassthrough">
+ <input type="checkbox" class="form-control" id="inputcredentialPassthrough" name="credentialPassthrough" {{credentialPassthrough_c}}>
<label for="inputcredentialPassthrough">{{lang_credentialPassing}}</label>
</div>
<div><i>{{lang_credPassingNotes}}</i></div>
@@ -42,7 +43,7 @@
<div class="form-group row">
<label for="inputshareDomain" class="control-label col-xs-4">{{lang_shareDomainLabel}}</label>
<div class="col-xs-8">
- <input type="text" class="form-control" id="inputshareDomain" name="shareDomain">
+ <input type="text" class="form-control" id="inputshareDomain" name="shareDomain" value="{{shareDomain}}">
<p><i>{{lang_shareDomainNote}}</i></p>
</div>
</div>
diff --git a/modules-available/sysconfig/templates/ldap-start.html b/modules-available/sysconfig/templates/ldap-start.html
index 683762f1..22f4e2fa 100644
--- a/modules-available/sysconfig/templates/ldap-start.html
+++ b/modules-available/sysconfig/templates/ldap-start.html
@@ -52,6 +52,15 @@
<br>
<div>
<label>
+ <input type="checkbox" name="fixnumeric" {{#fixnumeric}}checked{{/fixnumeric}}> {{lang_fixNumeric}}
+ </label>
+ <div>
+ <i>{{lang_fixNumericDescription}}</i>
+ </div>
+ </div>
+ <br>
+ <div>
+ <label>
<input type="checkbox" name="ssl" onchange="$('#cert-box').css('display', this.checked ? '' : 'none')" {{#ssl}}checked{{/ssl}}> {{lang_ssl}}
</label>
</div>
diff --git a/modules-available/syslog/baseconfig/getconfig.inc.php b/modules-available/syslog/baseconfig/getconfig.inc.php
index 9706eea1..23102af9 100644
--- a/modules-available/syslog/baseconfig/getconfig.inc.php
+++ b/modules-available/syslog/baseconfig/getconfig.inc.php
@@ -1,4 +1,4 @@
<?php
// Remote log URL
-$configVars["SLX_REMOTE_LOG"] = 'http://' . $_SERVER['SERVER_ADDR'] . $_SERVER['SCRIPT_NAME'] . '?do=clientlog';
+ConfigHolder::add("SLX_REMOTE_LOG", 'http://' . $_SERVER['SERVER_ADDR'] . $_SERVER['SCRIPT_NAME'] . '?do=clientlog');
diff --git a/modules-available/syslog/clientscript.js b/modules-available/syslog/clientscript.js
deleted file mode 100644
index 3d652628..00000000
--- a/modules-available/syslog/clientscript.js
+++ /dev/null
@@ -1,8 +0,0 @@
-/*
- * bootstrap-tagsinput v0.3.9 by Tim Schlechter
- *
- */
-!function(a){"use strict";function b(b,c){this.itemsArray=[],this.$element=a(b),this.$element.hide(),this.isSelect="SELECT"===b.tagName,this.multiple=this.isSelect&&b.hasAttribute("multiple"),this.objectItems=c&&c.itemValue,this.placeholderText=b.hasAttribute("placeholder")?this.$element.attr("placeholder"):"",this.inputSize=Math.max(1,this.placeholderText.length),this.$container=a('<div class="bootstrap-tagsinput"></div>'),this.$input=a('<input size="'+this.inputSize+'" type="text" placeholder="'+this.placeholderText+'"/>').appendTo(this.$container),this.$element.after(this.$container),this.build(c)}function c(a,b){if("function"!=typeof a[b]){var c=a[b];a[b]=function(a){return a[c]}}}function d(a,b){if("function"!=typeof a[b]){var c=a[b];a[b]=function(){return c}}}function e(a){return a?h.text(a).html():""}function f(a){var b=0;if(document.selection){a.focus();var c=document.selection.createRange();c.moveStart("character",-a.value.length),b=c.text.length}else(a.selectionStart||"0"==a.selectionStart)&&(b=a.selectionStart);return b}var g={tagClass:function(){return"label label-info"},itemValue:function(a){return a?a.toString():a},itemText:function(a){return this.itemValue(a)},freeInput:!0,maxTags:void 0,confirmKeys:[13],onTagExists:function(a,b){b.hide().fadeIn()}};b.prototype={constructor:b,add:function(b,c){var d=this;if(!(d.options.maxTags&&d.itemsArray.length>=d.options.maxTags||b!==!1&&!b)){if("object"==typeof b&&!d.objectItems)throw"Can't add objects when itemValue option is not set";if(!b.toString().match(/^\s*$/)){if(d.isSelect&&!d.multiple&&d.itemsArray.length>0&&d.remove(d.itemsArray[0]),"string"==typeof b&&"INPUT"===this.$element[0].tagName){var f=b.split(",");if(f.length>1){for(var g=0;g<f.length;g++)this.add(f[g],!0);return c||d.pushVal(),void 0}}var h=d.options.itemValue(b),i=d.options.itemText(b),j=d.options.tagClass(b),k=a.grep(d.itemsArray,function(a){return d.options.itemValue(a)===h})[0];if(k){if(d.options.onTagExists){var l=a(".tag",d.$container).filter(function(){return a(this).data("item")===k});d.options.onTagExists(b,l)}}else{d.itemsArray.push(b);var m=a('<span class="tag '+e(j)+'">'+e(i)+'<span data-role="remove"></span></span>');if(m.data("item",b),d.findInputWrapper().before(m),m.after(" "),d.isSelect&&!a('option[value="'+escape(h)+'"]',d.$element)[0]){var n=a("<option selected>"+e(i)+"</option>");n.data("item",b),n.attr("value",h),d.$element.append(n)}c||d.pushVal(),d.options.maxTags===d.itemsArray.length&&d.$container.addClass("bootstrap-tagsinput-max"),d.$element.trigger(a.Event("itemAdded",{item:b}))}}}},remove:function(b,c){var d=this;d.objectItems&&(b="object"==typeof b?a.grep(d.itemsArray,function(a){return d.options.itemValue(a)==d.options.itemValue(b)})[0]:a.grep(d.itemsArray,function(a){return d.options.itemValue(a)==b})[0]),b&&(a(".tag",d.$container).filter(function(){return a(this).data("item")===b}).remove(),a("option",d.$element).filter(function(){return a(this).data("item")===b}).remove(),d.itemsArray.splice(a.inArray(b,d.itemsArray),1)),c||d.pushVal(),d.options.maxTags>d.itemsArray.length&&d.$container.removeClass("bootstrap-tagsinput-max"),d.$element.trigger(a.Event("itemRemoved",{item:b}))},removeAll:function(){var b=this;for(a(".tag",b.$container).remove(),a("option",b.$element).remove();b.itemsArray.length>0;)b.itemsArray.pop();b.pushVal(),b.options.maxTags&&!this.isEnabled()&&this.enable()},refresh:function(){var b=this;a(".tag",b.$container).each(function(){var c=a(this),d=c.data("item"),f=b.options.itemValue(d),g=b.options.itemText(d),h=b.options.tagClass(d);if(c.attr("class",null),c.addClass("tag "+e(h)),c.contents().filter(function(){return 3==this.nodeType})[0].nodeValue=e(g),b.isSelect){var i=a("option",b.$element).filter(function(){return a(this).data("item")===d});i.attr("value",f)}})},items:function(){return this.itemsArray},pushVal:function(){var b=this,c=a.map(b.items(),function(a){return b.options.itemValue(a).toString()});b.$element.val(c,!0).trigger("change")},build:function(b){var e=this;e.options=a.extend({},g,b);var h=e.options.typeahead||{};e.objectItems&&(e.options.freeInput=!1),c(e.options,"itemValue"),c(e.options,"itemText"),c(e.options,"tagClass"),e.options.source&&(h.source=e.options.source),h.source&&a.fn.typeahead&&(d(h,"source"),e.$input.typeahead({source:function(b,c){function d(a){for(var b=[],d=0;d<a.length;d++){var g=e.options.itemText(a[d]);f[g]=a[d],b.push(g)}c(b)}this.map={};var f=this.map,g=h.source(b);a.isFunction(g.success)?g.success(d):a.when(g).then(d)},updater:function(a){e.add(this.map[a])},matcher:function(a){return-1!==a.toLowerCase().indexOf(this.query.trim().toLowerCase())},sorter:function(a){return a.sort()},highlighter:function(a){var b=new RegExp("("+this.query+")","gi");return a.replace(b,"<strong>$1</strong>")}})),e.$container.on("click",a.proxy(function(){e.$input.focus()},e)),e.$container.on("keydown","input",a.proxy(function(b){var c=a(b.target),d=e.findInputWrapper();switch(b.which){case 8:if(0===f(c[0])){var g=d.prev();g&&e.remove(g.data("item"))}break;case 46:if(0===f(c[0])){var h=d.next();h&&e.remove(h.data("item"))}break;case 37:var i=d.prev();0===c.val().length&&i[0]&&(i.before(d),c.focus());break;case 39:var j=d.next();0===c.val().length&&j[0]&&(j.after(d),c.focus());break;default:e.options.freeInput&&a.inArray(b.which,e.options.confirmKeys)>=0&&(e.add(c.val()),c.val(""),b.preventDefault())}c.attr("size",Math.max(this.inputSize,c.val().length))},e)),e.$container.on("click","[data-role=remove]",a.proxy(function(b){e.remove(a(b.target).closest(".tag").data("item"))},e)),e.options.itemValue===g.itemValue&&("INPUT"===e.$element[0].tagName?e.add(e.$element.val()):a("option",e.$element).each(function(){e.add(a(this).attr("value"),!0)}))},destroy:function(){var a=this;a.$container.off("keypress","input"),a.$container.off("click","[role=remove]"),a.$container.remove(),a.$element.removeData("tagsinput"),a.$element.show()},focus:function(){this.$input.focus()},input:function(){return this.$input},findInputWrapper:function(){for(var b=this.$input[0],c=this.$container[0];b&&b.parentNode!==c;)b=b.parentNode;return a(b)}},a.fn.tagsinput=function(c,d){var e=[];return this.each(function(){var f=a(this).data("tagsinput");if(f){var g=f[c](d);void 0!==g&&e.push(g)}else f=new b(this,c),a(this).data("tagsinput",f),e.push(f),"SELECT"===this.tagName&&a("option",a(this)).attr("selected","selected"),a(this).val(a(this).val())}),"string"==typeof c?e.length>1?e:e[0]:e},a.fn.tagsinput.Constructor=b;var h=a("<div />");a(function(){a("input[data-role=tagsinput], select[multiple][data-role=tagsinput]").tagsinput()})}(window.jQuery);
-/*
-//@ sourceMappingURL=bootstrap-tagsinput.min.js.map
-*/ \ No newline at end of file
diff --git a/modules-available/syslog/config.json b/modules-available/syslog/config.json
index 6778348d..2b718a8e 100644
--- a/modules-available/syslog/config.json
+++ b/modules-available/syslog/config.json
@@ -1,3 +1,4 @@
{
- "category":"main.status"
+ "category":"main.status",
+ "dependencies":["js_selectize"]
}
diff --git a/modules-available/syslog/page.inc.php b/modules-available/syslog/page.inc.php
index 486e0248..f2bc4854 100644
--- a/modules-available/syslog/page.inc.php
+++ b/modules-available/syslog/page.inc.php
@@ -15,6 +15,12 @@ class Page_SysLog extends Page
protected function doRender()
{
+ $cutoff = strtotime('-1 month');
+ $res = Database::simpleQuery("SELECT logtypeid, Count(*) AS counter FROM clientlog WHERE dateline > $cutoff GROUP BY logtypeid ORDER BY counter ASC");
+ $types = array();
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ $types[$row['logtypeid']] = $row;
+ }
if (isset($_GET['filter'])) {
$filter = $_GET['filter'];
$not = isset($_GET['not']) ? 'NOT' : '';
@@ -35,6 +41,9 @@ class Page_SysLog extends Page
$filterItem = preg_replace('/[^a-z0-9_\-]/', '', trim($filterItem));
if (empty($filterItem) || in_array($filterItem, $whereClause)) continue;
$whereClause[] = "'$filterItem'";
+ if (!isset($types[$filterItem])) {
+ $types[$filterItem] = ['logtypeid' => $filterItem, 'counter' => ''];
+ }
}
if (!empty($whereClause)) $whereClause = ' WHERE logtypeid ' . $not . ' IN (' . implode(', ', $whereClause) . ')';
}
@@ -67,7 +76,8 @@ class Page_SysLog extends Page
$paginate->render('page-syslog', array(
'filter' => $filter,
'not' => $not,
- 'list' => $lines
+ 'list' => $lines,
+ 'types' => json_encode(array_values($types)),
));
}
diff --git a/modules-available/syslog/style.css b/modules-available/syslog/style.css
deleted file mode 100644
index 98cfa7f3..00000000
--- a/modules-available/syslog/style.css
+++ /dev/null
@@ -1,45 +0,0 @@
-.bootstrap-tagsinput {
- background-color: #fff;
- border: 1px solid #ccc;
- box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
- display: inline-block;
- padding: 4px 6px;
- margin-bottom: 10px;
- color: #555;
- vertical-align: middle;
- border-radius: 4px;
- max-width: 100%;
- line-height: 22px;
-}
-.bootstrap-tagsinput input {
- border: none;
- box-shadow: none;
- outline: none;
- background-color: transparent;
- padding: 0;
- margin: 0;
- width: auto !important;
- max-width: inherit;
-}
-.bootstrap-tagsinput input:focus {
- border: none;
- box-shadow: none;
-}
-.bootstrap-tagsinput .tag {
- margin-right: 2px;
- color: white;
-}
-.bootstrap-tagsinput .tag [data-role="remove"] {
- margin-left: 8px;
- cursor: pointer;
-}
-.bootstrap-tagsinput .tag [data-role="remove"]:after {
- content: "x";
- padding: 0px 2px;
-}
-.bootstrap-tagsinput .tag [data-role="remove"]:hover {
- box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
-}
-.bootstrap-tagsinput .tag [data-role="remove"]:hover:active {
- box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
-}
diff --git a/modules-available/syslog/templates/page-syslog.html b/modules-available/syslog/templates/page-syslog.html
index 98e94291..cb20a93a 100644
--- a/modules-available/syslog/templates/page-syslog.html
+++ b/modules-available/syslog/templates/page-syslog.html
@@ -1,35 +1,47 @@
<h1>{{lang_clientLog}}</h1>
+<style type="text/css">
+ .selectize-dropdown {
+ max-width: 500px;
+ }
+</style>
<form method="post" action="?do=SysLog">
<input type="hidden" name="token" value="{{token}}">
+ <div class="pull-left">
+ <label for="filterstring">{{lang_filter}}</label>
+ </div>
+ <div class="clearfix"></div>
<div class="input-group">
- <span class="input-group-addon">{{lang_filter}}</span>
- <input id="filterstring" type="text" placeholder="id" value="{{filter}}" name="filter" data-role="tagsinput" />
- <span class="input-group-addon">
- <input type="checkbox" name="not" {{#not}}checked="checked"{{/not}}> {{lang_not}}
- </span>
+ <input id="filterstring" placeholder="id" value="{{filter}}" name="filter">
<span class="input-group-btn">
- <button class="btn btn-default" type="submit">{{lang_go}}</button>
- </span>
+ <button class="btn btn-default" type="submit">{{lang_go}}</button>
+ </span>
+ </div>
+ <div class="pull-left">
+ <div class="checkbox">
+ <input id="notbox" type="checkbox" name="not" {{#not}}checked="checked"{{/not}}>
+ <label for="notbox">{{lang_not}}</label>
+ </div>
</div>
</form>
{{{pagenav}}}
<table class="table table-striped table-condensed">
<thead>
- <th width="1"></th>
- <th>{{lang_when}}</th>
- <th>{{lang_client}}</th>
- <th>{{lang_event}}</th>
- <th width="1">{{lang_details}}</th>
+ <th width="1"></th>
+ <th>{{lang_when}}</th>
+ <th>{{lang_client}}</th>
+ <th>{{lang_event}}</th>
+ <th width="1">{{lang_details}}</th>
</thead>
<tbody>
{{#list}}
<tr>
- <td><span class="glyphicon {{icon}}" title="{{logtypeid}}" onclick="$('#filterstring').tagsinput('add', '{{logtypeid}}')"></span></td>
+ <td><span class="type-button glyphicon {{icon}}" title="{{logtypeid}}"></span></td>
<td class="text-right" nowrap="nowrap">{{date}}</td>
<td>{{clientip}}</td>
<td>{{description}}</td>
<td>{{#extra}}
- <a class="btn btn-default btn-xs pull-left" onclick="$('#details-body').html($('#extra-{{logid}}').html())" data-toggle="modal" data-target="#myModal">&raquo;</a>
+ <a class="btn btn-default btn-xs pull-left" onclick="$('#details-body').html($('#extra-{{logid}}').html())"
+ data-toggle="modal" data-target="#myModal">&raquo;</a>
<div class="hidden" id="extra-{{logid}}">{{extra}}</div>
{{/extra}}</td>
</tr>
@@ -39,20 +51,53 @@
{{{pagenav}}}
<div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
- <div class="modal-dialog modal-lg">
- <div class="modal-content">
- <div class="modal-header">
- <button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">&times;</span><span class="sr-only">Close</span></button>
- <h4 class="modal-title" id="myModalLabel">{{lang_details}}</h4>
- </div>
- <div class="modal-body">
- <pre id="details-body"></pre>
- </div>
- <div class="modal-footer">
- <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
- </div>
- </div>
- </div>
+ <div class="modal-dialog modal-lg">
+ <div class="modal-content">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">&times;</span><span
+ class="sr-only">Close</span></button>
+ <h4 class="modal-title" id="myModalLabel">{{lang_details}}</h4>
+ </div>
+ <div class="modal-body">
+ <pre id="details-body"></pre>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
+ </div>
+ </div>
+ </div>
</div>
+<script type="application/javascript"><!--
+document.addEventListener('DOMContentLoaded', function () {
+
+ function renderOption(item, escape) {
+ return '<div><div class="pull-right badge">' + escape(item.counter) + '</div>' + escape(item.logtypeid) + '</div>';
+ }
+
+ function renderSelected(item, escape) {
+ return '<div>' + escape(item.logtypeid) + '</div>';
+ }
+
+ var opts = {{{types}}} || [];
+ var $box = $('#filterstring').selectize({
+ options: opts,
+ plugins: ["remove_button"],
+ valueField: 'logtypeid',
+ searchField: "logtypeid",
+ openOnFocus: true,
+ create: true,
+ render: {option: renderOption, item: renderSelected},
+ maxItems: null,
+ highlight: false
+ });
+ var inst = $box[0].selectize;
+
+ $('.type-button').click(function() {
+ inst.addOption({logtypeid: this.title, counter: ''});
+ inst.addItem(this.title, true);
+ });
+});
+//--></script>
+
diff --git a/modules-available/systemstatus/hooks/main-warning.inc.php b/modules-available/systemstatus/hooks/main-warning.inc.php
new file mode 100644
index 00000000..406ae73c
--- /dev/null
+++ b/modules-available/systemstatus/hooks/main-warning.inc.php
@@ -0,0 +1,7 @@
+<?php
+
+if (file_exists('/run/reboot-required.pkgs')) {
+ $lines = file('/run/reboot-required.pkgs', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
+ $lines = array_unique($lines);
+ Message::addInfo('systemstatus.update-reboot-required', true, implode(', ', $lines));
+} \ No newline at end of file
diff --git a/modules-available/systemstatus/lang/de/messages.json b/modules-available/systemstatus/lang/de/messages.json
index 7838acfa..1a6df1b3 100644
--- a/modules-available/systemstatus/lang/de/messages.json
+++ b/modules-available/systemstatus/lang/de/messages.json
@@ -1,3 +1,4 @@
{
- "reboot-unconfirmed": "Sicherheitsabfrage zum Reboot nicht best\u00e4tigt"
+ "reboot-unconfirmed": "Sicherheitsabfrage zum Reboot nicht best\u00e4tigt",
+ "update-reboot-required": "Das Update der folgenden Pakete erfordert einen Reboot des Servers: {{0}}"
} \ No newline at end of file
diff --git a/modules-available/systemstatus/lang/de/module.json b/modules-available/systemstatus/lang/de/module.json
index e69c3fb0..dd1a115c 100644
--- a/modules-available/systemstatus/lang/de/module.json
+++ b/modules-available/systemstatus/lang/de/module.json
@@ -1,3 +1,8 @@
{
- "module_name": "System-Status"
+ "module_name": "System-Status",
+ "tab_DmsdLog": "bwLehrpool-Suite Server Log",
+ "tab_LdadpLog": "LDAP\/AD",
+ "tab_LighttpdLog": "lighttpd Log",
+ "tab_Netstat": "netstat -tulpn",
+ "tab_PsList": "ps auxf"
} \ No newline at end of file
diff --git a/modules-available/systemstatus/lang/de/template-tags.json b/modules-available/systemstatus/lang/de/template-tags.json
index 3b929a6b..1333fb7d 100644
--- a/modules-available/systemstatus/lang/de/template-tags.json
+++ b/modules-available/systemstatus/lang/de/template-tags.json
@@ -1,22 +1,21 @@
{
"lang_addressConfiguration": "Adresskonfiguration",
- "lang_advanced": "Erweitert \/ Debug",
"lang_attention": "Achtung!",
"lang_average": "Durchschnitt",
"lang_capacity": "Kapazit\u00e4t",
"lang_cpuLoad": "CPU-Last",
- "lang_dmsdLog": "dmsd Log",
+ "lang_dmsdUnreachable": "dmsd nicht erreichbar",
+ "lang_downloads": "Downloads",
"lang_foundStore": "Vorgefunden:",
"lang_free": "Frei",
"lang_goToStoreConf": "Zur VM-Store-Konfiguration wechseln",
"lang_iAmSure": "Ja, ich bin sicher",
- "lang_ldadpLog": "AD\/LDAP-Proxy Log",
- "lang_lighttpdLog": "lighttpd-Error Log",
"lang_logicCPUs": "Logische CPUs",
"lang_maintenance": "Maintenance",
"lang_notDetermined": "Konnte nicht ermittelt werden",
"lang_occupied": "Belegt",
"lang_onlyOS": "Nur OS",
+ "lang_overview": "\u00dcbersicht",
"lang_ramUsage": "RAM-Nutzung",
"lang_services": "Dienste",
"lang_space": "Speicherplatz",
@@ -28,6 +27,8 @@
"lang_systemPartition": "Systempartition",
"lang_systemStoreError": "Fehler beim Ermitteln des verf\u00fcgbaren Systemspeichers",
"lang_total": "Gesamt",
+ "lang_updatedPackages": "Ausstehende Updates",
+ "lang_uploads": "Uploads",
"lang_uptimeOS": "OS Uptime",
"lang_vmStore": "VM-Speicher",
"lang_vmStoreError": "Fehler beim Ermitteln des verf\u00fcgbaren Speicherplatzes am VM-Speicherort. Bitte \u00fcberpr\u00fcfen Sie die Konfiguration."
diff --git a/modules-available/systemstatus/lang/en/messages.json b/modules-available/systemstatus/lang/en/messages.json
index 5aaf57ec..5098eb76 100644
--- a/modules-available/systemstatus/lang/en/messages.json
+++ b/modules-available/systemstatus/lang/en/messages.json
@@ -1,3 +1,4 @@
{
- "reboot-unconfirmed": "Confirmation prompt to reboot not confirmed"
+ "reboot-unconfirmed": "Confirmation prompt to reboot not confirmed",
+ "update-reboot-required": "Updating the following system packages requires reboot: {{0}}"
} \ No newline at end of file
diff --git a/modules-available/systemstatus/lang/en/module.json b/modules-available/systemstatus/lang/en/module.json
index bb201773..9f6d937a 100644
--- a/modules-available/systemstatus/lang/en/module.json
+++ b/modules-available/systemstatus/lang/en/module.json
@@ -1,3 +1,8 @@
{
- "module_name": "System Status"
+ "module_name": "System Status",
+ "tab_DmsdLog": "bwLehrpool-Suite log",
+ "tab_LdadpLog": "LDAP\/AD",
+ "tab_LighttpdLog": "lighttpd log",
+ "tab_Netstat": "netstat -tulpn",
+ "tab_PsList": "ps auxf"
} \ No newline at end of file
diff --git a/modules-available/systemstatus/lang/en/template-tags.json b/modules-available/systemstatus/lang/en/template-tags.json
index b8730411..46b7e18b 100644
--- a/modules-available/systemstatus/lang/en/template-tags.json
+++ b/modules-available/systemstatus/lang/en/template-tags.json
@@ -1,22 +1,21 @@
{
"lang_addressConfiguration": "Address Configuration",
- "lang_advanced": "Advanced \/ Debug",
"lang_attention": "Attention!",
"lang_average": "Average",
"lang_capacity": "Capacity",
"lang_cpuLoad": "CPU Load",
- "lang_dmsdLog": "dmsd log",
+ "lang_dmsdUnreachable": "dmsd not reachable",
+ "lang_downloads": "Downloads",
"lang_foundStore": "Found:",
"lang_free": "Free",
"lang_goToStoreConf": "Go to VM store configuration",
"lang_iAmSure": "Yes, I am sure",
- "lang_ldadpLog": "AD\/LDAP proxy log",
- "lang_lighttpdLog": "lighttpd-error log",
"lang_logicCPUs": "Logic CPUs",
"lang_maintenance": "Maintenance",
"lang_notDetermined": "Could not be determined",
"lang_occupied": "Occupied",
"lang_onlyOS": "OS Only",
+ "lang_overview": "Overview",
"lang_ramUsage": "RAM Usage",
"lang_services": "Services",
"lang_space": "Space",
@@ -28,6 +27,8 @@
"lang_systemPartition": "System Partition",
"lang_systemStoreError": "Error querying available system storage",
"lang_total": "Total",
+ "lang_updatedPackages": "Pending updates",
+ "lang_uploads": "Uploads",
"lang_uptimeOS": "OS Uptime",
"lang_vmStore": "VM Store",
"lang_vmStoreError": "Error determining available space of the VM storage. Please check the configuration."
diff --git a/modules-available/systemstatus/page.inc.php b/modules-available/systemstatus/page.inc.php
index b9fef0fd..f7ec7022 100644
--- a/modules-available/systemstatus/page.inc.php
+++ b/modules-available/systemstatus/page.inc.php
@@ -29,6 +29,14 @@ class Page_SystemStatus extends Page
if (is_array($this->rebootTask) && isset($this->rebootTask['id'])) {
$data['rebootTask'] = $this->rebootTask['id'];
}
+ $tabs = array('DmsdLog', 'Netstat', 'PsList', 'LdadpLog', 'LighttpdLog');
+ $data['tabs'] = array();
+ foreach ($tabs as $tab) {
+ $data['tabs'][] = array(
+ 'type' => $tab,
+ 'name' => Dictionary::translate('tab_' . $tab)
+ );
+ }
Render::addTemplate('_page', $data);
}
@@ -51,17 +59,22 @@ class Page_SystemStatus extends Page
protected function ajaxDmsdUsers()
{
$ret = Download::asStringPost('http://127.0.0.1:9080/status/fileserver', false, 2, $code);
+ $args = array();
if ($code != 200) {
- Header('HTTP/1.1 502 Internal Server Error');
- die('Internal Server Wurst');
- }
- $data = @json_decode($ret, true);
- if (is_array($data)) {
- $ret = 'Uploads: ' . $data['activeUploads'] . ', Downloads: ' . $data['activeDownloads'];
+ $args['dmsd_error'] = true;
} else {
- $ret = '???';
+ $data = @json_decode($ret, true);
+ if (is_array($data)) {
+ $args['uploads'] = $data['activeUploads'];
+ $args['downloads'] = $data['activeDownloads'];
+ }
+ }
+ if (file_exists('/run/reboot-required.pkgs')) {
+ $lines = file('/run/reboot-required.pkgs', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
+ $lines = array_unique($lines);
+ $args['packages'] = implode(', ', $lines);
}
- die($ret);
+ echo Render::parse('ajax-reboot', $args);
}
protected function ajaxDiskStat()
diff --git a/modules-available/systemstatus/templates/_page.html b/modules-available/systemstatus/templates/_page.html
index 0573a20c..48a7e5c4 100644
--- a/modules-available/systemstatus/templates/_page.html
+++ b/modules-available/systemstatus/templates/_page.html
@@ -2,137 +2,126 @@
<div data-tm-id="{{rebootTask}}" data-tm-log="messages">Reboot...</div>
{{/rebootTask}}
-<div class="row">
+<ul class="nav nav-tabs tabs-up">
+ <li class="active">
+ <a href="#id-default_pane" id="id-default" class="active" data-toggle="tab" role="tab">
+ {{lang_overview}}
+ </a>
+ </li>
+ {{#tabs}}
+ <li>
+ <a href="#id-{{type}}_pane" class="ajax-tab" id="id-{{type}}" data-toggle="tab" role="tab">
+ {{name}}
+ </a>
+ </li>
+ {{/tabs}}
+</ul>
- <div class="col-sm-6">
- <div class="panel panel-default">
- <div class="panel-heading">
- {{lang_space}}
- </div>
- <div class="panel-body" id="diskstat">
- <span class="glyphicon glyphicon-refresh slx-rotation"></span>
- </div>
- </div>
- </div>
+<div class="tab-content">
+
+ <div class="tab-pane active" id="id-default_pane">
+ <div class="row">
- <div class="col-sm-6">
- <div class="panel panel-default">
- <div class="panel-heading">
- {{lang_services}}
+ <div class="col-sm-6">
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ {{lang_space}}
+ </div>
+ <div class="panel-body" id="diskstat">
+ <span class="glyphicon glyphicon-refresh slx-rotation"></span>
+ </div>
+ </div>
</div>
- <div class="panel-body" id="services">
- <span class="glyphicon glyphicon-refresh slx-rotation"></span>
+
+ <div class="col-sm-6">
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ {{lang_services}}
+ </div>
+ <div class="panel-body" id="services">
+ <span class="glyphicon glyphicon-refresh slx-rotation"></span>
+ </div>
+ </div>
</div>
- </div>
- </div>
-</div>
+ </div>
-<div class="row">
+ <div class="row">
- <div class="col-md-6">
- <div class="panel panel-default">
- <div class="panel-heading">
- {{lang_addressConfiguration}}
- </div>
- <div class="panel-body" id="addresses">
- <span class="glyphicon glyphicon-refresh slx-rotation"></span>
+ <div class="col-md-6">
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ {{lang_addressConfiguration}}
+ </div>
+ <div class="panel-body" id="addresses">
+ <span class="glyphicon glyphicon-refresh slx-rotation"></span>
+ </div>
+ </div>
</div>
- </div>
- </div>
- <div class="col-md-6">
- <div class="panel panel-default">
- <div class="panel-heading">
- {{lang_system}}
- </div>
- <div class="panel-body" id="systeminfo">
- <span class="glyphicon glyphicon-refresh slx-rotation"></span>
+ <div class="col-md-6">
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ {{lang_system}}
+ </div>
+ <div class="panel-body" id="systeminfo">
+ <span class="glyphicon glyphicon-refresh slx-rotation"></span>
+ </div>
+ </div>
</div>
- </div>
- </div>
-</div>
+ </div>
-<div class="row">
+ <div class="row">
- <div class="col-md-6">
- <div class="panel panel-default">
- <div class="panel-heading">
- {{lang_maintenance}}
- </div>
- <div class="panel-body">
- <form class="form-adduser" action="?do=SystemStatus" method="post">
- <input type="hidden" name="token" value="{{token}}">
- <input type="hidden" name="action" value="reboot">
- <div>Server Reboot</div>
- <label><input type="checkbox" name="confirm" value="yep"> {{lang_iAmSure}}</label>
- <button class="btn btn-warning btn-xs" type="submit">Reboot</button>
- </form>
- <div id="dmsd-users"></div>
+ <div class="col-md-6">
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ {{lang_maintenance}}
+ </div>
+ <div class="panel-body">
+ <form class="form-adduser" action="?do=SystemStatus" method="post">
+ <input type="hidden" name="token" value="{{token}}">
+ <input type="hidden" name="action" value="reboot">
+ <div>Server Reboot</div>
+ <label><input type="checkbox" name="confirm" value="yep"> {{lang_iAmSure}}</label>
+ <button class="btn btn-warning btn-xs" type="submit">Reboot</button>
+ </form>
+ <div id="dmsd-users"></div>
+ </div>
+ </div>
</div>
+
</div>
</div>
-</div>
-
-<h3>{{lang_advanced}}</h3>
-
-<div class="panel panel-default">
- <div class="panel-heading">
- {{lang_dmsdLog}}
- </div>
- <div class="panel-body" id="dmsd-log">
- <span class="glyphicon glyphicon-refresh slx-rotation"></span>
- </div>
-</div>
-<div class="panel panel-default">
- <div class="panel-heading">
- {{lang_ldadpLog}}
- </div>
- <div class="panel-body" id="ldadp-log">
- <span class="glyphicon glyphicon-refresh slx-rotation"></span>
- </div>
-</div>
-<div class="panel panel-default">
- <div class="panel-heading">
- {{lang_lighttpdLog}}
- </div>
- <div class="panel-body" id="lighttpd-log">
- <span class="glyphicon glyphicon-refresh slx-rotation"></span>
- </div>
-</div>
-<div class="panel panel-default">
- <div class="panel-heading">
- netstat -tulpn
- </div>
- <div class="panel-body" id="netstat">
- <span class="glyphicon glyphicon-refresh slx-rotation"></span>
- </div>
-</div>
-<div class="panel panel-default">
- <div class="panel-heading">
- ps auxf
- </div>
- <div class="panel-body" id="pslist">
+{{#tabs}}
+ <div class="tab-pane" id="id-{{type}}_pane">
<span class="glyphicon glyphicon-refresh slx-rotation"></span>
</div>
+{{/tabs}}
</div>
-<script type="text/javascript">
+<div>&nbsp;</div>
+
+<script type="text/javascript"><!--
document.addEventListener("DOMContentLoaded", function() {
$('#diskstat').load('?do=SystemStatus&action=DiskStat');
$('#addresses').load('?do=SystemStatus&action=AddressList');
$('#systeminfo').load('?do=SystemStatus&action=SystemInfo');
$('#services').load('?do=SystemStatus&action=Services');
$('#dmsd-users').load('?do=SystemStatus&action=DmsdUsers');
- setTimeout(function() {
- $('#dmsd-log').load('?do=SystemStatus&action=DmsdLog');
- $('#netstat').load('?do=SystemStatus&action=Netstat');
- $('#pslist').load('?do=SystemStatus&action=PsList');
- $('#ldadp-log').load('?do=SystemStatus&action=LdadpLog');
- $('#lighttpd-log').load('?do=SystemStatus&action=LighttpdLog');
- }, 300);
+ var slxDone = {};
+ $('.ajax-tab').click(function (e) {
+ e.preventDefault();
+ var $this = $(this);
+ var w = $this.attr('id');
+ if (!slxDone[w]) {
+ slxDone[w] = true;
+ var $pane = $('#' + w + '_pane');
+ $pane.load('?do=SystemStatus&action=' + w.substring(3));
+ }
+ });
}, false);
-</script>
+//--></script>
diff --git a/modules-available/systemstatus/templates/ajax-reboot.html b/modules-available/systemstatus/templates/ajax-reboot.html
new file mode 100644
index 00000000..a1aaf1e6
--- /dev/null
+++ b/modules-available/systemstatus/templates/ajax-reboot.html
@@ -0,0 +1,14 @@
+<div>
+ {{^dmsd_error}}
+ {{lang_uploads}}: {{uploads}},
+ {{lang_downloads}}: {{downloads}}
+ {{/dmsd_error}}
+ {{#dmsd_error}}
+ <div class="alert alert-warning">{{lang_dmsdUnreachable}}</div>
+ {{/dmsd_error}}
+</div>
+<div>
+ {{#packages}}
+ {{lang_updatedPackages}}: {{packages}}
+ {{/packages}}
+</div> \ No newline at end of file
diff --git a/modules-available/translation/page.inc.php b/modules-available/translation/page.inc.php
index 93616cd1..acc0d6e8 100644
--- a/modules-available/translation/page.inc.php
+++ b/modules-available/translation/page.inc.php
@@ -32,7 +32,7 @@ class Page_Translation extends Page
private $customHandler = false;
/**
- * @var type Language being handled (if any in current step)
+ * @var string Language being handled (if any in current step)
*/
private $destLang = false;
@@ -836,7 +836,7 @@ class Page_Translation extends Page
private function buildTranslationTable($file, $requiredTags = false, $findAlreadyTranslated = false)
{
$tags = array();
- if ($requiredTags !== false) {
+ if (is_array($requiredTags)) {
foreach ($requiredTags as $tagName) {
$tags[$tagName] = array('tag' => $tagName, 'required' => true);
}
diff --git a/modules-available/vmstore/baseconfig/getconfig.inc.php b/modules-available/vmstore/baseconfig/getconfig.inc.php
index e34c502a..75f7279b 100644
--- a/modules-available/vmstore/baseconfig/getconfig.inc.php
+++ b/modules-available/vmstore/baseconfig/getconfig.inc.php
@@ -5,18 +5,18 @@ $vmstore = Property::getVmStoreConfig();
if (is_array($vmstore) && isset($vmstore['storetype'])) {
switch ($vmstore['storetype']) {
case 'internal';
- $configVars["SLX_VM_NFS"] = $_SERVER['SERVER_ADDR'] . ":/srv/openslx/nfs";
+ ConfigHolder::add("SLX_VM_NFS", $_SERVER['SERVER_ADDR'] . ":/srv/openslx/nfs");
break;
case 'nfs';
- $configVars["SLX_VM_NFS"] = $vmstore['nfsaddr'];
+ ConfigHolder::add("SLX_VM_NFS", $vmstore['nfsaddr']);
break;
case 'cifs';
- $configVars["SLX_VM_NFS"] = $vmstore['cifsaddr'];
- $configVars["SLX_VM_NFS_USER"] = $vmstore['cifsuserro'];
- $configVars["SLX_VM_NFS_PASSWD"] = $vmstore['cifspasswdro'];
+ ConfigHolder::add("SLX_VM_NFS", $vmstore['cifsaddr']);
+ ConfigHolder::add("SLX_VM_NFS_USER", $vmstore['cifsuserro']);
+ ConfigHolder::add("SLX_VM_NFS_PASSWD", $vmstore['cifspasswdro']);
break;
}
}
// vm list url. doesn't really fit anywhere, seems to be a tie between here and dozmod
-$configVars["SLX_VMCHOOSER_BASE_URL"] = 'http://' . $_SERVER['SERVER_ADDR'] . '/vmchooser/';
+ConfigHolder::add("SLX_VMCHOOSER_BASE_URL", 'http://' . $_SERVER['SERVER_ADDR'] . '/vmchooser/');
diff --git a/modules-available/vmstore/templates/page-vmstore.html b/modules-available/vmstore/templates/page-vmstore.html
index adf87348..c1ab2472 100644
--- a/modules-available/vmstore/templates/page-vmstore.html
+++ b/modules-available/vmstore/templates/page-vmstore.html
@@ -95,14 +95,14 @@
</p>
<pre>
/mnt/images 1.2.3.4(rw,no_root_squash,async)
-/mnt/images *(ro,async,nolock)
+/mnt/images *(ro,async)
</pre>
<p>
{{lang_nfsHelp2}}
</p>
<pre>
-/mnt/images 1.2.3.4(rw,all_squash,anon_uid=1234,async)
-/mnt/images *(ro,async,nolock)
+/mnt/images 1.2.3.4(rw,all_squash,anonuid=1234,async)
+/mnt/images *(ro,async)
</pre>
</div>
<div class="modal-footer"><a class="btn btn-primary" data-dismiss="modal">{{lang_close}}</a></div>
diff --git a/modules-available/webinterface/lang/de/template-tags.json b/modules-available/webinterface/lang/de/template-tags.json
index ea1074d2..64ba84d0 100644
--- a/modules-available/webinterface/lang/de/template-tags.json
+++ b/modules-available/webinterface/lang/de/template-tags.json
@@ -3,14 +3,18 @@
"lang_caChain": "Optional k\u00f6nnen Sie hier die zum Zertifikat geh\u00f6rende Zertifikatkette (CA-Chain) einf\u00fcgen. Dies wird ben\u00f6tigt, wenn das Zertifikat nicht direkt von einer der in Browsern mitgeliferten CAs signiert wurde. Die Datei enth\u00e4lt ein oder meherere Zertifikatsbl\u00f6cke, im gleichen Format wie das oben gezeigte Zertifikat.",
"lang_certificate": "Bitte f\u00fcgen Sie hier das Zertifikat ein. Das Zertifikat wird im Base64-codierten x509-Format erwartet (manchmal pem genannt). Es sieht in etwa wie folgt aus:",
"lang_customCert": "Eigenes Zertifikat verwenden",
+ "lang_customization": "Designanpassung",
+ "lang_customizationDesc": "Hier k\u00f6nnen Sie kleine optische Anpassungen an der Weboberfl\u00e4che vornehmen. Dies ist hilfreich, wenn Sie z.B. ein Produktiv- und ein Testsystem betreiben und verhindern m\u00f6chten, dass Sie versehentlich in der falschen Weboberfl\u00e4che kritische Einstellungen ver\u00e4ndern.",
"lang_generatedSelected": "Der Server verwendet zur Zeit ein automatisch generiertes, selbst signiertes Zertifikat.",
"lang_hidePasswords": "Passw\u00f6rter maskieren",
"lang_httpsDescription": "Hier k\u00f6nnen Sie festlegen, ob das Web-Interface auch per HTTPS erreichbar sein soll, und welches Zertifikat daf\u00fcr verwendet werden soll.",
"lang_httpsRedirect": "Anfragen per HTTP immer auf HTTPS umleiten (sofern aktiviert)",
"lang_httpsSettings": "HTTPS-Konfiguration",
"lang_installAndRestart": "Zertifikat installieren und Webserver neustarten",
+ "lang_logoBackground": "Hintergrundfarbe des Logos",
"lang_noHttps": "HTTPS wieder deaktivieren, aktuelles Zertifikat l\u00f6schen",
"lang_offSelected": "HTTPS ist derzeit deaktiviert.",
+ "lang_pageTitlePrefix": "Pr\u00e4fix f\u00fcr den Seitentitel",
"lang_passwordFields": "Passwortfelder",
"lang_passwordsDescription": "Legen Sie fest, ob Passwortfelder in der Web-Schnittstelle maskiert werden, oder ob Ihr Inhalt sichtbar sein soll. Wenn Sie die Schnittstelle in einer sicheren Umgebung nutzen (keine neugierigen Augen), kann dies den Komfort erh\u00f6hen. Das Passwortfeld der Anmeldemaske ist von dieser Einstellung ausgenommen.",
"lang_privateKey": "Bitte f\u00fcgen Sie hier den privaten Schl\u00fcssel ein, der zum obigen Zertifikat geh\u00f6rt. Er muss ebenfalls im \"pem\"-Format vorliegen, und sieht wie folgt aus:",
@@ -18,6 +22,7 @@
"lang_showPasswords": "Passw\u00f6rter anzeigen",
"lang_suppliedSelected": "Der Server verwendet zur Zeit ein \u00fcber die Option \"Eigenes Zertifikat\" hochgeladenes Zertifikat.",
"lang_unknownSelected": "Unbekanntes oder ung\u00fcltiges Zertifikat vorhanden. Wahrscheinlich wurde der Server von einer alten Version aktualisiert. Um diese Meldung zu entfernen, die HTTPS-Konfiguration erneut vornehmen.",
+ "lang_useHsts": "HSTS aktivieren (dies erh\u00f6ht die Sicherheit, kann aber in bei sp\u00e4terem Deaktivieren von HTTPS zu Zugriffsproblemen f\u00fchren)",
"lang_youreNotUsingHttps": "Sie besuchen diese Seite nicht per HTTPS (oder die HTTPS-Terminierung wird von einem vorgeschalteten Proxy \u00fcbernommen).",
"lang_youreUsingHttps": "Sie besuchen diese Seite (aus Sicht des Webservers) per HTTPS."
} \ No newline at end of file
diff --git a/modules-available/webinterface/lang/en/template-tags.json b/modules-available/webinterface/lang/en/template-tags.json
index efe649cb..0fb4cc96 100644
--- a/modules-available/webinterface/lang/en/template-tags.json
+++ b/modules-available/webinterface/lang/en/template-tags.json
@@ -3,14 +3,18 @@
"lang_caChain": "Here you can paste an optional certificate chain. It should only be required if you have a certificate that was not directly signed by a certificate authority known by the browsers. It should contain one or more certificate blocks, looking just like the certificate above.",
"lang_certificate": "Please paste your certificate below. It has to be in base64 encoded x509 format (sometimes called pem). It should look something like this:",
"lang_customCert": "Supply own certificate",
+ "lang_customization": "Design customization",
+ "lang_customizationDesc": "Here you can make small changes to the design of the web interface. This might help to prevent accidents if you run multiple satellite servers and have an open tab for all of them at the same time.",
"lang_generatedSelected": "The server is currently using an automatically generated, self-signed certificate.",
"lang_hidePasswords": "Mask passwords",
"lang_httpsDescription": "Here you can set whether the web interface should be accessible via https. You can choose if you want to use a random self signed certificate, or supply your own.",
"lang_httpsRedirect": "Redirect incoming HTTP requests to HTTPS (if enabled).",
"lang_httpsSettings": "HTTPS settings",
"lang_installAndRestart": "Installing certificate and restarting web server",
+ "lang_logoBackground": "Logo background color",
"lang_noHttps": "Disable HTTPS, delete current certificate",
"lang_offSelected": "HTTPS is currently disabled.",
+ "lang_pageTitlePrefix": "Page title prefix",
"lang_passwordFields": "Password fields",
"lang_passwordsDescription": "Set whether password fields should be masked or not. The password field of the login page to the web interface is always masked.",
"lang_privateKey": "Please paste the private key belonging to the certificate here. It has to be in \"pem\" format too, which should look like this:",
@@ -18,6 +22,7 @@
"lang_showPasswords": "Show passwords",
"lang_suppliedSelected": "The server is currently using a certificate supplied using the \"Supply own certificate\" option.",
"lang_unknownSelected": "Unknown or invalid certificate in use. The server war probably updated from an old version while HTTPS was already enabled. Redo the HTTPS configuration steps to get rid of this message.",
+ "lang_useHsts": "Use HSTS (increases security but might lead to problems accessing the site if you disable HTTPS later)",
"lang_youreNotUsingHttps": "You're not using HTTPS to visit this website (or the HTTPS termination is done by a reverse proxy).",
"lang_youreUsingHttps": "You're visiting this server through an HTTPS connection (from the server's point of view)."
} \ No newline at end of file
diff --git a/modules-available/webinterface/page.inc.php b/modules-available/webinterface/page.inc.php
index 93d659f0..e576807e 100644
--- a/modules-available/webinterface/page.inc.php
+++ b/modules-available/webinterface/page.inc.php
@@ -5,6 +5,7 @@ class Page_WebInterface extends Page
const PROP_REDIRECT = 'webinterface.https-redirect';
const PROP_TYPE = 'webinterface.https-type';
+ const PROP_HSTS = 'webinterface.https-hsts';
protected function doPreprocess()
{
@@ -20,17 +21,18 @@ class Page_WebInterface extends Page
case 'password':
$this->actionShowHidePassword();
break;
+ case 'customization':
+ $this->actionCustomization();
+ break;
}
}
private function actionConfigureHttps()
{
- $task = false;
- $off = '';
- switch (Request::post('mode')) {
+ $mode = Request::post('mode');
+ switch ($mode) {
case 'off':
$task = $this->setHttpsOff();
- $off = '&hsts=off';
break;
case 'random':
$task = $this->setHttpsRandomCert();
@@ -42,9 +44,12 @@ class Page_WebInterface extends Page
$task = $this->setRedirectMode();
break;
}
+ if ($mode !== 'off') {
+ Property::set(self::PROP_HSTS, Request::post('usehsts', false, 'string') === 'on' ? 'True' : 'False');
+ }
if (isset($task['id'])) {
Session::set('https-id', $task['id']);
- Util::redirect('?do=WebInterface&show=httpsupdate' . $off);
+ Util::redirect('?do=WebInterface&show=httpsupdate');
}
Util::redirect('?do=WebInterface');
}
@@ -55,6 +60,17 @@ class Page_WebInterface extends Page
Util::redirect('?do=WebInterface');
}
+ private function actionCustomization()
+ {
+ $prefix = Request::post('prefix', '', 'string');
+ if (!empty($prefix) && !preg_match('/[\]\)\}\-_\s\&\$\!\/\+\*\^\>]$/', $prefix)) {
+ $prefix .= ' ';
+ }
+ Property::set('page-title-prefix', $prefix);
+ Property::set('logo-background', Request::post('bgcolor', '', 'string'));
+ Util::redirect('?do=WebInterface');
+ }
+
protected function doRender()
{
//
@@ -65,11 +81,13 @@ class Page_WebInterface extends Page
}
$type = Property::get(self::PROP_TYPE);
$force = Property::get(self::PROP_REDIRECT) === 'True';
+ $hsts = Property::get(self::PROP_HSTS) === 'True';
$https = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
$exists = file_exists('/etc/lighttpd/server.pem');
$data = array(
'httpsUsed' => $https,
- 'redirect_checked' => ($force ? 'checked' : '')
+ 'redirect_checked' => ($force ? 'checked' : ''),
+ 'hsts_checked' => ($hsts ? 'checked' : '')
);
// Type should be 'off', 'generated', 'supplied'
if ($type === 'off') {
@@ -114,12 +132,30 @@ class Page_WebInterface extends Page
else
$data['selected_hide'] = 'checked';
Render::addTemplate('passwords', $data);
+ $data = array('prefix' => Property::get('page-title-prefix'));
+ $data['colors'] = array_map(function ($i) { return array('color' => $i ? '#' . $i : '', 'text' => Render::readableColor($i)); },
+ array('', 'f00', '0f0', '00f', 'ff0', 'f0f', '0ff', 'fff', '000', 'f90', '09f', '90f', 'f09', '9f0'));
+ $color = Property::get('logo-background');
+ foreach ($data['colors'] as &$c) {
+ if ($c['color'] === $color) {
+ $c['selected'] = 'selected';
+ $color = false;
+ break;
+ }
+ }
+ unset($c);
+ if ($color) {
+ $data['colors'][] = array('color' => $color, 'selected' => 'selected');
+ }
+ Render::addTemplate('customization', $data);
}
private function setHttpsOff()
{
Property::set(self::PROP_TYPE, 'off');
+ Property::set(self::PROP_HSTS, 'off');
Header('Strict-Transport-Security: max-age=0', true);
+ Session::deleteCookie();
return Taskmanager::submit('LighttpdHttps', array());
}
diff --git a/modules-available/webinterface/templates/customization.html b/modules-available/webinterface/templates/customization.html
new file mode 100644
index 00000000..7949f95b
--- /dev/null
+++ b/modules-available/webinterface/templates/customization.html
@@ -0,0 +1,30 @@
+<form action="?do=WebInterface" method="post">
+ <input type="hidden" name="token" value="{{token}}">
+ <input type="hidden" name="action" value="customization">
+ <div class="panel panel-default">
+ <div class="panel-heading">{{lang_customization}}</div>
+ <div class="panel-body">
+ <p>{{lang_customizationDesc}}</p>
+ <div>
+ <label>
+ {{lang_pageTitlePrefix}}
+ <input type="text" class="form-control" name="prefix" value="{{prefix}}">
+ </label>
+
+ </div>
+ <div>
+ <label>
+ {{lang_logoBackground}}
+ <select class="form-control" name="bgcolor">
+ {{#colors}}
+ <option style="color:{{text}};background:{{color}}" {{selected}}>{{color}}</option>
+ {{/colors}}
+ </select>
+ </label>
+ </div>
+ <div class="pull-right">
+ <button type="submit" class="btn btn-primary">{{lang_save}}</button>
+ </div>
+ </div>
+ </div>
+</form>
diff --git a/modules-available/webinterface/templates/https.html b/modules-available/webinterface/templates/https.html
index 77585ddf..ecfe5f5d 100644
--- a/modules-available/webinterface/templates/https.html
+++ b/modules-available/webinterface/templates/https.html
@@ -78,6 +78,12 @@ MIIFfTCCA...
{{lang_httpsRedirect}}
</span>
</div>
+ <div class="input-group">
+ <span class="input-group-addon"><input id="usehsts" type="checkbox" name="usehsts" value="on" {{hsts_checked}}></span>
+ <span class="form-control" onclick="$('#usehsts').prop('checked', !$('#usehsts').prop('checked'))">
+ {{lang_useHsts}}
+ </span>
+ </div>
<br>
<div class="pull-right">
diff --git a/style/default.css b/style/default.css
index 894aa752..6566e7c3 100644
--- a/style/default.css
+++ b/style/default.css
@@ -214,6 +214,12 @@ input[readonly] {
border-width: 1px 0px;
}
+.slx-smallcol {
+ width: 1px;
+ overflow: visible;
+ white-space: nowrap;
+}
+
.slx-visible-rows > div.row + div.row {
border-top: none;
}
@@ -467,12 +473,18 @@ nav.navbar.sidebar {
color: #fff;
}
+.red-bg, input[type="checkbox"]:checked {
+ background:#f77;
+}
+
/* this is based on https://github.com/flatlogic/awesome-bootstrap-checkbox
and "fixes" the style of radio buttons and check boxes.
it only applies if they're in a container that has the checkbox class */
.checkbox {
padding-left: 20px;
+ margin-top: 5px;
+ margin-bottom: 2px;
}
.checkbox label {
display: inline-block;
@@ -481,6 +493,7 @@ it only applies if they're in a container that has the checkbox class */
padding-left: 5px;
}
.checkbox label::before {
+ top: 1px;
content: "";
display: inline-block;
position: absolute;
@@ -501,7 +514,7 @@ it only applies if they're in a container that has the checkbox class */
width: 16px;
height: 16px;
left: 0;
- top: 0;
+ top: 1px;
margin-left: -20px;
padding-left: 3px;
padding-top: 1px;
@@ -512,7 +525,9 @@ it only applies if they're in a container that has the checkbox class */
.checkbox input[type="radio"] {
opacity: 0;
z-index: 1;
- top: -6px;
+ top: -3px;
+ width: 17px;
+ height: 17px;
}