summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api.php5
-rw-r--r--apis/clientlog.inc.php8
-rw-r--r--apis/cron.inc.php9
-rw-r--r--apis/statistics.inc.php29
-rw-r--r--inc/database.inc.php94
-rw-r--r--inc/download.inc.php12
-rw-r--r--inc/property.inc.php8
-rw-r--r--inc/util.inc.php49
-rw-r--r--index.php13
-rw-r--r--modules-available/dozmod/lang/de/template-tags.json1
-rw-r--r--modules-available/dozmod/lang/en/template-tags.json5
-rw-r--r--modules-available/exams/page.inc.php9
-rw-r--r--modules-available/exams/templates/page-upcoming-lectures.html11
-rw-r--r--modules-available/rebootcontrol/api.inc.php36
-rw-r--r--modules-available/rebootcontrol/clientscript.js22
-rw-r--r--modules-available/rebootcontrol/config.json4
-rw-r--r--modules-available/rebootcontrol/hooks/config-tgz.inc.php18
-rw-r--r--modules-available/rebootcontrol/inc/rebootqueries.inc.php44
-rw-r--r--modules-available/rebootcontrol/inc/sshkey.inc.php40
-rw-r--r--modules-available/rebootcontrol/lang/de/messages.json4
-rw-r--r--modules-available/rebootcontrol/lang/de/module.json5
-rw-r--r--modules-available/rebootcontrol/lang/de/template-tags.json31
-rw-r--r--modules-available/rebootcontrol/lang/en/messages.json4
-rw-r--r--modules-available/rebootcontrol/lang/en/module.json5
-rw-r--r--modules-available/rebootcontrol/lang/en/template-tags.json31
-rw-r--r--modules-available/rebootcontrol/lang/pt/template-tags.json30
-rw-r--r--modules-available/rebootcontrol/page.inc.php106
-rw-r--r--modules-available/rebootcontrol/style.css47
-rw-r--r--modules-available/rebootcontrol/templates/_page.html245
-rw-r--r--modules-available/rebootcontrol/templates/status.html62
-rw-r--r--modules-available/roomplanner/js/grid.js58
-rw-r--r--modules-available/roomplanner/js/init.js4
-rw-r--r--modules-available/roomplanner/style.css7
-rw-r--r--modules-available/roomplanner/templates/page.html5
-rw-r--r--modules-available/serversetup-bwlp/page.inc.php9
-rw-r--r--modules-available/statistics/api.inc.php73
-rw-r--r--modules-available/statistics/hooks/config-tgz.inc.php32
-rw-r--r--modules-available/statistics/inc/devicetype.inc.php6
-rw-r--r--modules-available/statistics/install.inc.php60
-rw-r--r--modules-available/statistics/lang/de/template-tags.json5
-rw-r--r--modules-available/statistics/lang/en/template-tags.json5
-rw-r--r--modules-available/statistics/page.inc.php72
-rw-r--r--modules-available/statistics/templates/machine-main.html30
-rw-r--r--modules-available/statistics/templates/projector-list.html21
-rw-r--r--modules-available/statistics_reporting/hooks/cron.inc.php5
-rw-r--r--modules-available/statistics_reporting/inc/getdata.inc.php12
-rw-r--r--modules-available/statistics_reporting/inc/queries.inc.php14
-rw-r--r--modules-available/statistics_reporting/inc/remotereport.inc.php10
-rw-r--r--modules-available/statistics_reporting/templates/columnChooser.html2
-rw-r--r--modules-available/sysconfig/addmodule_branding.inc.php2
-rw-r--r--modules-available/sysconfig/addmodule_custommodule.inc.php2
-rw-r--r--modules-available/sysconfig/addmodule_sshconfig.inc.php2
-rw-r--r--modules-available/sysconfig/inc/ppd.inc.php1162
-rw-r--r--modules-available/webinterface/lang/de/messages.json6
-rw-r--r--modules-available/webinterface/lang/de/template-tags.json10
-rw-r--r--modules-available/webinterface/lang/en/messages.json6
-rw-r--r--modules-available/webinterface/lang/en/template-tags.json14
-rw-r--r--modules-available/webinterface/page.inc.php92
-rw-r--r--modules-available/webinterface/templates/httpd-restart.html38
-rw-r--r--modules-available/webinterface/templates/https.html34
-rw-r--r--modules-available/webinterface/templates/passwords.html1
61 files changed, 2668 insertions, 118 deletions
diff --git a/api.php b/api.php
index fdccf14a..a9eec1e7 100644
--- a/api.php
+++ b/api.php
@@ -42,8 +42,13 @@ if (Module::isAvailable($module)) {
if (!file_exists($module)) {
Util::traceError('Invalid module, or module without API: ' . $module);
}
+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('Content-Type: text/plain; charset=utf-8');
+
ob_start('ob_gzhandler');
// Load module - it will execute pre-processing, or act upon request parameters
require_once($module);
diff --git a/apis/clientlog.inc.php b/apis/clientlog.inc.php
index 81a7dbf7..b68e4632 100644
--- a/apis/clientlog.inc.php
+++ b/apis/clientlog.inc.php
@@ -10,7 +10,11 @@ if (empty($_POST['type'])) die('Missing options.');
$type = mb_strtolower($_POST['type']);
if ($type{0} === '~' || $type{0} === '.') {
- require 'modules/statistics/api.inc.php';
+ if (Module::isAvailable('statistics')) {
+ require 'modules/statistics/api.inc.php';
+ }
} else {
- require 'modules/syslog/api.inc.php';
+ if (Module::isAvailable('syslog')) {
+ require 'modules/syslog/api.inc.php';
+ }
}
diff --git a/apis/cron.inc.php b/apis/cron.inc.php
index a0042e61..cf96ac29 100644
--- a/apis/cron.inc.php
+++ b/apis/cron.inc.php
@@ -50,8 +50,13 @@ foreach (Hook::load('cron') as $hook) {
continue;
}
}
- $value = $hook . '|' . time();
+ $value = $hook->moduleId . '|' . time();
Property::addToList(CRON_KEY_STATUS, $value, 1800);
- handleModule($hook->file);
+ try {
+ handleModule($hook->file);
+ } catch (Exception $e) {
+ // Logging
+ EventLog::failure('Cronjob for module ' . $hook->moduleId . ' has crashed. Check the php or web server error log.', $e->toString());
+ }
Property::removeFromList(CRON_KEY_STATUS, $value);
}
diff --git a/apis/statistics.inc.php b/apis/statistics.inc.php
deleted file mode 100644
index 2be805ba..00000000
--- a/apis/statistics.inc.php
+++ /dev/null
@@ -1,29 +0,0 @@
-<?php
-
-$NOW = time();
-$cutoff = $NOW - 86400*90;
-
-$res = Database::simpleQuery("SELECT m.machineuuid, m.locationid, m.macaddr, m.clientip, m.lastseen, m.logintime, m.mbram,"
- . " m.kvmstate, m.cpumodel, m.systemmodel, m.id44mb, m.badsectors, m.hostname, GROUP_CONCAT(s.locationid) AS locs"
- . " FROM machine m"
- . " LEFT JOIN subnet s ON (INET_ATON(m.clientip) BETWEEN s.startaddr AND s.endaddr)"
- . " WHERE m.lastseen > $cutoff"
- . " GROUP BY m.machineuuid");
-
-$return = array(
- 'now' => $NOW,
- 'clients' => array(),
- 'locations' => Location::getLocationsAssoc()
-);
-while ($client = $res->fetch(PDO::FETCH_ASSOC)) {
- if ($NOW - $client['lastseen'] > 610) {
- $client['state'] = 'OFF';
- } elseif ($client['logintime'] == 0) {
- $client['state'] = 'IDLE';
- } else {
- $client['state'] = 'OCCUPIED';
- }
- $return['clients'][] = $client;
-}
-
-die(json_encode($return)); \ No newline at end of file
diff --git a/inc/database.inc.php b/inc/database.inc.php
index 4a5821f4..ff98f5ee 100644
--- a/inc/database.inc.php
+++ b/inc/database.inc.php
@@ -132,4 +132,98 @@ class Database
return self::$dbh->prepare($query);
}
+ /**
+ * Insert row into table, returning the generated key.
+ * This requires the table to have an AUTO_INCREMENT column and
+ * usually requires the given $uniqueValues to span across a UNIQUE index.
+ * The code first tries to SELECT the key for the given values without
+ * inserting first. This means this function is best used for cases
+ * where you expect that the entry already exists in the table, so
+ * only one SELECT will run. For all the entries that do not exist,
+ * an INSERT or INSERT IGNORE is run, depending on whether $additionalValues
+ * is empty or not. Another reason we don't run the INSERT (IGNORE) first
+ * is that it will increase the AUTO_INCREMENT value on InnoDB, even when
+ * no INSERT took place. So if you expect a lot of collisions you might
+ * use this function to prevent your A_I value from counting up too
+ * quickly.
+ * Other than that, this is just a dumb version of running INSERT and then
+ * getting the LAST_INSERT_ID(), or doing a query for the existing ID in
+ * case of a key collision.
+ *
+ * @param string $table table to insert into
+ * @param string $aiKey name of the AUTO_INCREMENT column
+ * @param array $uniqueValues assoc array containing columnName => value mapping
+ * @param array $additionalValues assoc array containing columnName => value mapping
+ * @return int[] list of AUTO_INCREMENT values matching the list of $values
+ */
+ public static function insertIgnore($table, $aiKey, $uniqueValues, $additionalValues = false)
+ {
+ // Sanity checks
+ if (array_key_exists($aiKey, $uniqueValues)) {
+ Util::traceError("$aiKey must not be in \$uniqueValues");
+ }
+ if (is_array($additionalValues) && array_key_exists($aiKey, $additionalValues)) {
+ Util::traceError("$aiKey must not be in \$additionalValues");
+ }
+ // Simple SELECT first
+ $selectSql = 'SELECT ' . $aiKey . ' FROM ' . $table . ' WHERE 1';
+ foreach ($uniqueValues as $key => $value) {
+ $selectSql .= ' AND ' . $key . ' = :' . $key;
+ }
+ $selectSql .= ' LIMIT 1';
+ $res = self::queryFirst($selectSql, $uniqueValues);
+ if ($res !== false) {
+ // Exists
+ if (!empty($additionalValues)) {
+ // Simulate ON DUPLICATE KEY UPDATE ...
+ $updateSql = 'UPDATE ' . $table . ' SET ';
+ $first = true;
+ foreach ($additionalValues as $key => $value) {
+ if ($first) {
+ $first = false;
+ } else {
+ $updateSql .= ', ';
+ }
+ $updateSql .= $key . ' = :' . $key;
+ }
+ $updateSql .= ' WHERE ' . $aiKey . ' = :' . $aiKey;
+ $additionalValues[$aiKey] = $res[$aiKey];
+ Database::exec($updateSql, $additionalValues);
+ }
+ return $res[$aiKey];
+ }
+ // Does not exist:
+ if (empty($additionalValues)) {
+ $combined =& $uniqueValues;
+ } else {
+ $combined = $uniqueValues + $additionalValues;
+ }
+ // Aight, try INSERT or INSERT IGNORE
+ $insertSql = 'INTO ' . $table . ' (' . implode(', ', array_keys($combined))
+ . ') VALUES (:' . implode(', :', array_keys($combined)) . ')';
+ if (empty($additionalValues)) {
+ // Simple INSERT IGNORE
+ $insertSql = 'INSERT IGNORE ' . $insertSql;
+ } else {
+ // INSERT ... ON DUPLICATE (in case we have a race)
+ $insertSql = 'INSERT ' . $insertSql . ' ON DUPLICATE KEY UPDATE ';
+ $first = true;
+ foreach ($additionalValues as $key => $value) {
+ if ($first) {
+ $first = false;
+ } else {
+ $insertSql .= ', ';
+ }
+ $insertSql .= $key . ' = VALUES(' . $key . ')';
+ }
+ }
+ self::exec($insertSql, $combined);
+ // Insert done, retrieve key again
+ $res = self::queryFirst($selectSql, $uniqueValues);
+ if ($res === false) {
+ Util::traceError('Could not find value in table ' . $table . ' that was just inserted');
+ }
+ return $res[$aiKey];
+ }
+
}
diff --git a/inc/download.inc.php b/inc/download.inc.php
index 51601545..a2054f78 100644
--- a/inc/download.inc.php
+++ b/inc/download.inc.php
@@ -49,8 +49,8 @@ class Download
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$data = curl_exec($ch);
$head = self::getContents($head);
- if (preg_match('#^HTTP/\d+\.\d+ (\d+) #', $head, $out)) {
- $code = (int) $out[1];
+ if (preg_match_all('#^HTTP/\d+\.\d+ (\d+) #m', $head, $out)) {
+ $code = (int) array_pop($out[1]);
} else {
$code = 999;
}
@@ -83,8 +83,8 @@ class Download
curl_setopt($ch, CURLOPT_POSTFIELDS, $string);
$data = curl_exec($ch);
$head = self::getContents($head);
- if (preg_match('#^HTTP/\d+\.\d+ (\d+) #', $head, $out)) {
- $code = (int) $out[1];
+ if (preg_match_all('#^HTTP/\d+\.\d+ (\d+) #m', $head, $out)) {
+ $code = (int) array_pop($out[1]);
} else {
$code = 999;
}
@@ -116,8 +116,8 @@ class Download
@unlink($target);
return false;
}
- if (preg_match_all('#\bHTTP/\d+\.\d+ (\d+) #', $head, $out, PREG_SET_ORDER)) {
- $code = (int) $out[count($out) - 1][1];
+ if (preg_match_all('#^HTTP/\d+\.\d+ (\d+) #m', $head, $out)) {
+ $code = (int) array_pop($out[1]);
} else {
$code = '999 ' . curl_error($ch);
}
diff --git a/inc/property.inc.php b/inc/property.inc.php
index b3d8081a..b33e1bff 100644
--- a/inc/property.inc.php
+++ b/inc/property.inc.php
@@ -146,19 +146,19 @@ class Property
public static function getVersionCheckInformation()
{
- $data = json_decode(self::get('versioncheck-data'), true);
- if (isset($data['time']) && $data['time'] + 120 > time())
+ $data = json_decode(self::get('versioncheck-data', '[]'), true);
+ if (isset($data['time']) && $data['time'] + 60 > time())
return $data;
$task = Taskmanager::submit('DownloadText', array(
'url' => CONFIG_REMOTE_ML . '/list.php'
));
if (!isset($task['id']))
return 'Could not start list download (' . Message::asString() . ')';
- if ($task['statusCode'] !== TASK_FINISHED) {
+ if (!Taskmanager::isFinished($task)) {
$task = Taskmanager::waitComplete($task['id'], 5000);
}
if ($task['statusCode'] !== TASK_FINISHED || !isset($task['data']['content'])) {
- return $task['data']['error'];
+ return isset($task['data']['error']) ? $task['data']['error'] : 'Timeout';
}
$data = json_decode($task['data']['content'], true);
$data['time'] = time();
diff --git a/inc/util.inc.php b/inc/util.inc.php
index d454d18d..5d1a4563 100644
--- a/inc/util.inc.php
+++ b/inc/util.inc.php
@@ -21,6 +21,11 @@ class Util
exit(1);
}
Header('HTTP/1.1 500 Internal Server Error');
+ if (isset($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'html') === false ) {
+ Header('Content-Type: text/plain; charset=utf-8');
+ echo 'API ERROR: ', $message, "\n", self::formatBacktracePlain(debug_backtrace());
+ exit(0);
+ }
Header('Content-Type: text/html; charset=utf-8');
echo '<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><style>', "\n",
".arg { color: red; background: white; }\n",
@@ -79,19 +84,38 @@ SADFACE;
exit(0);
}
+ private static function formatArgument($arg, $expandArray = true)
+ {
+ if (is_string($arg)) {
+ $arg = "'$arg'";
+ } elseif (is_object($arg)) {
+ $arg = 'instanceof ' . get_class($arg);
+ } elseif (is_array($arg)) {
+ if ($expandArray && count($arg) < 20) {
+ $expanded = '';
+ foreach ($arg as $key => $value) {
+ if (!empty($expanded)) {
+ $expanded .= ', ';
+ }
+ $expanded .= $key . ': ' . self::formatArgument($value, false);
+ if (strlen($expanded) > 200)
+ break;
+ }
+ if (strlen($expanded) <= 200)
+ return '[' . $expanded . ']';
+ }
+ $arg = 'Array(' . count($arg) . ')';
+ }
+ return $arg;
+ }
+
public static function formatBacktraceHtml($trace, $escape = true)
{
$output = '';
foreach ($trace as $idx => $line) {
$args = array();
foreach ($line['args'] as $arg) {
- if (is_string($arg)) {
- $arg = "'$arg'";
- } elseif (is_object($arg)) {
- $arg = 'instanceof ' . get_class($arg);
- } elseif (is_array($arg)) {
- $arg = 'Array(' . count($arg) . ')';
- }
+ $arg = self::formatArgument($arg);
$args[] = '<span class="arg">' . htmlspecialchars($arg) . '</span>';
}
$frame = str_pad('#' . $idx, 3, ' ', STR_PAD_LEFT);
@@ -111,14 +135,7 @@ SADFACE;
foreach ($trace as $idx => $line) {
$args = array();
foreach ($line['args'] as $arg) {
- if (is_string($arg)) {
- $arg = "'$arg'";
- } elseif (is_object($arg)) {
- $arg = 'instanceof ' . get_class($arg);
- } elseif (is_array($arg)) {
- $arg = 'Array(' . count($arg) . ')';
- }
- $args[] = $arg;
+ $args[] = self::formatArgument($arg);
}
$frame = str_pad('#' . $idx, 3, ' ', STR_PAD_LEFT);
$args = implode(', ', $args);
@@ -375,7 +392,7 @@ SADFACE;
* @param bool $secure true = only use strong random sources
* @return string|bool string of requested length, false on error
*/
- public static function randomBytes($length, $secure)
+ public static function randomBytes($length, $secure = true)
{
if (function_exists('random_bytes')) {
return random_bytes($length);
diff --git a/index.php b/index.php
index 5fff7e5e..20049335 100644
--- a/index.php
+++ b/index.php
@@ -114,6 +114,19 @@ 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') {
+ 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");
+
// Now determine which module to run
Page::init();
diff --git a/modules-available/dozmod/lang/de/template-tags.json b/modules-available/dozmod/lang/de/template-tags.json
index f5419b93..141ac68d 100644
--- a/modules-available/dozmod/lang/de/template-tags.json
+++ b/modules-available/dozmod/lang/de/template-tags.json
@@ -4,7 +4,6 @@
"lang_allowLoginDescription": "Wenn diese Option aktiviert ist, k\u00f6nnen sich alle Mitarbeiter der Einrichtung \u00fcber die bwLehrpool-Suite anmelden und VMs\/Veranstaltungen verwalten. Wenn Sie diese Option deaktivieren, m\u00fcssen Sie in der Untersektion \"Benutzer und Berechtigungen\" jeden Benutzer nach dem ersten Loginversuch manuell freischalten.",
"lang_asteriskRequired": "Felder mit (*) sind erforderlich",
"lang_blockCount": "Anzahl Bl\u00f6cke",
- "lang_canLogin": "Nutzer dieser Einrichtung k\u00f6nnen sich am Satelliten anmelden",
"lang_canLoginOrganization": "Nutzer dieser Einrichtung k\u00f6nnen sich am Satelliten anmelden",
"lang_canLoginUser": "Nutzer kann sich am Satelliten anmelden",
"lang_createTime": "Erstellt",
diff --git a/modules-available/dozmod/lang/en/template-tags.json b/modules-available/dozmod/lang/en/template-tags.json
index d69870c5..5532bdcb 100644
--- a/modules-available/dozmod/lang/en/template-tags.json
+++ b/modules-available/dozmod/lang/en/template-tags.json
@@ -1,12 +1,11 @@
{
- " lang_canLoginOrganization": "Members of this organization can login",
- " lang_canLoginUser": "User can login",
"lang_actionTarget": "Action target",
"lang_allowLoginByDefault": "Allow all staff members to login and use the bwLehrpool-Suite",
"lang_allowLoginDescription": "If this option is enabled, all members of the organization marked as staff or employee are allowed to login to this server and manage VMs\/courses. Otherwise, new users need to be individually allowed access after their first login attempt by visiting the sub page \"users and permissions\" in this web interface.",
"lang_asteriskRequired": "Fields marked with (*) are required",
"lang_blockCount": "Block count",
- "lang_canLogin": "Members of this organization can login",
+ "lang_canLoginOrganization": "Users from this organization can login",
+ "lang_canLoginUser": "This user can login",
"lang_createTime": "Created",
"lang_currentFilter": "Current filter",
"lang_defaultImagePermissionAdmin": "Administrate",
diff --git a/modules-available/exams/page.inc.php b/modules-available/exams/page.inc.php
index 49b48bb6..930ba62c 100644
--- a/modules-available/exams/page.inc.php
+++ b/modules-available/exams/page.inc.php
@@ -165,7 +165,8 @@ class Page_Exams extends Page
{
$out = [];
$now = time();
- $cutoff = strtotime('+ 5 day');
+ $cutoff = strtotime('+30 day');
+ $theCount = 0;
foreach ($this->lectures as $lecture) {
if ($lecture['endtime'] < $now || $lecture['starttime'] > $cutoff)
continue;
@@ -179,6 +180,9 @@ class Page_Exams extends Page
if ($duration < 86400) {
$entry['duration_s'] = gmdate('H:i', $duration);
}
+ if (++$theCount > 5) {
+ $entry['class'] = 'collapse';
+ }
$out[] = $entry;
}
return $out;
@@ -356,7 +360,8 @@ class Page_Exams extends Page
Message::addInfo('no-upcoming-lecture-exams');
} else {
Render::addTemplate('page-upcoming-lectures', [
- 'pending_lectures' => $upcoming
+ 'pending_lectures' => $upcoming,
+ 'decollapse' => array_key_exists('class', end($upcoming))
]);
}
// Vis.js timeline
diff --git a/modules-available/exams/templates/page-upcoming-lectures.html b/modules-available/exams/templates/page-upcoming-lectures.html
index 4a62bc29..a1867444 100644
--- a/modules-available/exams/templates/page-upcoming-lectures.html
+++ b/modules-available/exams/templates/page-upcoming-lectures.html
@@ -8,7 +8,7 @@
<th>{{lang_actions}}</th>
</tr>
{{#pending_lectures}}
- <tr>
+ <tr class="{{class}}">
<td>
{{displayname}}
<div class="small">
@@ -30,5 +30,14 @@
</td>
</tr>
{{/pending_lectures}}
+ {{#decollapse}}
+ <tr class="slx-decollapse">
+ <td colspan="3">
+ <span class="btn-group btn-group-justified">
+ <span class="btn btn-default btn-sm"><span class="glyphicon glyphicon-menu-down"></span></span>
+ </span>
+ </td>
+ </tr>
+ {{/decollapse}}
</table>
</div> \ No newline at end of file
diff --git a/modules-available/rebootcontrol/api.inc.php b/modules-available/rebootcontrol/api.inc.php
new file mode 100644
index 00000000..77687f8e
--- /dev/null
+++ b/modules-available/rebootcontrol/api.inc.php
@@ -0,0 +1,36 @@
+<?php
+/*
+ Needed POST-Parameters:
+ 'token' -- for authentification
+ 'action' -- which action should be performed (shutdown or reboot)
+ 'clients' -- which are to reboot/shutdown (json encoded array!)
+ 'timer' -- (optional) when to perform action in minutes (default value is 0)
+*/
+
+$ips = json_decode(Request::post('clients'));
+$minutes = Request::post('timer', 0, 'int');
+
+$clients = array();
+foreach ($ips as $client) {
+ $clients[] = array("ip" => $client);
+}
+
+if (Request::post('token') == Property::get("rebootcontrol_APIPOSTKEY")) {
+ if (Request::isPost()) {
+ if (Request::post('action') == 'shutdown') {
+ $shutdown = true;
+ $task = Taskmanager::submit("RemoteReboot", array("clients" => $clients, "shutdown" => $shutdown, "minutes" => $minutes));
+ echo $task["id"];
+ } else if (Request::post('action') == 'reboot') {
+ $shutdown = false;
+ $task = Taskmanager::submit("RemoteReboot", array("clients" => $clients, "shutdown" => $shutdown, "minutes" => $minutes));
+ echo $task["id"];
+ } else {
+ echo "Only action=shutdown and action=reboot available.";
+ }
+ } else {
+ echo "Only POST Method available.";
+ }
+} else {
+ echo "Not authorized";
+} \ No newline at end of file
diff --git a/modules-available/rebootcontrol/clientscript.js b/modules-available/rebootcontrol/clientscript.js
new file mode 100644
index 00000000..d3ecbe48
--- /dev/null
+++ b/modules-available/rebootcontrol/clientscript.js
@@ -0,0 +1,22 @@
+document.addEventListener("DOMContentLoaded", function() {
+ var table = $("table");
+ table.stupidtable({
+ "ipsort":function(a,b){
+ var aa = a.split(".");
+ var bb = b.split(".");
+
+ var resulta = aa[0]*0x1000000 + aa[1]*0x10000 + aa[2]*0x100 + aa[3]*1;
+ var resultb = bb[0]*0x1000000 + bb[1]*0x10000 + bb[2]*0x100 + bb[3]*1;
+
+ return resulta-resultb;
+ }
+ });
+
+ table.on("aftertablesort", function (event, data) {
+ var th = $(this).find("th");
+ th.find(".arrow").remove();
+ var dir = $.fn.stupidtable.dir;
+ var arrow = data.direction === dir.ASC ? "down" : "up";
+ th.eq(data.column).append(' <span class="arrow glyphicon glyphicon-chevron-'+arrow+'"></span>');
+ });
+}); \ No newline at end of file
diff --git a/modules-available/rebootcontrol/config.json b/modules-available/rebootcontrol/config.json
new file mode 100644
index 00000000..2cc05822
--- /dev/null
+++ b/modules-available/rebootcontrol/config.json
@@ -0,0 +1,4 @@
+{
+ "category":"main.content",
+ "dependencies": [ "locations", "js_stupidtable" ]
+}
diff --git a/modules-available/rebootcontrol/hooks/config-tgz.inc.php b/modules-available/rebootcontrol/hooks/config-tgz.inc.php
new file mode 100644
index 00000000..0b706960
--- /dev/null
+++ b/modules-available/rebootcontrol/hooks/config-tgz.inc.php
@@ -0,0 +1,18 @@
+<?php
+
+$pubkey = SSHKey::getPublicKey();
+$tmpfile = '/tmp/bwlp-' . md5($pubkey) . '.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("/root/.ssh/authorized_keys.d/rebootcontrol", $pubkey);
+ $file = $tmpfile;
+ } catch (Exception $e) {
+ EventLog::failure('Could not include ssh key for reboot-control in config.tgz', (string)$e);
+ }
+} elseif (is_file($tmpfile) && is_readable($tmpfile)) {
+ $file = $tmpfile;
+}
diff --git a/modules-available/rebootcontrol/inc/rebootqueries.inc.php b/modules-available/rebootcontrol/inc/rebootqueries.inc.php
new file mode 100644
index 00000000..df3c13d8
--- /dev/null
+++ b/modules-available/rebootcontrol/inc/rebootqueries.inc.php
@@ -0,0 +1,44 @@
+<?php
+
+class RebootQueries
+{
+
+ // Get Client+IP+CurrentVM+CurrentUser+Location to fill the table
+ public static function getMachineTable($locationId) {
+ if ($locationId === 0) {
+ $where = 'machine.locationid IS NULL';
+ } else {
+ $where = 'machine.locationid = :locationid';
+ }
+ $leftJoin = '';
+ $sessionField = 'machine.currentsession';
+ if (Module::get('dozmod') !== false) {
+ // SELECT lectureid, displayname FROM sat.lecture WHERE lectureid = :lectureid
+ $leftJoin = 'LEFT JOIN sat.lecture ON (lecture.lectureid = machine.currentsession)';
+ $sessionField = 'IFNULL(lecture.displayname, machine.currentsession) AS currentsession';
+ }
+ $res = Database::simpleQuery("
+ SELECT machine.machineuuid, machine.hostname, machine.clientip,
+ IF(machine.lastboot = 0 OR UNIX_TIMESTAMP() - machine.lastseen >= 600, 0, 1) AS status,
+ $sessionField, machine.currentuser, machine.locationid
+ FROM machine
+ $leftJoin
+ WHERE " . $where, array('locationid' => $locationId));
+ return $res->fetchAll(PDO::FETCH_ASSOC);
+ }
+
+ /**
+ * Get machines by list of UUIDs
+ * @param string[] $list list of system UUIDs
+ * @return array list of machines with machineuuid, clientip and locationid
+ */
+ public static function getMachinesByUuid($list)
+ {
+ 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);
+ return $res->fetchAll(PDO::FETCH_ASSOC);
+ }
+
+} \ No newline at end of file
diff --git a/modules-available/rebootcontrol/inc/sshkey.inc.php b/modules-available/rebootcontrol/inc/sshkey.inc.php
new file mode 100644
index 00000000..b4e36d25
--- /dev/null
+++ b/modules-available/rebootcontrol/inc/sshkey.inc.php
@@ -0,0 +1,40 @@
+<?php
+
+class SSHKey
+{
+
+ public static function getPrivateKey() {
+ $privKey = Property::get("rebootcontrol-private-key");
+ if (!$privKey) {
+ $rsaKey = openssl_pkey_new(array(
+ 'private_key_bits' => 2048,
+ 'private_key_type' => OPENSSL_KEYTYPE_RSA));
+ openssl_pkey_export( openssl_pkey_get_private($rsaKey), $privKey);
+ Property::set("rebootcontrol-private-key", $privKey);
+ }
+ return $privKey;
+ }
+
+ public static function getPublicKey() {
+ $pkImport = openssl_pkey_get_private(self::getPrivateKey());
+ return self::sshEncodePublicKey($pkImport);
+ }
+
+ private static function sshEncodePublicKey($privKey) {
+ $keyInfo = openssl_pkey_get_details($privKey);
+ $buffer = pack("N", 7) . "ssh-rsa" .
+ self::sshEncodeBuffer($keyInfo['rsa']['e']) .
+ self::sshEncodeBuffer($keyInfo['rsa']['n']);
+ return "ssh-rsa " . base64_encode($buffer);
+ }
+
+ private static function sshEncodeBuffer($buffer) {
+ $len = strlen($buffer);
+ if (ord($buffer[0]) & 0x80) {
+ $len++;
+ $buffer = "\x00" . $buffer;
+ }
+ return pack("Na*", $len, $buffer);
+ }
+
+} \ No newline at end of file
diff --git a/modules-available/rebootcontrol/lang/de/messages.json b/modules-available/rebootcontrol/lang/de/messages.json
new file mode 100644
index 00000000..2a7e1299
--- /dev/null
+++ b/modules-available/rebootcontrol/lang/de/messages.json
@@ -0,0 +1,4 @@
+{
+ "no-clients-selected": "Keine Clients ausgew\u00e4hlt",
+ "some-machine-not-found": "Einige Clients aus dem POST request wurden nicht gefunden"
+} \ No newline at end of file
diff --git a/modules-available/rebootcontrol/lang/de/module.json b/modules-available/rebootcontrol/lang/de/module.json
new file mode 100644
index 00000000..03196610
--- /dev/null
+++ b/modules-available/rebootcontrol/lang/de/module.json
@@ -0,0 +1,5 @@
+{
+ "module_name": "Reboot Control",
+ "notAssigned": "Nicht zugewiesen",
+ "page_title": "Reboot Control"
+} \ No newline at end of file
diff --git a/modules-available/rebootcontrol/lang/de/template-tags.json b/modules-available/rebootcontrol/lang/de/template-tags.json
new file mode 100644
index 00000000..2a04e746
--- /dev/null
+++ b/modules-available/rebootcontrol/lang/de/template-tags.json
@@ -0,0 +1,31 @@
+{
+ "lang_authFail": "Athentifizierung fehlgeschlagen",
+ "lang_client": "Client",
+ "lang_connecting": "Verbinde...",
+ "lang_error": "Nicht erreichbar",
+ "lang_genNew": "Neues Schl\u00fcsselpaar generieren",
+ "lang_ip": "IP",
+ "lang_location": "Standort",
+ "lang_minutes": " Minuten",
+ "lang_off": "Aus",
+ "lang_on": "An",
+ "lang_online": "Online",
+ "lang_pubKey": "SSH Public Key:",
+ "lang_reboot": "Neustarten",
+ "lang_rebootAt": "Neustart um:",
+ "lang_rebootButton": "Neustarten",
+ "lang_rebootCheck": "Wollen Sie wirklich die ausgew\u00e4hlten Rechner neustarten?",
+ "lang_rebooting": "Neustart...",
+ "lang_selectall": "Alle ausw\u00e4hlen",
+ "lang_selected": "Ausgew\u00e4hlt",
+ "lang_session": "Sitzung",
+ "lang_settings": "Einstellungen",
+ "lang_shutdown": "Herunterfahren",
+ "lang_shutdownAt": "Herunterfahren um: ",
+ "lang_shutdownButton": "Herunterfahren",
+ "lang_shutdownCheck": "Wollen Sie wirklich die ausgew\u00e4hlten Rechner herunterfahren?",
+ "lang_shutdownIn": "Herunterfahren in: ",
+ "lang_status": "Status",
+ "lang_unselectall": "Alle abw\u00e4hlen",
+ "lang_user": "Nutzer"
+} \ No newline at end of file
diff --git a/modules-available/rebootcontrol/lang/en/messages.json b/modules-available/rebootcontrol/lang/en/messages.json
new file mode 100644
index 00000000..50bdd7fe
--- /dev/null
+++ b/modules-available/rebootcontrol/lang/en/messages.json
@@ -0,0 +1,4 @@
+{
+ "no-clients-selected": "No clients selected",
+ "some-machine-not-found": "Some machines from your POST request don't exist"
+} \ No newline at end of file
diff --git a/modules-available/rebootcontrol/lang/en/module.json b/modules-available/rebootcontrol/lang/en/module.json
new file mode 100644
index 00000000..129140dd
--- /dev/null
+++ b/modules-available/rebootcontrol/lang/en/module.json
@@ -0,0 +1,5 @@
+{
+ "module_name": "Reboot Control",
+ "notAssigned": "Not assigned",
+ "page_title": "Reboot Control"
+} \ No newline at end of file
diff --git a/modules-available/rebootcontrol/lang/en/template-tags.json b/modules-available/rebootcontrol/lang/en/template-tags.json
new file mode 100644
index 00000000..ca44171a
--- /dev/null
+++ b/modules-available/rebootcontrol/lang/en/template-tags.json
@@ -0,0 +1,31 @@
+{
+ "lang_authFail": "Authentication failed",
+ "lang_client": "Client",
+ "lang_connecting": "Connecting...",
+ "lang_error": "Not available",
+ "lang_genNew": "Generate new keypair",
+ "lang_ip": "IP",
+ "lang_location": "Location",
+ "lang_minutes": " Minutes",
+ "lang_off": "Off",
+ "lang_on": "On",
+ "lang_online": "Online",
+ "lang_pubKey": "SSH Public Key:",
+ "lang_reboot": "Reboot",
+ "lang_rebootAt": "Reboot at:",
+ "lang_rebootButton": "Reboot",
+ "lang_rebootCheck": "Do you really want to reboot the selected clients?",
+ "lang_rebooting": "Rebooting...",
+ "lang_selectall": "Select all",
+ "lang_selected": "Selected",
+ "lang_session": "Session",
+ "lang_settings": "Settings",
+ "lang_shutdown": "Shut Down",
+ "lang_shutdownAt": "Shutdown at: ",
+ "lang_shutdownButton": "Shutdown",
+ "lang_shutdownCheck": "Do you really want to shut down the selected clients?",
+ "lang_shutdownIn": "Shutdown in: ",
+ "lang_status": "Status",
+ "lang_unselectall": "Unselect all",
+ "lang_user": "User"
+} \ No newline at end of file
diff --git a/modules-available/rebootcontrol/lang/pt/template-tags.json b/modules-available/rebootcontrol/lang/pt/template-tags.json
new file mode 100644
index 00000000..89fa4d96
--- /dev/null
+++ b/modules-available/rebootcontrol/lang/pt/template-tags.json
@@ -0,0 +1,30 @@
+{
+ "lang_client": "Client",
+ "lang_ip": "IP",
+ "lang_session": "Session",
+ "lang_user": "User",
+ "lang_location": "Location",
+ "lang_locations": "Locations",
+ "lang_selectall": "Select all",
+ "lang_unselectall": "Unselect all",
+ "lang_status": "Status",
+ "lang_rebootButton": "Reboot",
+ "lang_rebootCheck": "Do you really want to reboot the selected clients?",
+ "lang_shutdownButton": "Shut Down",
+ "lang_shutdownCheck": "Do you really want to shut down the selected clients?",
+ "lang_cancel": "Cancel",
+ "lang_reboot": "Reboot",
+ "lang_connecting": "Connecting...",
+ "lang_rebooting": "Rebooting...",
+ "lang_online": "Online",
+ "lang_error": "Not available",
+ "lang_shutdown": "Shut Down",
+ "lang_shutdownIn": "Shutdown in: ",
+ "lang_shutdownAt": "Shutdown at: ",
+ "lang_minutes": " Minutes",
+ "lang_back": "Back",
+ "lang_pubKey": "SSH Public Key:",
+ "lang_settings": "Settings",
+ "lang_genNew": "Generate new keypair",
+ "lang_selected": "Selected"
+} \ No newline at end of file
diff --git a/modules-available/rebootcontrol/page.inc.php b/modules-available/rebootcontrol/page.inc.php
new file mode 100644
index 00000000..d7083528
--- /dev/null
+++ b/modules-available/rebootcontrol/page.inc.php
@@ -0,0 +1,106 @@
+<?php
+
+class Page_RebootControl extends Page
+{
+
+ private $action = false;
+
+ /**
+ * 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'); // does not return
+ }
+
+ $this->action = Request::any('action', 'show', 'string');
+
+
+ if ($this->action === 'startReboot' || $this->action === 'startShutdown') {
+ $clients = Request::post('clients');
+ if (!is_array($clients) || empty($clients)) {
+ Message::addError('no-clients-selected');
+ Util::redirect();
+ }
+ $locationId = Request::post('locationId', false, 'int');
+ if ($locationId === false) {
+ Message::addError('locations.invalid-location-id', $locationId);
+ Util::redirect();
+ }
+ $shutdown = $this->action === "startShutdown";
+ $minutes = Request::post('minutes', 0, 'int');
+ $privKey = SSHKey::getPrivateKey();
+
+ $list = RebootQueries::getMachinesByUuid($clients);
+ if (count($list) !== count($clients)) {
+ // We could go ahead an see which ones were not found in DB but this should not happen anyways unless the
+ // user manipulated the request
+ Message::addWarning('some-machine-not-found');
+ }
+ // TODO: Iterate over list and check if a locationid is not in permissions
+ // TODO: we could also check if the locationid is equal or a sublocation of the $locationId from above
+ // (this would be more of a sanity check though, or does the UI allow selecting machines from different locations)
+
+ $task = Taskmanager::submit("RemoteReboot", array(
+ "clients" => $list,
+ "shutdown" => $shutdown,
+ "minutes" => $minutes,
+ "locationId" => $locationId,
+ "sshkey" => $privKey,
+ "port" => 22, // TODO: Get from ssh config
+ ));
+
+ Util::redirect("?do=rebootcontrol&taskid=".$task["id"]);
+ }
+
+ }
+
+ /**
+ * Menu etc. has already been generated, now it's time to generate page content.
+ */
+
+ protected function doRender()
+ {
+ if ($this->action === 'show') {
+
+ $taskId = Request::get("taskid");
+
+ if ($taskId && Taskmanager::isTask($taskId)) {
+ $task = Taskmanager::status($taskId);
+ $data['taskId'] = $taskId;
+ $data['locationId'] = $task['data']['locationId'];
+ $data['locationName'] = Location::getName($task['data']['locationId']);
+ $data['clients'] = $task['data']['clients'];
+ Render::addTemplate('status', $data);
+ } else {
+ //location you want to see, default are "not assigned" clients
+ $requestedLocation = Request::get('location', 0, 'int');
+
+ $data['data'] = RebootQueries::getMachineTable($requestedLocation);
+ $data['locations'] = Location::getLocations($requestedLocation, 0, true);
+
+ $data['pubKey'] = SSHKey::getPublicKey();
+
+ Render::addTemplate('_page', $data);
+ }
+ }
+ }
+
+ function doAjax()
+ {
+ $this->action = Request::post('action', false, 'string');
+ if ($this->action === 'generateNewKeypair') {
+ Property::set("rebootcontrol-private-key", false);
+ echo SSHKey::getPublicKey();
+ } else {
+ echo 'Invalid action.';
+ }
+ }
+
+
+
+}
diff --git a/modules-available/rebootcontrol/style.css b/modules-available/rebootcontrol/style.css
new file mode 100644
index 00000000..442cd5de
--- /dev/null
+++ b/modules-available/rebootcontrol/style.css
@@ -0,0 +1,47 @@
+.rebootTimerForm {
+ margin-top: 20px;
+}
+
+.statusColumn {
+ text-align: center;
+}
+
+.table > tbody > tr > td {
+ vertical-align: middle;
+ height: 50px;
+}
+
+.checkbox {
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+#rebootButton, #settingsButton, #selectAllButton, #unselectAllButton {
+ margin-left: 10px;
+}
+
+#rebootButton, #shutdownButton, #selectAllButton, #unselectAllButton {
+ width: 140px;
+}
+
+#dataTable {
+ margin-top: 20px;
+}
+
+
+#shutdownTimer {
+ text-align: center;
+}
+#pubKeyTitle {
+ display: inline-block;
+ margin-top: 7px;
+ margin-bottom: 20px;
+}
+
+pre {
+ white-space: pre-wrap;
+}
+
+th[data-sort] {
+ cursor: pointer;
+} \ No newline at end of file
diff --git a/modules-available/rebootcontrol/templates/_page.html b/modules-available/rebootcontrol/templates/_page.html
new file mode 100644
index 00000000..690316df
--- /dev/null
+++ b/modules-available/rebootcontrol/templates/_page.html
@@ -0,0 +1,245 @@
+<form id="tableDataForm" method="post" action="?do=rebootcontrol" class="form-inline">
+ <input type="hidden" name="token" value="{{token}}">
+ <div class="row">
+ <div class="col-md-12">
+ <label>{{lang_location}}:
+ <select id="locationDropdown" name="locationId" class="form-control" onchange="selectLocation()">
+ {{#locations}}
+ <option value="{{locationid}}" {{#selected}}selected{{/selected}}>{{locationpad}} {{locationname}}</option>
+ {{/locations}}
+ </select>
+ </label>
+ <button type="button" id="settingsButton" class="btn btn-default pull-right" data-toggle="modal" data-target="#settingsModal"><span class="glyphicon glyphicon-cog"></span></button>
+ <button type="button" id="selectAllButton" class="btn btn-primary pull-right" onclick="selectAllRows()"><span class="glyphicon glyphicon-check"></span> {{lang_selectall}}</button>
+ <button type="button" id="unselectAllButton" class="btn btn-default pull-right" onclick="unselectAllRows()" style="display: none;"><span class="glyphicon glyphicon-unchecked"></span> {{lang_unselectall}}</button>
+ <button type="button" id="rebootButton" class="btn btn-warning pull-right" data-toggle="modal" data-target="#rebootModal" disabled><span class="glyphicon glyphicon-repeat"></span> {{lang_rebootButton}}</button>
+ <button type="button" id="shutdownButton" class="btn btn-danger pull-right" data-toggle="modal" data-target="#shutdownModal" disabled><span class="glyphicon glyphicon-off"></span> {{lang_shutdownButton}}</button>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-md-12">
+ <table class="table table-condensed table-hover" id="dataTable">
+ <thead>
+ <tr>
+ <th data-sort="string">{{lang_client}}</th>
+ <th data-sort="ipsort">{{lang_ip}}</th>
+ <th data-sort="string">{{lang_status}}</th>
+ <th data-sort="string">{{lang_session}}</th>
+ <th data-sort="string">{{lang_user}}</th>
+ <th data-sort="int" data-sort-default="desc">{{lang_selected}}</th>
+ </tr>
+ </thead>
+
+ <tbody>
+ {{#data}}
+ <tr>
+ <td>
+ {{hostname}}
+ {{^hostname}}{{clientip}}{{/hostname}}
+ </td>
+ <td>{{clientip}}</td>
+ <td class="statusColumn">
+ {{#status}}
+ {{lang_on}}
+ {{/status}}
+ {{^status}}
+ {{lang_off}}
+ {{/status}}
+ </td>
+ <td>{{#status}}{{currentsession}}{{/status}}</td>
+ <td>{{#status}}{{currentuser}}{{/status}}</td>
+ <td data-sort-value="0" class="checkboxColumn">
+ <div class="checkbox">
+ <input id="m-{{machineuuid}}" type="checkbox" name="clients[]" value='{{machineuuid}}'>
+ <label for="m-{{machineuuid}}"></label>
+ </div>
+ </td>
+ </tr>
+ {{/data}}
+ </tbody>
+ </table>
+ </div>
+ </div>
+
+
+ <!-- Modals -->
+
+ <div id="settingsModal" class="modal fade" role="dialog">
+ <div class="modal-dialog">
+
+ <div class="modal-content">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal">&times;</button>
+ <h4 class="modal-title"><b>{{lang_settings}}</b></h4>
+ </div>
+ <div class="modal-body">
+ <span id="pubKeyTitle">{{lang_pubKey}}</span>
+ <button class="btn btn-s btn-warning pull-right" onclick="generateNewKeypair()" type="button">{{lang_genNew}}</button>
+ <pre id="pubKey">{{pubKey}}</pre>
+ </div>
+ <div class="modal-footer">
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class ="modal fade" id="rebootModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
+ <div class="modal-dialog" role="document">
+ <div class="modal-content">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
+ <h4 class="modal-title" id="myModalLabel">{{lang_rebootButton}}</h4>
+ </div>
+ <div class="modal-body">
+ {{lang_rebootCheck}}
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-default" data-dismiss="modal">{{lang_cancel}}</button>
+ <button type="submit" name="action" value="startReboot" class="btn btn-warning"><span class="glyphicon glyphicon-repeat"></span> {{lang_reboot}}</button>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class ="modal fade" id="shutdownModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
+ <div class="modal-dialog" role="document">
+ <div class="modal-content">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
+ <h4 class="modal-title" id="myModalLabel2">{{lang_shutdownButton}}</h4>
+ </div>
+ <div class="modal-body">
+ {{lang_shutdownCheck}}
+ {{lang_shutdownIn}} <input id="shutdownTimer" name="minutes" title="{{lang_shutdownIn}}" type="number" value="0" min="0" onkeypress="return isNumberKey(event)"> {{lang_minutes}}
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-default" data-dismiss="modal">{{lang_cancel}}</button>
+ <button type="submit" name="action" value="startShutdown" class="btn btn-danger"><span class="glyphicon glyphicon-off"></span> {{lang_shutdownButton}}</button>
+ </div>
+ </div>
+ </div>
+ </div>
+</form>
+
+
+<script type="application/javascript">
+ document.addEventListener("DOMContentLoaded", function() {
+ markCheckedRows();
+
+ $('input:checkbox').change(
+ function(){
+ //give each checkbox the function to mark the row (in green)
+ if ($(this).is(':checked')) {
+ markRows($(this).closest("tr"), true);
+ $(this).closest("td").data("sort-value", 1);
+ } else {
+ markRows($(this).closest("tr"), false);
+ $(this).closest("td").data("sort-value", 0);
+ }
+
+ //if all are checked, change the selectAll-Button to unselectAll. if one is not checked, change unselectAll to selectAll
+ var dataTable = $("#dataTable");
+ var unchecked = dataTable.find("input[type=checkbox]:not(:checked)").length;
+ if (unchecked == 0) {
+ $('#selectAllButton').hide();
+ $('#unselectAllButton').show();
+ } else if (unchecked == 1) {
+ $('#selectAllButton').show();
+ $('#unselectAllButton').hide();
+ }
+
+ //if no client is selected, disable the shutdown/reboot button, and enable them if a client is selected
+ var checked = dataTable.find("input[type=checkbox]:checked").length;
+ if (checked == 0) {
+ $('#rebootButton').prop('disabled', true);
+ $('#shutdownButton').prop('disabled', true);
+ } else {
+ $('#rebootButton').prop('disabled', false);
+ $('#shutdownButton').prop('disabled', false);
+ }
+ });
+ $('.checkboxColumn').click(function(e) {
+ if (e.target === this) {
+ $(this).find('input[type="checkbox"]').click();
+ }
+ });
+ });
+
+ // Change Location when selected in Dropdown Menu
+ function selectLocation() {
+ var dropdown = $("#locationDropdown");
+ var location = dropdown.val();
+ window.location.replace("?do=rebootcontrol&location="+location);
+ }
+
+ // Check all checkboxes, change selectAll button, make shutdown/reboot button enabled as clients will certainly be selected
+ function selectAllRows() {
+ var checked = $("tr input:checkbox:checked");
+
+ //change button
+ $('#selectAllButton').hide();
+ $('#unselectAllButton').show();
+
+ //check rows and mark them
+ $('input[type="checkbox"]', '#dataTable').prop('checked', true);
+ markRows($("tr:not(:first)"), true);
+ $(".checkboxColumn").data("sort-value", 1);
+
+ //enable shutdown/reboot button
+ $('#rebootButton').prop('disabled', false);
+ $('#shutdownButton').prop('disabled', false);
+ }
+
+ // Uncheck all checkboxes, change unselectAll Button, make shutdown/reboot button disabled as clients will certainly be not selected
+ function unselectAllRows() {
+ //change button
+ $('#selectAllButton').show();
+ $('#unselectAllButton').hide();
+
+ //uncheck rows and unmark them
+ $('input[type="checkbox"]', '#dataTable').prop('checked', false);
+ markRows($("tr"), false);
+ $(".checkboxColumn").data("sort-value", 0);
+
+ //disable shutdown/reboot button
+ $('#rebootButton').prop('disabled', true);
+ $('#shutdownButton').prop('disabled', true);
+ }
+
+ // mark all previous checked rows (used when loading site)
+ function markCheckedRows() {
+ var checked = $("tr input:checkbox:checked");
+ markRows(checked.closest("tr"), true);
+ var unchecked = $("#dataTable").find("input[type=checkbox]:not(:checked)").length;
+ if(unchecked == 0) {
+ $('#selectAllButton').hide();
+ $('#unselectAllButton').show();
+ }
+ }
+
+ // only allow numbers to get typed into the "shutdown in X Minutes" box.
+ function isNumberKey(evt){
+ var charCode = (evt.which) ? evt.which : event.keyCode;
+ return !(charCode > 31 && (charCode < 48 || charCode > 57));
+ }
+
+ function markRows($rows, marked) {
+ if (marked) {
+ $rows.addClass('active');
+ } else {
+ $rows.removeClass('active');
+ }
+ }
+
+ function generateNewKeypair() {
+ $.ajax({
+ url: '?do=rebootcontrol',
+ type: 'POST',
+ data: { action: "generateNewKeypair", token: TOKEN },
+ success: function(value) {
+ $('#pubKey').text(value);
+ }
+ });
+ }
+
+</script> \ No newline at end of file
diff --git a/modules-available/rebootcontrol/templates/status.html b/modules-available/rebootcontrol/templates/status.html
new file mode 100644
index 00000000..35bbe42f
--- /dev/null
+++ b/modules-available/rebootcontrol/templates/status.html
@@ -0,0 +1,62 @@
+<div>
+ <form class="form-inline">
+ <b>{{lang_location}}: {{locationName}}</b>
+ <input type="hidden" name="do" value="rebootcontrol">
+ <input type="hidden" name="location" value="{{locationId}}">
+ <button type="submit" class="btn btn-primary pull-right"><span class="glyphicon glyphicon-arrow-left"></span> {{lang_back}}</button>
+ </form>
+</div>
+
+<div data-tm-id="{{taskId}}" data-tm-log="error" data-tm-callback="updateStatus"></div>
+
+<div>
+ <table class="table table-hover" id="dataTable">
+ <thead>
+ <tr>
+ <th data-sort="string">{{lang_client}}</th>
+ <th data-sort="ipsort">{{lang_ip}}</th>
+ <th data-sort="string">
+ {{lang_status}}
+ </th>
+ </tr>
+ </thead>
+
+ <tbody>
+ {{#clients}}
+ <tr>
+ <td>{{machineuuid}}</td>
+ <td>{{clientip}}</td>
+ <td id="status-{{machineuuid}}"></td>
+ </tr>
+ {{/clients}}
+ </tbody>
+ </table>
+</div>
+
+<script type="application/javascript">
+ statusStrings = {
+ "CONNECTING" : "{{lang_connecting}}",
+ "REBOOTING" : "{{lang_rebooting}}",
+ "REBOOT_AT" : "{{lang_rebootAt}}",
+ "ONLINE" : "{{lang_online}}",
+ "ERROR" : "{{lang_error}}",
+ "SHUTDOWN" : "{{lang_shutdown}}",
+ "SHUTDOWN_AT" : "{{lang_shutdownAt}}",
+ "AUTH_FAIL" : "{{lang_authFail}}"
+ };
+
+ function updateStatus(task) {
+ if (!task || !task.data || !task.data.clientStatus)
+ return;
+ var clientStatus = task.data.clientStatus;
+ for (var uuid in clientStatus) {
+ if (clientStatus.hasOwnProperty(uuid)) {
+ var shutdownTime = ' ';
+ if (clientStatus[uuid] === 'SHUTDOWN_AT' || clientStatus[uuid] === 'REBOOT_AT') {
+ shutdownTime += task.data.time;
+ }
+ $("#status-" + uuid).text(statusStrings[clientStatus[uuid]] + shutdownTime);
+ }
+ }
+ }
+</script>
diff --git a/modules-available/roomplanner/js/grid.js b/modules-available/roomplanner/js/grid.js
index cc09e21d..334057bf 100644
--- a/modules-available/roomplanner/js/grid.js
+++ b/modules-available/roomplanner/js/grid.js
@@ -17,6 +17,7 @@ if (!roomplanner) var roomplanner = {
},
settings: {
cellsep: 4,
+ cellsize: 25,
scale: 100,
room: {
width: 33,
@@ -363,6 +364,7 @@ if (!roomplanner) var roomplanner = {
});
roomplanner.grid.scale(roomplanner.settings.scale);
+ roomplanner.fitContent();
},
clear: function() {
$('#draw-element-area').html('');
@@ -393,20 +395,18 @@ roomplanner.grid = (function() {
$('#drawarea').css('background-size',num);
roomplanner.settings.scale = num;
$('#draw-element-area .ui-draggable').each(function(idx,item) {
- var h = $(item).attr('data-height') * roomplanner.getScaleFactor();
- var w = $(item).attr('data-width') * roomplanner.getScaleFactor();
+ var $item = $(item);
+ var h = $item.attr('data-height') * roomplanner.getScaleFactor();
+ var w = $item.attr('data-width') * roomplanner.getScaleFactor();
//var pos = roomplanner.getCelloffset()
+
+ var pos = roomplanner.getCellPositionFromGrid($item.attr('gridRow'),$item.attr('gridCol'));
- var l = parseInt($(item).css('left')) * roomplanner.getScaleFactor();
- var t = parseInt($(item).css('top')) * roomplanner.getScaleFactor();
-
- var pos = roomplanner.getCellPositionFromGrid($(item).attr('gridRow'),$(item).attr('gridCol'));
-
- $(item).css({width: w+"px", height: h+"px", left: pos[0]+"px", top: pos[1]+"px"});
- $(item).draggable("option","grid",[(roomplanner.settings.scale / 4), (roomplanner.settings.scale / 4)]);
+ $item.css({width: w+"px", height: h+"px", left: pos[0]+"px", top: pos[1]+"px"});
+ $item.draggable("option","grid",[(roomplanner.settings.scale / 4), (roomplanner.settings.scale / 4)]);
if (roomplanner.isElementResizable(item)) {
- $(item).resizable("option","grid",[(roomplanner.settings.scale / 4), (roomplanner.settings.scale / 4)]);
+ $item.resizable("option","grid",[(roomplanner.settings.scale / 4), (roomplanner.settings.scale / 4)]);
}
});
this.resize();
@@ -423,6 +423,44 @@ roomplanner.grid = (function() {
}
)();
+roomplanner.fitContent = function() {
+ var minX = 99999;
+ var minY = 99999;
+ var maxX = -99999;
+ var maxY = -99999;
+ $('#draw-element-area .ui-draggable').each(function(idx,item) {
+ var $item = $(item);
+
+ var l = parseInt($item.attr('gridcol')) * roomplanner.settings.cellsize;
+ var r = l + parseInt($item.attr('data-width'));
+ var t = parseInt($item.attr('gridrow')) * roomplanner.settings.cellsize;
+ var b = t + parseInt($item.attr('data-height'));
+
+ if (l < minX) minX = l;
+ if (t < minY) minY = t;
+ if (r > maxX) maxX = r;
+ if (b > maxY) maxY = b;
+ });
+ if (minX > maxX)
+ return;
+ var width = (maxX - minX) / $('#drawpanel .panel-body').width();
+ var height = (maxY - minY) / $('#drawpanel .panel-body').height();
+ var scale;
+ if (width > height) {
+ scale = Math.floor(100 / width);
+ } else {
+ scale = Math.floor(100 / height);
+ }
+ roomplanner.slider.slider('value', scale);
+ scale = roomplanner.settings.scale;
+ var opts = {
+ left: -(minX * (scale / 100)) + "px",
+ top: -(minY * (scale / 100)) + "px"
+ };
+
+ $('#drawarea').css(opts);
+};
+
$(document).ready(function(){
roomplanner.grid.init();
diff --git a/modules-available/roomplanner/js/init.js b/modules-available/roomplanner/js/init.js
index ef3d15a7..7cada0dd 100644
--- a/modules-available/roomplanner/js/init.js
+++ b/modules-available/roomplanner/js/init.js
@@ -59,6 +59,10 @@ function initRoomplanner() {
$('#zoom-in').click(function() {
roomplanner.slider.slider('value', roomplanner.settings.scale + 10);
});
+
+ $('#zoom-fit').click(function() {
+ roomplanner.fitContent();
+ });
}
var translation = {
diff --git a/modules-available/roomplanner/style.css b/modules-available/roomplanner/style.css
index 1460364a..6a68a444 100644
--- a/modules-available/roomplanner/style.css
+++ b/modules-available/roomplanner/style.css
@@ -40,7 +40,7 @@ body {
#scaleContainer {
position: absolute;
bottom: 5px;
- right: 30px;
+ right: 50px;
width: 15%;
z-index:1000;
}
@@ -48,7 +48,7 @@ body {
#scaleslider {
position:relative;}
-#zoom-out, #zoom-in {
+#zoom-out, #zoom-in, #zoom-fit {
cursor:pointer;
}
@@ -61,6 +61,9 @@ body {
#scaleContainer .glyphicon-zoom-out {
left:-20px;}
+#scaleContainer .glyphicon-move {
+ right:-40px;}
+
#scaleContainer .glyphicon-zoom-in {
right:-20px; }
diff --git a/modules-available/roomplanner/templates/page.html b/modules-available/roomplanner/templates/page.html
index 8bfa0ca4..e8544ce8 100644
--- a/modules-available/roomplanner/templates/page.html
+++ b/modules-available/roomplanner/templates/page.html
@@ -301,9 +301,12 @@
<div id="draw-element-area" style="width:100%; height:100%;"></div>
</div>
<div id="scaleContainer">
+ <span id="zoom-fit" class="glyphicon glyphicon-move" aria-hidden="true"></span>
+ <div>
<div id="scaleslider"></div>
<span id="zoom-out" class="glyphicon glyphicon-zoom-out" aria-hidden="true"></span>
<span id="zoom-in" class="glyphicon glyphicon-zoom-in" aria-hidden="true"></span>
+ </div>
</div>
</div>
@@ -311,7 +314,7 @@
</div>
<div class="pull-left">
- <div class="input-group" style="width:400px">
+ <div class="input-group" style="width:1px">
<div class="input-group-addon">{{lang_managerIp}}</div>
<input class="form-control" type="text" id="manager-ip" value="{{managerip}}" placeholder="1.2.3.4" style="width:120px">
<div class="input-group-addon checkbox"><input id="dedi-mgr" type="checkbox" {{dediMgrChecked}}> <label for="dedi-mgr">{{lang_dedicatedManager}}</label></div>
diff --git a/modules-available/serversetup-bwlp/page.inc.php b/modules-available/serversetup-bwlp/page.inc.php
index 9bea4b50..9d7d11ac 100644
--- a/modules-available/serversetup-bwlp/page.inc.php
+++ b/modules-available/serversetup-bwlp/page.inc.php
@@ -81,8 +81,13 @@ class Page_ServerSetup extends Page
return false;
}
- if ($this->taskStatus['statusCode'] === TASK_WAITING) { // TODO: Async if just displaying
- $this->taskStatus = Taskmanager::waitComplete($this->taskStatus['id']);
+ if (!Taskmanager::isFinished($this->taskStatus)) { // TODO: Async if just displaying
+ $this->taskStatus = Taskmanager::waitComplete($this->taskStatus['id'], 4000);
+ }
+
+ if (Taskmanager::isFailed($this->taskStatus) || !isset($this->taskStatus['data']['addresses'])) {
+ $this->taskStatus['data']['addresses'] = false;
+ return false;
}
$sortIp = array();
diff --git a/modules-available/statistics/api.inc.php b/modules-available/statistics/api.inc.php
index 2ac6e782..126c6e91 100644
--- a/modules-available/statistics/api.inc.php
+++ b/modules-available/statistics/api.inc.php
@@ -201,6 +201,79 @@ if ($type{0} === '~') {
}
}
Database::exec('UPDATE machine SET logintime = 0, lastseen = UNIX_TIMESTAMP(), lastboot = 0 WHERE machineuuid = :uuid', array('uuid' => $uuid));
+ } elseif ($type === '~screens') {
+ $screens = Request::post('screen', false, 'array');
+ if (is_array($screens)) {
+ // `devicetype`, `devicename`, `subid`, `machineuuid`
+ // Make sure all screens are in the general hardware table
+ $hwids = array();
+ foreach ($screens as $port => $screen) {
+ if (!array_key_exists('name', $screen))
+ continue;
+ if (array_key_exists($screen['name'], $hwids)) {
+ $hwid = $hwids[$screen['name']];
+ } else {
+ $hwid = (int)Database::insertIgnore('statistic_hw', 'hwid',
+ array('hwtype' => DeviceType::SCREEN, 'hwname' => $screen['name']));
+ $hwids[$screen['name']] = $hwid;
+ }
+ // Now add new entries
+ $machinehwid = Database::insertIgnore('machine_x_hw', 'machinehwid', array(
+ 'hwid' => $hwid,
+ 'machineuuid' => $uuid,
+ 'devpath' => $port,
+ ), array('disconnecttime' => 0));
+ $validProps = array();
+ if (count($screen) > 1) {
+ // Screen has additional properties (resolution, size, etc.)
+ unset($screen['name']);
+ foreach ($screen as $key => $value) {
+ if (!preg_match('/^[a-zA-Z0-9][\x21-\x7e]{0,15}$/', $key)) {
+ echo "No matsch '$key'\n";
+ continue; // Ignore evil key names
+ }
+ $validProps[] = $key;
+ Database::exec("INSERT INTO machine_x_hw_prop (machinehwid, prop, value)"
+ . " VALUES (:id, :key, :value) ON DUPLICATE KEY UPDATE value = VALUES(value)", array(
+ 'id' => $machinehwid,
+ 'key' => $key,
+ 'value' => $value,
+ ));
+ }
+ }
+ // Purge properties that might have existed in the past
+ if (empty($validProps)) {
+ Database::exec("DELETE FROM machine_x_hw_prop WHERE machinehwid = :machinehwid AND prop NOT LIKE '@%'",
+ array('machinehwid' => $machinehwid));
+ } else {
+ $qs = '?' . str_repeat(',?', count($validProps) - 1);
+ array_unshift($validProps, $machinehwid);
+ Database::exec("DELETE FROM machine_x_hw_prop"
+ . " WHERE machinehwid = ? AND prop NOT LIKE '@%' AND prop NOT IN ($qs)",
+ $validProps);
+ }
+ }
+ // Remove/disable stale entries
+ if (empty($hwids)) {
+ // No screens connected at all, purge all screen entries for this machine
+ Database::exec("UPDATE machine_x_hw x, statistic_hw h"
+ . " SET x.disconnecttime = UNIX_TIMESTAMP()"
+ . " WHERE x.machineuuid = :uuid AND x.hwid = h.hwid AND h.hwtype = :type AND x.disconnecttime = 0",
+ array('uuid' => $uuid, 'type' => DeviceType::SCREEN));
+ } else {
+ // Some screens connected, make sure old entries get removed
+ $params = array_values($hwids);
+ array_unshift($params, $uuid);
+ array_unshift($params, DeviceType::SCREEN);
+ $qs = '?' . str_repeat(',?', count($hwids) - 1);
+ Database::exec("UPDATE machine_x_hw x, statistic_hw h"
+ . " SET x.disconnecttime = UNIX_TIMESTAMP()"
+ . " WHERE h.hwid = x.hwid AND x.disconnecttime = 0 AND h.hwtype = ? AND x.machineuuid = ? AND x.hwid NOT IN ($qs)", $params);
+
+ }
+ }
+ } else {
+ die("INVALID ACTION '$type'");
}
die("OK. (RESULT=0)\n");
}
diff --git a/modules-available/statistics/hooks/config-tgz.inc.php b/modules-available/statistics/hooks/config-tgz.inc.php
new file mode 100644
index 00000000..1272a94f
--- /dev/null
+++ b/modules-available/statistics/hooks/config-tgz.inc.php
@@ -0,0 +1,32 @@
+<?php
+
+$res = Database::simpleQuery('SELECT h.hwname FROM statistic_hw h'
+ . " INNER JOIN statistic_hw_prop p ON (h.hwid = p.hwid AND p.prop = :projector)"
+ . " WHERE h.hwtype = :screen ORDER BY h.hwname ASC", array(
+ 'projector' => 'projector',
+ 'screen' => DeviceType::SCREEN,
+));
+
+$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);
+ $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;
+ }
+}
diff --git a/modules-available/statistics/inc/devicetype.inc.php b/modules-available/statistics/inc/devicetype.inc.php
new file mode 100644
index 00000000..41ee237d
--- /dev/null
+++ b/modules-available/statistics/inc/devicetype.inc.php
@@ -0,0 +1,6 @@
+<?php
+
+class DeviceType
+{
+ const SCREEN = 'SCREEN';
+}
diff --git a/modules-available/statistics/install.inc.php b/modules-available/statistics/install.inc.php
index 7baf046e..79346f99 100644
--- a/modules-available/statistics/install.inc.php
+++ b/modules-available/statistics/install.inc.php
@@ -12,7 +12,7 @@ $res[] = tableCreate('statistic', "
`dateline` int(10) unsigned NOT NULL,
`typeid` varchar(30) NOT NULL,
`clientip` varchar(40) NOT NULL,
- `machineuuid` varchar(36) CHARACTER SET ascii DEFAULT NULL,
+ `machineuuid` char(36) CHARACTER SET ascii DEFAULT NULL,
`username` varchar(30) NOT NULL,
`data` varchar(255) NOT NULL,
PRIMARY KEY (`logid`),
@@ -24,7 +24,7 @@ $res[] = tableCreate('statistic', "
// Main table containing all known clients
-$res[] = tableCreate('machine', "
+$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),
@@ -61,6 +61,40 @@ $res[] = tableCreate('machine', "
KEY `systemmodel` (`systemmodel`)
");
+$res[] = $machineHwCreate = tableCreate('machine_x_hw', "
+ `machinehwid` int(10) unsigned NOT NULL AUTO_INCREMENT,
+ `hwid` int(10) unsigned NOT NULL,
+ `machineuuid` char(36) CHARACTER SET ascii NOT NULL,
+ `devpath` char(50) CHARACTER SET ascii NOT NULL,
+ `disconnecttime` int(10) unsigned NOT NULL COMMENT 'time the device was not connected to the pc anymore for the first time, 0 if it is connected',
+ PRIMARY KEY (`machinehwid`),
+ UNIQUE KEY `hwid` (`hwid`,`machineuuid`,`devpath`),
+ KEY `machineuuid` (`machineuuid`,`hwid`),
+ KEY `disconnecttime` (`disconnecttime`)
+ ");
+
+$res[] = tableCreate('machine_x_hw_prop', "
+ `machinehwid` int(10) unsigned NOT NULL,
+ `prop` char(16) CHARACTER SET ascii NOT NULL,
+ `value` varchar(500) NOT NULL,
+ PRIMARY KEY (`machinehwid`,`prop`)
+");
+
+$res[] = tableCreate('statistic_hw', "
+ `hwid` int(10) unsigned NOT NULL AUTO_INCREMENT,
+ `hwtype` char(11) CHARACTER SET ascii NOT NULL,
+ `hwname` varchar(200) NOT NULL,
+ PRIMARY KEY (`hwid`),
+ UNIQUE KEY `hwtype` (`hwtype`,`hwname`)
+");
+
+$res[] = tableCreate('statistic_hw_prop', "
+ `hwid` int(10) unsigned NOT NULL,
+ `prop` char(16) CHARACTER SET ascii NOT NULL,
+ `value` varchar(500) NOT NULL,
+ PRIMARY KEY (`hwid`,`prop`)
+");
+
// PCI-ID cache
$res[] = tableCreate('pciid', "
@@ -71,7 +105,8 @@ $res[] = tableCreate('pciid', "
PRIMARY KEY (`category`,`id`)
");
-if (in_array(UPDATE_DONE, $res)) {
+// need trigger?
+if ($machineCreate === UPDATE_DONE) {
$addTrigger = true;
}
@@ -165,6 +200,25 @@ if ($addTrigger) {
}
}
+if ($machineHwCreate === UPDATE_DONE) {
+ $ret = Database::exec('ALTER TABLE `machine_x_hw`
+ ADD CONSTRAINT `machine_x_hw_ibfk_1` FOREIGN KEY (`hwid`) REFERENCES `statistic_hw` (`hwid`) ON DELETE CASCADE,
+ ADD CONSTRAINT `machine_x_hw_ibfk_2` FOREIGN KEY (`machineuuid`) REFERENCES `machine` (`machineuuid`) ON DELETE CASCADE');
+ if ($ret === false) {
+ finalResponse(UPDATE_FAILED, 'Adding constraints to machine_x_hw failed: ' . Database::lastError());
+ }
+ $ret = Database::exec('ALTER TABLE `machine_x_hw_prop`
+ ADD CONSTRAINT `machine_x_hw_prop_ibfk_1` FOREIGN KEY (`machinehwid`) REFERENCES `machine_x_hw` (`machinehwid`) ON DELETE CASCADE');
+ if ($ret === false) {
+ finalResponse(UPDATE_FAILED, 'Adding constraint to machine_x_hw_prop failed: ' . Database::lastError());
+ }
+ $ret = Database::exec('ALTER TABLE `statistic_hw_prop`
+ ADD CONSTRAINT `statistic_hw_prop_ibfk_1` FOREIGN KEY (`hwid`) REFERENCES `statistic_hw` (`hwid`) ON DELETE CASCADE');
+ if ($ret === false) {
+ finalResponse(UPDATE_FAILED, 'Adding constraint to statistic_hw_prop failed: ' . Database::lastError());
+ }
+}
+
// Create response
if (in_array(UPDATE_DONE, $res)) {
diff --git a/modules-available/statistics/lang/de/template-tags.json b/modules-available/statistics/lang/de/template-tags.json
index ca6c56a7..7274aef4 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_noProjectorsDefined": "Keine Beamer-Overrides definiert",
"lang_notes": "Anmerkungen",
"lang_onlineMachines": "Gestartete Clients",
"lang_partName": "Name",
@@ -55,17 +56,21 @@
"lang_partitionSize": "Gr\u00f6\u00dfe",
"lang_pendingSectors": "Potentiell defekte Sektoren",
"lang_powerOnTime": "Betriebszeit",
+ "lang_projector": "Beamer",
+ "lang_projectors": "Beamer",
"lang_ram": "Arbeitsspeicher",
"lang_ramSize": "Gr\u00f6\u00dfe",
"lang_ramSlots": "Speicher-Slots",
"lang_realCores": "Kerne",
"lang_reallocatedSectors": "Defekte Sektoren",
+ "lang_screens": "Bildschirme",
"lang_serialNo": "Serien-Nr",
"lang_showList": "Liste",
"lang_showVisualization": "Visualisierung",
"lang_sockets": "Sockel",
"lang_tempPart": "Temp. Partition",
"lang_tempPartStats": "Tempor\u00e4re Partition",
+ "lang_thoseAreProjectors": "Diese Modellnamen werden als Beamer behandelt, auch wenn die EDID-Informationen des Ger\u00e4tes anderes berichten.",
"lang_timebarDesc": "Visuelle Darstellung der letzten Tage. Rote Abschnitte zeigen, wann der Rechner belegt war, gr\u00fcne, wann er nicht verwendet wurde, aber eingeschaltet war. Die leicht abgedunkelten Abschnitte markieren N\u00e4chte (22 bis 8 Uhr).",
"lang_tmpGb": "HDD-Temp",
"lang_total": "Gesamt",
diff --git a/modules-available/statistics/lang/en/template-tags.json b/modules-available/statistics/lang/en/template-tags.json
index 55003ea9..4e135388 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_noProjectorsDefined": "No projector overrides defined",
"lang_notes": "Notes",
"lang_onlineMachines": "Online clients",
"lang_partName": "Name",
@@ -55,17 +56,21 @@
"lang_partitionSize": "Size",
"lang_pendingSectors": "Sectors pending reallocation",
"lang_powerOnTime": "Power on time",
+ "lang_projector": "Projector",
+ "lang_projectors": "Projectors",
"lang_ram": "Memory",
"lang_ramSize": "Size",
"lang_ramSlots": "Memory slots",
"lang_realCores": "Cores",
"lang_reallocatedSectors": "Bad sectors",
+ "lang_screens": "Screens",
"lang_serialNo": "Serial no",
"lang_showList": "Show list",
"lang_showVisualization": "Show visualization",
"lang_sockets": "Sockets",
"lang_tempPart": "Temp. partition",
"lang_tempPartStats": "Temporary partition",
+ "lang_thoseAreProjectors": "These model names will always be treated as beamers, even if the device's EDID data says otherwise.",
"lang_timebarDesc": "Visual representation of the last few days. Red parts mark periods where the client was occupied, green parts where the client was idle. Dimmed parts mark nights (10pm to 8am).",
"lang_tmpGb": "HDD temp",
"lang_total": "Total",
diff --git a/modules-available/statistics/page.inc.php b/modules-available/statistics/page.inc.php
index 6a9acd14..5ad8bc20 100644
--- a/modules-available/statistics/page.inc.php
+++ b/modules-available/statistics/page.inc.php
@@ -119,6 +119,53 @@ class Page_Statistics extends Page
/* TODO ... */
}
+ /*
+ * TODO: Move to separate unit... hardware configurator?
+ */
+
+ protected function handleProjector($action)
+ {
+ $hwid = Request::post('hwid', false, 'int');
+ if ($hwid === false) {
+ Util::traceError('Param hwid missing');
+ }
+ if ($action === 'addprojector') {
+ Database::exec('INSERT INTO statistic_hw_prop (hwid, prop, value)'
+ . ' VALUES (:hwid, :prop, :value)', array(
+ 'hwid' => $hwid,
+ 'prop' => 'projector',
+ 'value' => 'true',
+ ));
+ } else {
+ Database::exec('DELETE FROM statistic_hw_prop WHERE hwid = :hwid AND prop = :prop', array(
+ 'hwid' => $hwid,
+ 'prop' => 'projector',
+ ));
+ }
+ if (Module::isAvailable('sysconfig')) {
+ ConfigTgz::rebuildAllConfigs();
+ }
+ Util::redirect('?do=statistics&show=projectors');
+ }
+
+ protected function showProjectors()
+ {
+ $res = Database::simpleQuery('SELECT h.hwname, h.hwid FROM statistic_hw h'
+ . " INNER JOIN statistic_hw_prop p ON (h.hwid = p.hwid AND p.prop = :projector)"
+ . " WHERE h.hwtype = :screen ORDER BY h.hwname ASC", array(
+ 'projector' => 'projector',
+ 'screen' => DeviceType::SCREEN,
+ ));
+ $data = array(
+ 'projectors' => $res->fetchAll(PDO::FETCH_ASSOC)
+ );
+ Render::addTemplate('projector-list', $data);
+ }
+
+ /*
+ * End TODO
+ */
+
protected function doPreprocess()
{
$this->initConstants();
@@ -140,6 +187,8 @@ class Page_Statistics extends Page
));
Message::addSuccess('notes-saved');
Util::redirect('?do=Statistics&uuid=' . $uuid);
+ } elseif ($action === 'addprojector' || $action === 'delprojector') {
+ $this->handleProjector($action);
}
// Fix online state of machines that crashed -- TODO: Make cronjob for this
Database::exec("UPDATE machine SET lastboot = 0 WHERE lastseen < UNIX_TIMESTAMP() - 610");
@@ -174,6 +223,9 @@ class Page_Statistics extends Page
Render::closeTag('div');
$this->showMachineList($filterSet);
return;
+ } elseif ($show === 'projectors') {
+ $this->showProjectors();
+ return;
}
Render::openTag('div', array('class' => 'row'));
$this->showFilter('stat', $filterSet);
@@ -723,6 +775,24 @@ class Page_Statistics extends Page
}
$client['locations'] = $output;
}
+ // Screens TODO Move everything else to hw table instead of blob parsing above
+ // `devicetype`, `devicename`, `subid`, `machineuuid`
+ $res = Database::simpleQuery("SELECT m.hwid, h.hwname, m.devpath AS connector, m.disconnecttime,"
+ . " p.value AS resolution, q.prop AS projector FROM machine_x_hw m"
+ . " INNER JOIN statistic_hw h ON (m.hwid = h.hwid AND h.hwtype = :screen)"
+ . " LEFT JOIN machine_x_hw_prop p ON (m.machinehwid = p.machinehwid AND p.prop = 'resolution')"
+ . " LEFT JOIN statistic_hw_prop q ON (m.hwid = q.hwid AND q.prop = 'projector')"
+ . " WHERE m.machineuuid = :uuid",
+ array('screen' => DeviceType::SCREEN, 'uuid' => $uuid));
+ $client['screens'] = array();
+ $ports = array();
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ if ($row['disconnecttime'] != 0)
+ continue;
+ $ports[] = $row['connector'];
+ $client['screens'][] = $row;
+ }
+ array_multisort($ports, SORT_ASC, $client['screens']);
// Throw output at user
Render::addTemplate('machine-main', $client);
// Sessions
@@ -806,7 +876,7 @@ class Page_Statistics extends Page
// Client log
if (Module::get('syslog') !== false) {
$lres = Database::simpleQuery('SELECT logid, dateline, logtypeid, clientip, description, extra FROM clientlog'
- . ' WHERE clientip = :clientip ORDER BY logid DESC LIMIT 25', array('clientip' => $client['clientip']));
+ . ' WHERE machineuuid = :uuid ORDER BY logid DESC LIMIT 25', array('uuid' => $client['machineuuid']));
$today = date('d.m.Y');
$yesterday = date('d.m.Y', time() - 86400);
$count = 0;
diff --git a/modules-available/statistics/templates/machine-main.html b/modules-available/statistics/templates/machine-main.html
index 0b333a27..bdc51167 100644
--- a/modules-available/statistics/templates/machine-main.html
+++ b/modules-available/statistics/templates/machine-main.html
@@ -130,6 +130,36 @@
<td class="text-nowrap">{{lang_64bitSupport}}</td>
<td>{{kvmstate}}</td>
</tr>
+ <tr>
+ <td class="text-nowrap">{{lang_screens}}</td>
+ <td>
+ <form method="post" action="?do=statistics" id="delprojector">
+ <input type="hidden" name="token" value="{{token}}">
+ <input type="hidden" name="action" value="delprojector">
+ </form>
+ <form method="post" action="?do=statistics" id="addprojector">
+ <input type="hidden" name="token" value="{{token}}">
+ <input type="hidden" name="action" value="addprojector">
+ </form>
+ {{#screens}}
+ <div class="small">
+ <div class="pull-right btn-group btn-group-xs">
+ {{#projector}}
+ <a href="?do=statistics&amp;show=projectors" class="btn btn-default">{{lang_projector}}</a>
+ <button form="delprojector" type="submit" name="hwid" value="{{hwid}}"
+ class="btn btn-danger"><span class="glyphicon glyphicon-remove"></span></button>
+ {{/projector}}
+ {{^projector}}
+ <button form="addprojector" type="submit" name="hwid" value="{{hwid}}"
+ class="btn btn-success"><span class="glyphicon glyphicon-plus"></span> {{lang_projector}}</button>
+ {{/projector}}
+ </div>
+ {{connector}}: <b>{{hwname}}</b> {{resolution}}
+ <div class="clearfix"></div>
+ </div>
+ {{/screens}}
+ </td>
+ </tr>
</table>
<h4>{{lang_devices}}</h4>
{{#lspci1}}
diff --git a/modules-available/statistics/templates/projector-list.html b/modules-available/statistics/templates/projector-list.html
new file mode 100644
index 00000000..bc9ecdbd
--- /dev/null
+++ b/modules-available/statistics/templates/projector-list.html
@@ -0,0 +1,21 @@
+<div class="panel panel-default">
+ <div class="panel-heading">{{lang_projectors}}</div>
+ <div class="panel-body">
+ <form method="post" action="?do=statistics" id="delprojector">
+ <input type="hidden" name="token" value="{{token}}">
+ <input type="hidden" name="action" value="delprojector">
+ <p>{{lang_thoseAreProjectors}}</p>
+ {{#projectors}}
+ <div>
+ <button type="submit" name="hwid" value="{{hwid}}" class="btn btn-danger">
+ <span class="glyphicon glyphicon-remove"></span>
+ </button>
+ {{hwname}}
+ </div>
+ {{/projectors}}
+ {{^projectors}}
+ <div class="alert alert-info">{{lang_noProjectorsDefined}}</div>
+ {{/projectors}}
+ </form>
+ </div>
+</div> \ No newline at end of file
diff --git a/modules-available/statistics_reporting/hooks/cron.inc.php b/modules-available/statistics_reporting/hooks/cron.inc.php
index a48f74c2..afb18a23 100644
--- a/modules-available/statistics_reporting/hooks/cron.inc.php
+++ b/modules-available/statistics_reporting/hooks/cron.inc.php
@@ -4,7 +4,7 @@ if (RemoteReport::isReportingEnabled()) {
$nextReporting = RemoteReport::getReportingTimestamp();
// It's time to generate a new report
- if ($nextReporting <= time()) {
+ while ($nextReporting <= time()) {
RemoteReport::writeNextReportingTimestamp();
$from = strtotime("-7 days", $nextReporting);
@@ -18,6 +18,9 @@ if (RemoteReport::isReportingEnabled()) {
if ($code != 200) {
EventLog::warning("Statistics Reporting failed: " . $code, $result);
+ } else {
+ EventLog::info('Statistics report sent to ' . CONFIG_REPORTING_URL);
}
+ $nextReporting = strtotime("+7 days", $nextReporting);
}
} \ No newline at end of file
diff --git a/modules-available/statistics_reporting/inc/getdata.inc.php b/modules-available/statistics_reporting/inc/getdata.inc.php
index f65ee868..da3a9a26 100644
--- a/modules-available/statistics_reporting/inc/getdata.inc.php
+++ b/modules-available/statistics_reporting/inc/getdata.inc.php
@@ -40,6 +40,7 @@ class GetData
$res = Queries::getLocationStatistics(self::$from, self::$to, self::$lowerTimeBound, self::$upperTimeBound);
$data = array();
while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ self::nullToZero($row);
$median = self::calcMedian(self::calcMedian($row['medianSessionLength']));
$entry = array(
'location' => ($anonymize ? $row['locHash'] : $row['locName']),
@@ -69,6 +70,7 @@ class GetData
$res = Queries::getClientStatistics(self::$from, self::$to, self::$lowerTimeBound, self::$upperTimeBound);
$data = array();
while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ self::nullToZero($row);
$median = self::calcMedian(self::calcMedian($row['medianSessionLength']));
$entry = array(
'hostname' => ($anonymize ? $row['clientHash'] : $row['clientName']),
@@ -116,12 +118,20 @@ class GetData
$data = array();
$vm = $anonymize ? 'vmHash' : 'name';
while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ self::nullToZero($row);
$data[] = array('vm' => $row[$vm], 'sessions' => $row['count']);
}
return $data;
}
-
+ private static function nullToZero(&$row)
+ {
+ foreach ($row as &$field) {
+ if (is_null($field)) {
+ $field = 0;
+ }
+ }
+ }
// Format $seconds into ".d .h .m .s" format (day, hour, minute, second)
private static function formatSeconds($seconds)
diff --git a/modules-available/statistics_reporting/inc/queries.inc.php b/modules-available/statistics_reporting/inc/queries.inc.php
index 3e944c92..2269e764 100644
--- a/modules-available/statistics_reporting/inc/queries.inc.php
+++ b/modules-available/statistics_reporting/inc/queries.inc.php
@@ -8,16 +8,16 @@ class Queries
public static function getClientStatistics($from, $to, $lowerTimeBound = 0, $upperTimeBound = 24, $excludeToday = false) {
$notassigned = Dictionary::translate('notAssigned', true);
Database::exec("SET SESSION group_concat_max_len = 1000000000");
- $res = Database::simpleQuery("SELECT name AS clientName, timeSum, medianSessionLength, offlineSum, IFNULL(lastStart, 0) as lastStart, IFNULL(lastLogout, 0) as lastLogout, longSessions, shortSessions, locId, locName, MD5(CONCAT(locId, :salt)) AS locHash, MD5(CONCAT(t1.uuid, :salt)) AS clientHash FROM (
+ $res = Database::simpleQuery("SELECT t2.name AS clientName, timeSum, medianSessionLength, offlineSum, IFNULL(lastStart, 0) as lastStart, IFNULL(lastLogout, 0) as lastLogout, longSessions, shortSessions, t2.locId, t2.locName, MD5(CONCAT(t2.locId, :salt)) AS locHash, MD5(CONCAT(t2.uuid, :salt)) AS clientHash FROM (
SELECT machine.machineuuid AS 'uuid', SUM(CAST(sessionTable.length AS UNSIGNED)) AS 'timeSum', GROUP_CONCAT(sessionTable.length) AS 'medianSessionLength', SUM(sessionTable.length >= 60) AS 'longSessions', SUM(sessionTable.length < 60) AS 'shortSessions', MAX(sessionTable.endInBound) AS 'lastLogout'
FROM ".self::getBoundedTableQueryString('~session-length', $from, $to, $lowerTimeBound, $upperTimeBound)." sessionTable
- INNER JOIN machine ON sessionTable.machineuuid = machine.machineuuid
+ RIGHT JOIN machine ON sessionTable.machineuuid = machine.machineuuid
GROUP BY machine.machineuuid
) t1
RIGHT JOIN (
- SELECT machine.hostname AS 'name', machine.machineuuid AS 'uuid', SUM(CAST(offlineTable.length AS UNSIGNED)) AS 'offlineSum', MAX(offlineTable.endInBound) AS 'lastStart', IFNULL(location.locationname, '$notassigned') AS 'locName', location.locationid AS 'locId'
+ SELECT IF(machine.hostname = '', machine.clientip, machine.hostname) AS 'name', machine.machineuuid AS 'uuid', SUM(CAST(offlineTable.length AS UNSIGNED)) AS 'offlineSum', MAX(offlineTable.endInBound) AS 'lastStart', IFNULL(location.locationname, '$notassigned') AS 'locName', location.locationid AS 'locId'
FROM ".self::getBoundedTableQueryString('~offline-length', $from, $to, $lowerTimeBound, $upperTimeBound)." offlineTable
- INNER JOIN machine ON offlineTable.machineuuid = machine.machineuuid
+ RIGHT JOIN machine ON offlineTable.machineuuid = machine.machineuuid
LEFT JOIN location ON machine.locationid = location.locationid
GROUP BY machine.machineuuid
) t2
@@ -30,17 +30,17 @@ class Queries
public static function getLocationStatistics($from, $to, $lowerTimeBound = 0, $upperTimeBound = 24, $excludeToday = false) {
$notassigned = Dictionary::translate('notAssigned', true);
Database::exec("SET SESSION group_concat_max_len = 1000000000");
- $res = Database::simpleQuery("SELECT t1.locId, locName AS locName, MD5(CONCAT(t1.locId, :salt)) AS locHash, timeSum, medianSessionLength, offlineSum, longSessions, shortSessions FROM (
+ $res = Database::simpleQuery("SELECT t2.locId, t2.locName, MD5(CONCAT(t2.locId, :salt)) AS locHash, timeSum, medianSessionLength, offlineSum, longSessions, shortSessions FROM (
SELECT location.locationid AS 'locId', SUM(CAST(sessionTable.length AS UNSIGNED)) AS 'timeSum', GROUP_CONCAT(sessionTable.length) AS 'medianSessionLength', SUM(sessionTable.length >= 60) AS 'longSessions', SUM(sessionTable.length < 60) AS 'shortSessions'
FROM ".self::getBoundedTableQueryString('~session-length', $from, $to, $lowerTimeBound, $upperTimeBound)." sessionTable
- INNER JOIN machine ON sessionTable.machineuuid = machine.machineuuid
+ RIGHT JOIN machine ON sessionTable.machineuuid = machine.machineuuid
LEFT JOIN location ON machine.locationid = location.locationid
GROUP BY machine.locationid
) t1
RIGHT JOIN (
SELECT IFNULL(location.locationname, '$notassigned') AS 'locName', location.locationid AS 'locId', SUM(CAST(offlineTable.length AS UNSIGNED)) AS 'offlineSum'
FROM ".self::getBoundedTableQueryString('~offline-length', $from, $to, $lowerTimeBound, $upperTimeBound)." offlineTable
- INNER JOIN machine ON offlineTable.machineuuid = machine.machineuuid
+ RIGHT JOIN machine ON offlineTable.machineuuid = machine.machineuuid
LEFT JOIN location ON machine.locationid = location.locationid
GROUP BY machine.locationid
) t2
diff --git a/modules-available/statistics_reporting/inc/remotereport.inc.php b/modules-available/statistics_reporting/inc/remotereport.inc.php
index 7aad8b3a..4c5f604b 100644
--- a/modules-available/statistics_reporting/inc/remotereport.inc.php
+++ b/modules-available/statistics_reporting/inc/remotereport.inc.php
@@ -40,7 +40,7 @@ class RemoteReport
if ($ts === 0) {
// No timestamp stored yet - might be a fresh install
// schedule for next time
- self::updateNextReportingTimestamp();
+ self::writeNextReportingTimestamp();
$ts = Property::get(self::NEXT_SUBMIT_ID, 0);
} elseif ($ts < strtotime('last monday')) {
// Too long ago, move forward to last monday
@@ -63,18 +63,16 @@ class RemoteReport
* Generate the multi-dimensional array containing the anonymized
* (weekly) statistics to report.
*
- * @param $from start timestamp
- * @param $to end timestamp
+ * @param int $from start timestamp
+ * @param int $to end timestamp
* @return array wrapped up statistics, ready for reporting
*/
public static function generateReport($from, $to) {
GetData::$from = $from;
GetData::$to = $to;
- GetData::$salt = bin2hex(Util::randomBytes(20));
+ GetData::$salt = bin2hex(Util::randomBytes(20, false));
$data = GetData::total(GETDATA_ANONYMOUS);
$data['perLocation'] = GetData::perLocation(GETDATA_ANONYMOUS);
- $data['perClient'] = GetData::perClient(GETDATA_ANONYMOUS);
- $data['perUser'] = GetData::perUser(GETDATA_ANONYMOUS);
$data['perVM'] = GetData::perVM(GETDATA_ANONYMOUS);
$data['tsFrom'] = $from;
$data['tsTo'] = $to;
diff --git a/modules-available/statistics_reporting/templates/columnChooser.html b/modules-available/statistics_reporting/templates/columnChooser.html
index f08daf1c..a5ac828b 100644
--- a/modules-available/statistics_reporting/templates/columnChooser.html
+++ b/modules-available/statistics_reporting/templates/columnChooser.html
@@ -150,6 +150,8 @@
updateColumn(box);
}
});
+
+ $('th[data-sort]').first().click();
});
function updateColumn(checkbox) {
diff --git a/modules-available/sysconfig/addmodule_branding.inc.php b/modules-available/sysconfig/addmodule_branding.inc.php
index 84602614..c2f9e690 100644
--- a/modules-available/sysconfig/addmodule_branding.inc.php
+++ b/modules-available/sysconfig/addmodule_branding.inc.php
@@ -9,7 +9,7 @@ class Branding_Start extends AddModule_Base
protected function renderInternal()
{
- Render::addDialog(Dictionary::translate('config-module', 'branding_title'), false, 'branding-start', array(
+ Render::addDialog(Dictionary::translateFile('config-module', 'branding_title'), false, 'branding-start', array(
'step' => 'Branding_ProcessFile',
'edit' => $this->edit ? $this->edit->id() : false
));
diff --git a/modules-available/sysconfig/addmodule_custommodule.inc.php b/modules-available/sysconfig/addmodule_custommodule.inc.php
index 7c3ccf0f..8c24a071 100644
--- a/modules-available/sysconfig/addmodule_custommodule.inc.php
+++ b/modules-available/sysconfig/addmodule_custommodule.inc.php
@@ -12,7 +12,7 @@ class CustomModule_Start extends AddModule_Base
protected function renderInternal()
{
Session::set('mod_temp', false);
- Render::addDialog(Dictionary::translate('config-module', 'custom_title'), false, 'custom-upload', array(
+ Render::addDialog(Dictionary::translateFile('config-module', 'custom_title'), false, 'custom-upload', array(
'step' => 'CustomModule_ProcessUpload',
'edit' => $this->edit ? $this->edit->id() : false
));
diff --git a/modules-available/sysconfig/addmodule_sshconfig.inc.php b/modules-available/sysconfig/addmodule_sshconfig.inc.php
index 19272c32..ec01f878 100644
--- a/modules-available/sysconfig/addmodule_sshconfig.inc.php
+++ b/modules-available/sysconfig/addmodule_sshconfig.inc.php
@@ -18,7 +18,7 @@ class SshConfig_Start extends AddModule_Base
} else {
$data = array();
}
- Render::addDialog(Dictionary::translate('lang_clientSshConfig'), false, 'sshconfig-start', $data + array(
+ Render::addDialog(Dictionary::translateFile('config-module', 'sshconfig_title'), false, 'sshconfig-start', $data + array(
'step' => 'SshConfig_Finish',
));
}
diff --git a/modules-available/sysconfig/inc/ppd.inc.php b/modules-available/sysconfig/inc/ppd.inc.php
new file mode 100644
index 00000000..aa2e0e5a
--- /dev/null
+++ b/modules-available/sysconfig/inc/ppd.inc.php
@@ -0,0 +1,1162 @@
+<?php
+
+/**
+ * Class Ppd for parsing PPD files. This class was developed around
+ * the PPD spec v4.3. All comments in this class referring to sections of
+ * the spec will refer to this version, if not stated otherwise.
+ */
+class Ppd
+{
+
+ const FILE = 0;
+ const STRING = 1;
+
+ const INCLUDE_UNKNOWN_MAIN_KEYWORDS = 1;
+
+ /**
+ * regexp matching valid PPD keywords ASCII 33-126, excluding colon and slash.
+ * See section 3.2/3.3
+ */
+ const EXP_KEYWORD = '[\x21-\x2e\x30-\x39\x3b-\x7e]+';
+
+ const PPD_INT = '\-?\d+';
+
+ const PPD_REAL = '\-?\d+(\.\d+)?';
+
+ const PPD_BOOL = 'True|False';
+
+ const PPD_RECTANGLE = '\-?\d+(\.\d+)?\s+\-?\d+(\.\d+)?\s+\-?\d+(\.\d+)?\s+\-?\d+(\.\d+)?';
+
+ const PPD_DIMENSION = '\-?\d+(\.\d+)?\s+\-?\d+(\.\d+)?';
+
+ private $REQUIRED_KEYWORDS = array(
+ 'PPD-Adobe' => '4\.[0123]',
+ 'FileVersion' => '.*',
+ 'FormatVersion' => '.*',
+ 'LanguageEncoding' => '.*',
+ 'LanguageVersion' => '.*',
+ 'Manufacturer' => '.*',
+ 'ModelName' => '.*',
+ 'NickName' => '.*',
+ 'PCFileName' => '.*',
+ 'Product' => '\(.*\)',
+ 'PSVersion' => '\(.*\)\s+\d+',
+ 'ShortNickName' => '.*'
+ );
+
+ private $LANGUAGE_MAPPINGS = array(
+ 'English' => 'ISOLatin1',
+ 'Chinese' => 'None',
+ 'Danish' => 'ISOLatin1',
+ 'Dutch' => 'ISOLatin1',
+ 'Finnish' => 'ISOLatin1',
+ 'French' => 'ISOLatin1',
+ 'German' => 'ISOLatin1',
+ 'Italian' => 'ISOLatin1',
+ 'Japanese' => 'JIS83-RKSJ',
+ 'Norwegian' => 'ISOLatin1',
+ 'Portuguese' => 'ISOLatin1',
+ 'Russian' => 'None',
+ 'Spanish' => 'ISOLatin1',
+ 'Swedish' => 'ISOLatin1',
+ 'Turkish' => 'None'
+ );
+
+ private $ENCODINGS = array(
+ 'ISOLatin1' => 'ISO-8859-1',
+ 'ISOLatin2' => 'ISO-8859-2',
+ 'ISOLatin5' => 'ISO-8859-5',
+ 'JIS83-RKSJ' => 'SJIS',
+ 'MacStandard' => 'MACINTOSH',
+ 'WindowsANSI' => 'Windows-1252'
+ );
+
+ /**
+ * @var string name of source charset (PPD)
+ */
+ private $sourceEncoding;
+ /**
+ * @var string 'mb' or 'iconv'
+ */
+ private $encoder;
+
+
+ /**
+ * List of known main keywords.
+ * Key is the keyword, value is either a regex for the value, if we don't care about the option format,
+ * or an array with [0] = regex for option keyword, and [1] = regex for value
+ *
+ * @var array
+ */
+ private $KNOWN_KEYWORDS = array(
+ /*
+ * Basic Device Capabilities, section 5.5
+ */
+ 'ColorDevice' => self::PPD_BOOL,
+ 'DefaultColorSpace' => 'CMY|CMYK|RGB|Gray',
+ 'Extensions' => '(DPS|CMYK|Composite|FileSystem)(\s+(DPS|CMYK|Composite|FileSystem))*',
+ 'FaxSupport' => 'Base',
+ 'FileSystem' => self::PPD_BOOL,
+ 'LanguageLevel' => self::PPD_INT,
+ 'Throughput' => '\d+(\.\d+)?',
+ 'TTRasterizer' => 'None|Accept68K|Type42|TrueImage',
+ '1284Modes' => 'Compat|Nibble|Byte|ECP|EPP',
+ '1284DeviceID' => '.*',
+ /*
+ * System Management, section 5.6
+ */
+ 'PatchFile' => '.*',
+ 'JobPatchFile' => array(self::PPD_INT, '.*'),
+ 'FreeVM' => self::PPD_INT,
+ 'VMOption' => self::PPD_INT,
+ 'InstalledMemory' => '.*',
+ 'DefaultInstalledMemory' => '.*',
+ 'Reset' => '.*',
+ 'Password' => '.*',
+ 'ExitJamRecovery' => array(self::PPD_BOOL, '.*'),
+ 'DefaultExitJamRecovery' => 'True|False|Unknown',
+ 'ExitServer' => '.*',
+ 'SuggestedJobTimeout' => self::PPD_INT,
+ 'SuggestedManualFeedTimeout' => self::PPD_INT, // XXX: Typo in spec? It says "SuggestedManualfFeedTimeout"
+ 'SuggestedWaitTimeout' => self::PPD_INT,
+ 'PrintPSErrors' => self::PPD_BOOL,
+ 'DeviceAdjustMatrix' => '\[[\d\s]+\]',
+ /*
+ * Emulations and Protocols, section 5.7
+ */
+ 'Protocols' => '(BCP|PJL|TBCP)(\s+(BCP|PJL|TBCP))*',
+ 'Emulators' => '\S+(\s+\S+)*', // TODO This requires matching *(Start|Stop)Emulator_(\S+): "code" main keywords
+ /*
+ * JCL, section 5.8
+ */
+ 'JCLBegin' => '.*',
+ 'JCLToPSInterpreter' => '.*',
+ 'JCLEnd' => '.*',
+ // TODO: The above three need to be either completely absent, or all three must be defined
+ /*
+ * Resolution and Appearence Control, section 5.9
+ */
+ /*
+ * Gray Levels and Halftoning, section 5.10
+ */
+ 'AccurateScreensSupport' => self::PPD_BOOL,
+ 'ContoneOnly' => self::PPD_BOOL,
+ 'DefaultHalftoneType' => self::PPD_INT,
+ 'ScreenAngle' => self::PPD_REAL,
+ 'ScreenFreq' => self::PPD_REAL,
+ 'ResScreenFreq' => self::PPD_REAL,
+ 'ResScreenAngle' => self::PPD_REAL,
+ 'DefaultScreenProc' => 'Dot|Line|Ellipse|Cross|Mezzo|DiamondDot',
+ 'ScreenProc' => array('Dot|Line|Ellipse|Cross|Mezzo|DiamondDot', '.*'),
+ 'DefaultTransfer' => 'Null|Factory', // XXX: Spec seems to allow only these two values as default, but why
+ 'Transfer' => array('Null|Factory|Normalized|Red|Green|Blue', '.*'),
+ /*
+ * Color Adjustment, section 5.11
+ */
+ 'BlackSubstitution' => array(self::PPD_BOOL, '.*'),
+ 'DefaultBlackSubstitution' => 'True|False|Unknown',
+ 'ColorModel' => array('CMY|CMYK|RGB|Gray', '.*'),
+ 'DefaultColorModel' => 'CMY|CMYK|RGB|Gray|Unknown',
+ 'RenderingIntent' => '.*',
+ 'PageDeviceName' => '.*',
+ 'HalftoneName' => '.*',
+ /*
+ * Media Selection, section 5.14
+ */
+ 'ManualFeed' => array(self::PPD_BOOL, '.*'),
+ 'DefaultManualFeed' => 'True|False|Unknown',
+ /*
+ * Information About Media Sizes, section 5.15
+ */
+ 'ImageableArea' => self::PPD_RECTANGLE,
+ 'PaperDimension' => self::PPD_DIMENSION,
+ 'RequiresPageRegion' => self::PPD_BOOL,
+ 'LandscapeOrientation' => 'Plus90|Minus90|Any',
+ /*
+ * Custom Page Size, section 5.16
+ */
+ 'CustomPageSize' => array('True', '.*'),
+ 'ParamCustomPageSize' => array('Width|Height|WidthOffset|HeightOffset|Orientation', '\d+\s+(int|real|points)\s+' . self::PPD_REAL . '\s+' . self::PPD_REAL),
+ 'MaxMediaWidth' => self::PPD_REAL,
+ 'MaxMediaHeight' => self::PPD_REAL,
+ 'CenterRegistered' => self::PPD_BOOL,
+ 'LeadingEdge' => array('Short|Long|PreferLong|Forced|Unknown', '\s*'),
+ 'DefaultLeadingEdge' => 'Short|Long|PreferLong|Forced|Unknown',
+ 'HWMargins' => self::PPD_RECTANGLE,
+ 'UseHWMargins' => array(self::PPD_BOOL, '\s*'),
+ 'DefaultUseHWMargins' => self::PPD_BOOL,
+ /*
+ * Media Handling Features, section 5.17
+ */
+ 'OutputOrder' => array('Normal|Reverse', '.*'),
+ 'DefaultOutputOrder' => 'Normal|Reverse|Unknown',
+ 'PageStackOrder' => 'Normal|Reverse',
+ 'TraySwitch' => array(self::PPD_BOOL, '.*'),
+ 'DefaultTraySwitch' => 'True|False|Unknown',
+ 'Duplex' => array('DuplexTumble|DuplexNoTumble|SimplexTumble|None|False|SimplexNoTumble', '.*'),
+ 'DefaultDuplex' => 'DuplexTumble|DuplexNoTumble|SimplexTumble|None|False|SimplexNoTumble',
+ /*
+ * Finishing Features, section 5.18ff
+ * TODO
+ */
+
+ /*
+ * Font Related Keywords, section 5.20
+ */
+ 'FDirSize' => self::PPD_INT,
+ 'FCacheSize' => self::PPD_INT,
+ // TODO: 'Font' = >
+ /*
+ * Printer Messages, section 5.21
+ */
+ 'PrinterError' => '.*',
+ 'Status' => '.*',
+ 'Source' => '.*',
+ 'Message' => '.*',
+ /*
+ * 5.22
+ */
+ 'InkName' => '.+',
+ );
+
+ /**
+ * Appendix A.1: UI Keywords.
+ * SORTED, so we can do a binary search.
+ *
+ * @var array list of UI keywords.
+ */
+ private $UI_KEYWORDS = array('AdvanceMedia', 'BindColor', 'BindEdge', 'BindType', 'BindWhen', 'BitsPerPixel',
+ 'BlackSubstitution', 'Booklet', 'Collate', 'ColorModel', 'CutMedia', 'Duplex', 'ExitJamRecovery', 'FoldType',
+ 'FoldWhen', 'InputSlot', 'InstalledMemory', 'Jog', 'ManualFeed', 'MediaColor', 'MediaType', 'MediaWeight',
+ 'MirrorPrint', 'NegativePrint', 'OutputBin', 'OutputMode', 'OutputOrder', 'PageSize', 'PageRegion', 'Separations',
+ 'Signature', 'Slipsheet', 'Smoothing', 'Sorter', 'StapleLocation', 'StapleOrientation', 'StapleWhen', 'StapleX',
+ 'StapleY', 'TraySwitch'
+ );
+
+ /**
+ * Appendix A.2: Repeated Keywords.
+ * SORTED, so we can do a binary search.
+ *
+ * @var array list of repeated keywords
+ */
+ private $REPEATED_KEYWORDS = array('HalftoneName', 'Include', 'InkName', 'Message', 'NonUIConstraints', 'NonUIOrderDependency',
+ 'OrderDependency', 'PageDeviceName', 'PrinterError', 'Product', 'PSVersion', 'QueryOrderDependency',
+ 'RenderingIntent', 'Source', 'Status', 'UIConstraints'
+ );
+
+ private $data;
+ private $dataLen;
+
+ private $error;
+ private $warnings;
+
+ private $knownKeywordMalformed;
+
+ /**
+ * @var PpdSettingInternal[] known options of this ppd
+ */
+ private $settings;
+
+ private $requiredKeywords;
+
+ function __construct($ppd, $type = self::FILE, $flags = 0)
+ {
+ if (empty($ppd)) {
+ $this->error = 'Empty $ppd';
+ return;
+ }
+ if ($type == self::FILE) {
+ $this->data = file_get_contents($ppd);
+ if ($this->data === false) {
+ $this->error = 'Could not open ' . substr($ppd, 1);
+ return;
+ }
+ } elseif ($type == self::STRING) {
+ $this->data = $ppd;
+ } else {
+ $this->error = 'Invalid $type passed';
+ return;
+ }
+ $this->parse();
+ }
+
+ private function parse()
+ {
+ $r = substr_count($this->data, "\r");
+ $n = substr_count($this->data, "\n");
+ if ($r > 10 && abs($r - $n) < $r / 10) {
+ if (substr($this->data, -2) !== "\r\n") {
+ $this->data .= "\r\n";
+ }
+ } elseif ($r > $n) {
+ if (substr($this->data, -1) !== "\r") {
+ $this->data .= "\r";
+ }
+ } else {
+ if (substr($this->data, -1) !== "\n") {
+ $this->data .= "\n";
+ }
+ }
+
+ $this->dataLen = strlen($this->data);
+ $this->encoder = false;
+ $this->sourceEncoding = false;
+ $this->error = false;
+ $this->warnings = array();
+ $this->knownKeywordMalformed = false;
+ $this->settings = array();
+ $this->requiredKeywords = array();
+
+ // Parse
+ /* @var $rawOption \PpdOption */
+ /* @var $currentBlock \PpdBlockInternal */
+ $currentBlock = false;
+ $inRawBlock = false; // True if in a multi-line InvocationValue or QuotedValue (3.6: Parsing Summary for Values)
+ $wantsEnd = false;
+ // For now we ignore values mostly while parsing. The spec says that InvocationValues must only contain printable
+ // ASCII characters, so we should issue a warning if we encounter invalid chars in them.
+ $lStart = -1;
+ $lEnd = -1;
+ $no = 0;
+ while ($lStart < $this->dataLen && $lEnd !== false) {
+ unset($mainKeyword, $optionKeyword, $optionTranslation, $option, $value, $valueTranslation);
+ if ($no !== 0 && $this->data{$lEnd} === "\r" && $this->data{$lEnd + 1} === "\n") {
+ $lEnd++;
+ }
+ if ($no === 1) {
+ // The first line must be *PPD-Adobe, check if that was the case
+ if (!isset($this->requiredKeywords['PPD-Adobe'])) {
+ $this->error = 'First line does not contain *PPD-Adobe main keyword';
+ return;
+ }
+ }
+ $lStart = $lEnd + 1;
+ $lEnd = $this->nextLineEnd($lStart);
+ $no++;
+ // Validate
+ $len = $lEnd - $lStart;
+ $line = substr($this->data, $lStart, $len);
+ if ($len === 0) {
+ continue;
+ }
+ if ($len > 255) {
+ $this->warn($no, 'Exceeds length of 255');
+ }
+ if (!$inRawBlock && preg_match_all('/[^\x09\x0A\x0D\x20-\xFF]/', $line, $out)) {
+ $chars = $this->escapeBinaryArray($out[0]);
+ $this->warn($no, 'Contains invalid character(s) ' . $chars);
+ }
+ // Handle
+ // 1) We're inside an InvocationValue or QuotedValue, need a single " at line end to close it
+ if ($inRawBlock) {
+ if (substr($line, -1) === '"') {
+ $inRawBlock = false;
+ $wantsEnd = true;
+ if (isset($rawOption)) {
+ $rawOption->lineLen = $lEnd - $rawOption->lineOffset;
+ }
+ }
+ continue;
+ }
+ // 2) InvocationValue or QuotedValue just closed, an '*End' has to follow
+ if ($wantsEnd) {
+ $wantsEnd = false;
+ if ($line !== '*End' && $line !== '*SymbolEnd') { // XXX: We don't properly check which one we expected...
+ $this->warn($no, 'End of multi-line InvocationValue or QuotedValue not followed by "*(Symbol)End"');
+ unset($rawOption);
+ } else {
+ if (isset($rawOption)) {
+ $rawOption->lineLen = $lEnd - $rawOption->lineOffset;
+ }
+ unset($rawOption);
+ continue;
+ }
+ }
+ // 3) Handle "key [option]: value"
+ if ($line{0} === '*') {
+ if ($line{1} === '%') {
+ // Skip comment
+ continue;
+ }
+ $parts = preg_split('/\s*:\s*/', $line, 2); // TODO: UIConstrains
+ if (count($parts) !== 2) {
+ $this->warn($no, 'No colon found; not in "key [option]: value" format, ignoring line');
+ continue;
+ }
+ // Now $parts[0] is "key[ option]" and $parts[1] is "value"
+ // 3a) Determine key and option
+ if (1 > preg_match(',^\*(' . self::EXP_KEYWORD . ')($|\s+([^/]+)(/.*)?$),', $parts[0], $out)) {
+ $this->warn($no, 'Not a valid Main Keyword, "' . $parts[0] . '", line ignored');
+ continue;
+ }
+ $mainKeyword = $out[1];
+ $optionKeyword = isset($out[3]) ? $out[3] : false;
+ $optionTranslation = isset($out[4]) ? $this->unhexTranslation($no, substr($out[4], 1)) : $optionKeyword; // If no translation given, fallback to option
+ // 3b) Handle value
+ $value = $parts[1];
+ if ($value{0} === '"') {
+ // Start of InvocationValue or QuotedValue
+ if (preg_match(',^"([^"]*)"(/.*)?$,', $value, $vMatch)) {
+ // Single line
+ $value = $vMatch[1];
+ $valueTranslation = isset($vMatch[2]) ? $this->unhexTranslation($no, substr($vMatch[2], 1)) : $value;
+ } else {
+ // Multi-line
+ $inRawBlock = true;
+ $value = '<TODO: Multiline>'; // TODO: Handle multi-line values properly
+ $valueTranslation = '';
+ }
+ } elseif (preg_match(',^\^' . self::EXP_KEYWORD . '$,', $value)) {
+ // SymbolValue TODO: Can be followed by translation?
+ $valueTranslation = $value;
+ } elseif (preg_match(',^([^"][^/]*)(/.*)?$,', $value, $vMatch)) {
+ // StringValue
+ $value = $vMatch[1];
+ $valueTranslation = isset($vMatch[2]) ? $this->unhexTranslation($no, substr($vMatch[2], 1)) : $value;
+ }
+ // Key-value-pair parsed, now the fun part
+ // Special cases for openening closing certain groups
+ if ($mainKeyword === 'OpenGroup') {
+ if ($currentBlock !== false) {
+ $this->error = 'Line ' . $no . ': OpenGroup while other block (type=' . $currentBlock->type
+ . ', id=' . $currentBlock->id . ') was not closed yet';
+ return;
+ }
+ // TODO: Check unique
+ $nb = new PpdBlockInternal($value, $valueTranslation, 'Group', $currentBlock, $lStart);
+ if ($currentBlock !== false) {
+ $currentBlock->childBlocks[] = $nb;
+ }
+ $currentBlock = $nb;
+ continue;
+ } elseif ($mainKeyword === 'OpenSubGroup') {
+ if ($currentBlock === false || $currentBlock->type !== 'Group') {
+ $this->error = 'Line ' . $no . ': OpenSubGroup with no preceeding OpenGroup';
+ return;
+ }
+ // TODO: Check unique
+ $nb = new PpdBlockInternal($value, $valueTranslation, 'SubGroup', $currentBlock, $lStart);
+ if ($currentBlock !== false) {
+ $currentBlock->childBlocks[] = $nb;
+ }
+ $currentBlock = $nb;
+ continue;
+ } elseif ($mainKeyword === 'OpenUI' || $mainKeyword === 'JCLOpenUI') {
+ $type = $mainKeyword;
+ if (substr($type, 0, 3) === 'JCL') {
+ $type = 'JCL' . substr($type, 7);
+ } else {
+ $type = substr($type, 4);
+ }
+ if ($currentBlock !== false && $currentBlock->isUi()) {
+ $this->error = 'Line ' . $no . ': ' . $mainKeyword . ' while previous ' . $type . ' "'
+ . $currentBlock->id . '" was not closed yet';
+ return;
+ }
+ if ($optionKeyword === false) {
+ $this->error = 'Line ' . $no . ': ' . $mainKeyword . ' with no option keyword';
+ return;
+ }
+ if ($optionKeyword{0} !== '*') {
+ $this->error = 'Line ' . $no . ': ' . $mainKeyword . " with option keyword that doesn't start with asterisk (*).";
+ return;
+ }
+ // TODO: Check unique
+ $nb = new PpdBlockInternal($optionKeyword, $optionTranslation, $type, $currentBlock, $lStart);
+ $nb->value = $value;
+ if ($currentBlock !== false) {
+ $currentBlock->childBlocks[] = $nb;
+ }
+ $currentBlock = $nb;
+ $this->getOption(substr($optionKeyword, 1), $currentBlock); // ->type = $value; unused?
+ continue;
+ } elseif ($mainKeyword === 'CloseGroup' || $mainKeyword === 'CloseSubGroup' || $mainKeyword === 'CloseUI'
+ || $mainKeyword === 'JCLCloseUI'
+ ) {
+ $type = $mainKeyword;
+ if (substr($type, 0, 3) === 'JCL') {
+ $type = 'JCL' . substr($type, 8);
+ } else {
+ $type = substr($type, 5);
+ }
+ if ($currentBlock === false) {
+ $this->error = 'Line ' . $no . ': ' . $mainKeyword . ' with no Open' . $type;
+ return;
+ }
+ if ($currentBlock->type !== $type) {
+ $this->error = 'Line ' . $no . ': ' . $mainKeyword . ' after Open' . $currentBlock->type;
+ return;
+ }
+ if ($currentBlock->id !== $value) {
+ $this->error = 'Line ' . $no . ': ' . $mainKeyword . ' for "' . $value . '" while currently open '
+ . $type . ' is "' . $currentBlock->id . '"';
+ return;
+ }
+ $currentBlock->end = $lEnd;
+ $currentBlock = $currentBlock->parent;
+ continue;
+ } elseif ($mainKeyword === 'OrderDependency') {
+ if ($currentBlock === false || $currentBlock->isUi()) {
+ $this->warn($no, 'OrderDependency outside OpenUI/CloseUI block');
+ }
+ continue;
+ } elseif ($mainKeyword === 'Include') {
+ $this->warn($no, 'PPD tries to include a file (' . $value
+ . '), which is not supported. Will continue, but errors might occur');
+ continue;
+ } elseif ($mainKeyword === 'UIConstraints' || $mainKeyword === 'NonUIConstraints'
+ || $mainKeyword === 'SymbolLength' || $mainKeyword === 'SymbolValue'
+ || $mainKeyword === 'SymbolEnd' || $mainKeyword === 'NonUIOrderDependency'
+ ) {
+ continue;
+ }
+ // General information keywords, which are required
+ if (isset($this->REQUIRED_KEYWORDS[$mainKeyword])) {
+ if (isset($this->requiredKeywords[$mainKeyword])) {
+ if ($this->binary_in_array($mainKeyword, $this->REPEATED_KEYWORDS)) {
+ $this->requiredKeywords[$mainKeyword][] = $value;
+ } else {
+ $this->warn($no, 'Required keyword ' . $mainKeyword . ' declared twice, ignoring');
+ continue;
+ }
+ }
+ $this->requiredKeywords[$mainKeyword] = array($value);
+ if (($err = $this->validateLine($this->REQUIRED_KEYWORDS[$mainKeyword], $optionKeyword, $value)) !== true) {
+ $this->warn($no, 'Required main keyword ' . $mainKeyword . ': ' . $err);
+ $this->knownKeywordMalformed = true;
+ }
+ continue;
+ }
+ // Other well known keywords
+ if (isset($this->KNOWN_KEYWORDS[$mainKeyword])) {
+ if (($err = $this->validateLine($this->KNOWN_KEYWORDS[$mainKeyword], $optionKeyword, $value)) !== true) {
+ $this->warn($no, 'Known main keyword ' . $mainKeyword . ': ' . $err);
+ $this->knownKeywordMalformed = true;
+ }
+ }
+ if (substr($mainKeyword, 0, 7) === 'Default') {
+ // Default keyword
+ $option = $this->getOption(substr($mainKeyword, 7), $currentBlock);
+ $option->default = new PpdOption($lStart, $len, $value, $valueTranslation);
+ continue;
+ } elseif (substr($mainKeyword, 0, 17) === 'FoomaticRIPOption') {
+ if ($optionKeyword === false) {
+ $this->warn($no, "$mainKeyword with no option keyword");
+ } elseif ($currentBlock !== false && isset($this->settings[$optionKeyword])) {
+ $option = $this->getOption($optionKeyword, $currentBlock);
+ $option->foomatic[substr($mainKeyword, 11)] = new PpdOption($lStart, $len, $value, $valueTranslation);
+ } else {
+ $this->warn($no, 'TODO: ' . $line);
+ }
+ } elseif (substr($mainKeyword, 0, 6) === 'Custom') {
+ if ($optionKeyword === false) {
+ $this->warn($no, "$mainKeyword with no option keyword");
+ } elseif ($optionKeyword !== 'True') {
+ $this->warn($no, "$mainKeyword with option keyword other than 'True'; ignored");
+ } else {
+ $option = $this->getOption(substr($mainKeyword, 6), $currentBlock);
+ $option->custom = new PpdOption($lStart, $len, $value, $valueTranslation);
+ }
+ } elseif (substr($mainKeyword, 0, 11) === 'ParamCustom') {
+ if ($optionKeyword === false) {
+ $this->warn($no, "$mainKeyword with no option keyword");
+ } elseif (substr($mainKeyword, 11) !== $optionKeyword) {
+ $this->warn($no, "Don't know how to handle $mainKeyword with option keyword $optionKeyword "
+ . "(expected '*ParamCustomSomething Something: <format>'");
+ } else {
+ $option = $this->getOption($optionKeyword, $currentBlock);
+ $option->customParam = new PpdOption($lStart, $len, $value, $valueTranslation);
+ }
+ } elseif ($mainKeyword{0} === '?') {
+ // Ignoring option query for now
+ } elseif ($optionKeyword === false && !isset($this->KNOWN_KEYWORDS[$mainKeyword])) {
+ // Must be a definition for an option
+ $this->warn($no, "Don't know how to handle line with main keyword '$mainKeyword', no option keyword found.");
+ } else {
+ // Some option for some option ;)
+ if ($optionKeyword === false) {
+ // We know that this is a known main keyword otherwise we would have hit the previous elseif block
+ $optionKeyword = $value;
+ $optionTranslation = $valueTranslation;
+ }
+ $option = $this->getOption($mainKeyword, $currentBlock);
+ $optionInstance = new PpdOption($lStart, $len, $optionKeyword, $optionTranslation);
+ if ($this->binary_in_array($mainKeyword, $this->REPEATED_KEYWORDS)) {
+ // This can occur multiple times, just pile them up
+ $option->values[] = $optionInstance;
+ } else {
+ $key = "k$optionKeyword";
+ if (isset($option->values[$key])) {
+ $this->warn($no, "Ignoring re-definition of option '$optionKeyword' for Main Keyword '$mainKeyword'");
+ } else {
+ $option->values[$key] = $optionInstance;
+ }
+ }
+ if ($inRawBlock) {
+ $optionInstance->multiLine = true;
+ $rawOption = $optionInstance;
+ }
+ unset($optionInstance);
+ }
+ } elseif (strlen(trim($line)) !== 0) {
+ $this->warn($no, 'Invalid format; not empty and not starting with asterisk (*)');
+ }
+ }
+ //
+ if ($currentBlock !== false) {
+ $this->error = 'Block ' . $currentBlock->id . ' (' . $currentBlock->type . ') was never closed.';
+ return;
+ }
+ foreach ($this->REQUIRED_KEYWORDS as $kw => $regex) {
+ if (!isset($this->requiredKeywords[$kw])) {
+ $this->warn(0, "Required keyword '$kw' missing from file.'");
+ $this->error = 'One or more required keywords missing';
+ }
+ }
+ if ($this->error !== false) {
+ return;
+ }
+ // All required keywords exist
+ if (preg_match('/utf\-?8/i', $this->requiredKeywords['LanguageEncoding'][0])) {
+ $this->sourceEncoding = false; // Would be a NOOP
+ } elseif (isset($this->ENCODINGS[$this->requiredKeywords['LanguageEncoding'][0]])) {
+ $this->sourceEncoding = $this->ENCODINGS[$this->requiredKeywords['LanguageEncoding'][0]];
+ } else if (isset($this->LANGUAGE_MAPPINGS[$this->requiredKeywords['LanguageVersion'][0]])) {
+ $this->sourceEncoding = $this->ENCODINGS[$this->LANGUAGE_MAPPINGS[$this->requiredKeywords['LanguageVersion'][0]]];
+ } elseif (!empty($this->requiredKeywords['LanguageEncoding'][0])) {
+ $this->sourceEncoding = $this->requiredKeywords['LanguageEncoding'][0];
+ }
+ if ($this->sourceEncoding !== false) {
+ if (is_callable('iconv')) {
+ $encoding = strtoupper($this->sourceEncoding);
+ if (@iconv($encoding, 'UTF-8//TRANSLIT', 'test') === 'test') {
+ $this->encoder = function ($string, $reverse = false) use ($encoding) {
+ if ($reverse) {
+ $retval = iconv('UTF-8', $encoding . '//TRANSLIT', $string);
+ } else {
+ $retval = iconv($encoding, 'UTF-8//TRANSLIT', $string);
+ }
+ if ($retval === false)
+ return $string;
+ return $retval;
+ };
+ }
+ }
+ if ($this->encoder === false && is_callable('mb_list_encodings')) {
+ $encodings = mb_list_encodings();
+ foreach ($encodings as $encoding) {
+ if (strtolower($encoding) === $this->sourceEncoding) {
+ $this->sourceEncoding = $encoding;
+ $this->encoder = function ($string, $reverse = false) use ($encoding) {
+ if ($reverse) {
+ $retval = mb_convert_encoding($string, $encoding, 'UTF-8');
+ } else {
+ $retval = mb_convert_encoding($string, 'UTF-8', $encoding);
+ }
+ if ($retval === false)
+ return $string;
+ return $retval;
+ };
+ break;
+ }
+ }
+ }
+ }
+ if ($this->encoder === false) {
+ $this->encoder = function ($foo, $reverse = false) { return $foo; };
+ }
+ }
+
+ private function nextLineEnd($start)
+ {
+ if ($start >= $this->dataLen)
+ return false;
+ while ($start < $this->dataLen) {
+ $char = $this->data{$start};
+ if ($char === "\r" || $char === "\n")
+ return $start;
+ ++$start;
+ }
+ return $this->dataLen;
+ }
+
+ private function warn($lineNo, $message)
+ {
+ $line = 'Line ' . $lineNo . ': ' . $message;
+ $this->warnings[] = $line;
+ }
+
+ private function escapeBinaryArray($array)
+ {
+ $chars = array_reduce(array_unique($array), function ($carry, $item) {
+ return $carry . '\x' . dechex(ord($item));
+ }, '');
+ }
+
+ private function unhexTranslation($lineNo, $translation)
+ {
+ if (strpos($translation, '<') === false)
+ return $translation;
+ return preg_replace_callback('/<[^>]*>/', function ($match) use ($lineNo) {
+ if (preg_match_all('/[^a-fA-F0-9\<\>\s]/', $match[0], $out)) {
+ $this->warn($lineNo, 'Invalid character(s) in hex substring: ' . $this->escapeBinaryArray($out[0]));
+ }
+ $string = preg_replace('/[^a-fA-F0-9]/', '', $match[0]);
+ if (strlen($string) % 2 !== 0) {
+ $this->warn('Odd number of hex digits in hex substring');
+ $string = substr($string, 0, -1);
+ }
+ return pack('H*', $string);
+ }, $translation);
+ }
+
+ private function hexTranslation($translation)
+ {
+ return preg_replace_callback('/[\x00-\x1f\x7b-\xff\:\<\>]+/', function ($match) {
+ return '<' . unpack('H*', $match[0])[1] . '>';
+ }, $translation);
+ }
+
+ /**
+ * Get option object
+ *
+ * @param string $name option name
+ * @param \PpdBlockInternal $block which block this option is defined in
+ * @return \PpdSettingInternal the option object
+ */
+ private function getOption($name, $block = false)
+ {
+ if (!isset($this->settings[$name])) {
+ $this->settings[$name] = new PpdSettingInternal();
+ $this->settings[$name]->block = $block;
+ } elseif ($block !== false) {
+ if ($this->settings[$name]->block === false || $block->isChildOf($this->settings[$name]->block)) {
+ $this->settings[$name]->block = $block;
+ }
+ }
+ return $this->settings[$name];
+ }
+
+ private function binary_in_array($elem, $array)
+ {
+ $top = sizeof($array) - 1;
+ $bot = 0;
+ while ($top >= $bot) {
+ $p = floor(($top + $bot) / 2);
+ if ($array[$p] < $elem)
+ $bot = $p + 1;
+ elseif ($array[$p] > $elem)
+ $top = $p - 1;
+ else return true;
+ }
+ return false;
+ }
+
+ private function validateLine($validator, $option, $value)
+ {
+ if (is_array($validator)) {
+ $oExp = $validator[0];
+ $vExp = $validator[1];
+ } else {
+ $oExp = false;
+ $vExp = $validator;
+ }
+ $regex = '/^\s*' . $vExp . '\s*$/s';
+ if (!preg_match($regex, $value)) {
+ return "Value '$value' does not match $regex";
+ }
+ if ($oExp !== false) {
+ if ($option === false) {
+ return 'Option keyword required, but not present';
+ }
+ $regex = '/^\s*' . $oExp . '\s*$/s';
+ if (!preg_match($regex, $option)) {
+ return "Option keyword '$option' does not match $regex";
+ }
+ }
+ return true;
+ }
+
+ private function getEolChar()
+ {
+ $rn = substr_count("\r\n", $this->data);
+ $r = substr_count("\r", $this->data) - $rn;
+ $n = substr_count("\n", $this->data) - $rn;
+ if ($rn > $r && $rn > $n) {
+ $eol = "\r\n";
+ } elseif ($r > $n) {
+ $eol = "\r";
+ } else {
+ $eol = "\n";
+ }
+ return $eol;
+ }
+
+ /*
+ *
+ */
+
+ public function getError()
+ {
+ return $this->error;
+ }
+
+ public function getWarnings()
+ {
+ return $this->warnings;
+ }
+
+ public function getUISettings()
+ {
+ $result = array();
+ foreach ($this->settings as $mk => $option) {
+ $isUi = ($option->block !== false && $option->block->isUi()) || isset($this->UI_KEYWORDS[$mk]);
+ if ($isUi) {
+ $result[] = $mk;
+ }
+ }
+ return $result;
+ }
+
+ public function getSetting($name)
+ {
+ if (!isset($this->settings[$name]))
+ return false;
+ return new PpdSetting($this->settings[$name], isset($this->UI_KEYWORDS[$name]), $this->encoder);
+ }
+
+ public function removeSetting($name)
+ {
+ if (!isset($this->settings[$name]))
+ return false;
+ $setting = $this->settings[$name];
+ $ranges = array();
+ $this->mergeRanges($ranges, $setting->default);
+ $this->mergeRanges($ranges, $setting->custom);
+ $this->mergeRanges($ranges, $setting->customParam);
+ foreach ($setting->foomatic as $obj) {
+ $this->mergeRanges($ranges, $obj);
+ }
+ foreach ($setting->values as $obj) {
+ $this->mergeRanges($ranges, $obj);
+ }
+ if ($setting->block !== false && $setting->block->isUi()) {
+ $this->mergeRanges($ranges, $setting->block->start, $setting->block->end);
+ }
+ $tmp = array_map(function ($e) { return $e[0]; }, $ranges);
+ array_multisort($tmp, SORT_NUMERIC, $ranges);
+ $new = '';
+ $last = 0;
+ foreach ($ranges as $range) {
+ $new .= substr($this->data, $last, $range[0] - $last);
+ $last = $range[1];
+ if ($this->data{$last} === "\r") {
+ $last++;
+ }
+ if ($this->data{$last} === "\n") {
+ $last++;
+ }
+ }
+ $new .= substr($this->data, $last);
+ $this->data = $new;
+ $this->parse();
+ return $this->error === false;
+ }
+
+ public function addEmptyOption($settingName, $option, $translation = false, $prepend = true)
+ {
+ if (!isset($this->settings[$settingName]))
+ return false;
+ $setting = $this->settings[$settingName];
+ $pos = false;
+ if (!empty($setting->values)) {
+ if ($prepend) {
+ $pos = array_reduce($setting->values, function ($carry, $option) { return min($carry, $option->lineOffset); }, PHP_INT_MAX);
+ } else {
+ $pos = array_reduce($setting->values, function ($carry, $option) { return max($carry, $option->lineOffset); }, 0);
+ }
+ } elseif ($setting->default !== false) {
+ $pos = $setting->default->lineOffset;
+ } elseif ($setting->block !== false && $setting->block->isUi()) {
+ $pos = $this->nextLineEnd($setting->block->start);
+ while ($pos !== false && $pos < $this->dataLen && ($this->data{$pos} === "\r" || $this->data{$pos} === "\n")) {
+ $pos++;
+ }
+ }
+ if ($pos === false) {
+ return false;
+ }
+ $line = '*' . $settingName . ' ' . $option;
+ if ($translation !== false) {
+ $line .= '/' . $this->hexTranslation(($this->encoder)($translation, true));
+ }
+ $eol = $this->getEolChar();
+ $line .= ': ""' . $eol;
+ $this->data = substr($this->data, 0, $pos) . $line . substr($this->data, $pos);
+ $this->parse();
+ return $this->error === false;
+ }
+
+ public function setDefaultOption($settingName, $optionName)
+ {
+ if (!isset($this->settings[$settingName]))
+ return false;
+ $setting = $this->settings[$settingName];
+ $line = '*Default' . $settingName . ': ' . $optionName;
+ if ($setting->default !== false) {
+ $start = $setting->default->lineOffset;
+ $end = $start + $setting->default->lineLen;
+ } elseif (empty($setting->values)) {
+ return false;
+ } else {
+ $option = reset($setting->values);
+ $end = $start = $option->lineOffset;
+ $line .= $this->getEolChar();
+ }
+ $this->data = substr($this->data, 0, $start) . $line . substr($this->data, $end);
+ $this->parse();
+ return $this->error === false;
+ }
+
+ public function write($file)
+ {
+ return file_put_contents($file, $this->data);
+ }
+
+ private function mergeRanges(&$ranges, $start, $end = false)
+ {
+ if (is_object($start) && get_class($start) === 'PpdOption') {
+ $end = $start->lineOffset + $start->lineLen;
+ $start = $start->lineOffset;
+ }
+ if ($start === false || $end === false)
+ return;
+ if ($start >= $end)
+ return; // Don't even bother
+ foreach (array_keys($ranges) as $key) {
+ if ($start <= $ranges[$key][0] && $end >= $ranges[$key][1]) {
+ // Fully dominated
+ unset($ranges[$key]);
+ continue; // Might partially overlap with additional ranges, keep going
+ }
+ if ($ranges[$key][0] <= $start && $ranges[$key][1] >= $start) {
+ // $start lies within existing range
+ if ($ranges[$key][0] <= $end && $ranges[$key][1] >= $end)
+ 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
+ $start = $ranges[$key][0];
+ unset($ranges[$key]);
+ continue;
+ }
+ // Last possibility: $start is before range, $end within range
+ if ($ranges[$key][0] <= $end && $ranges[$key][1] >= $end) {
+ // $start must lie before range start, otherwise we'd have hit the case above
+ $end = $ranges[$key][1];
+ unset($ranges[$key]);
+ continue;
+ }
+ }
+ $ranges[] = array($start, $end);
+ }
+
+ /**
+ * @return bool whether there was at least one known option with format restriction violated.
+ */
+ public function hasInvalidOption()
+ {
+ return $this->knownKeywordMalformed;
+ }
+
+}
+
+/*
+ * Helper classes
+ */
+
+/**
+ * Class PpdOption represents a ppd option
+ */
+class PpdSetting
+{
+
+ /**
+ * @var string default value for this option, or false if not set
+ */
+ public $default = false;
+ /**
+ * @var string|bool what type of block this is in.
+ * Format: Group<groupname>/SubGroup<subgroupname>
+ */
+ public $group = false;
+ /**
+ * @var bool true if this is a ui option
+ */
+ public $isUi;
+ /**
+ * @var string[] list of options mapping optionKeyword => translation
+ */
+ public $options = array();
+ /**
+ * @var bool|string FoomaticRIPOption (format of option) if set, false otherwise
+ */
+ public $foomaticOption = false;
+
+ /**
+ * @var bool|string PickOne, Boolean or PickMany
+ */
+ public $uiOptionType = false;
+
+ public $uiOptionTranslation = false;
+
+ /**
+ * PpdSetting constructor.
+ *
+ * @param \PpdSettingInternal $setting
+ */
+ public function __construct($setting, $isUi, $enc)
+ {
+ if ($setting->default !== false) {
+ $this->default = $setting->default->option;
+ }
+ if ($setting->block !== false && $setting->block->isUi()) {
+ $this->uiOptionType = $setting->block->value;
+ $this->uiOptionTranslation = $enc($setting->block->translation);
+ $this->isUi = true;
+ } else if ($isUi) {
+ $this->uiOptionType = 'PickOne'; // Kinda our fallback
+ $this->isUi = true;
+ } else {
+ $this->isUi = false;
+ }
+ $block = $setting->block;
+ while ($block !== false) {
+ if ($block->isUi()) {
+ if ($this->group === false) {
+ $this->group = $block->type . $block->id;
+ } else {
+ $this->group = $block->type . $block->id . '/' . $this->group;
+ }
+ }
+ $block = $block->parent;
+ }
+ foreach ($setting->values as $value) {
+ $this->options[$value->option] = $enc($value->optionTranslation);
+ }
+ if (isset($setting->foomatic['Option'])) {
+ $this->foomaticOption = $setting->foomatic['Option']->option;
+ }
+ }
+
+}
+
+class PpdSettingInternal
+{
+ /**
+ * @var \PpdOption
+ */
+ public $default = false;
+ /**
+ * @var \PpdOption[]
+ */
+ public $values = array();
+ /**
+ * @var \PpdOption[]
+ */
+ public $foomatic = array();
+ /**
+ * @var \PpdOption
+ */
+ public $custom = false;
+ /**
+ * @var \PpdOption
+ */
+ public $customParam = false;
+ /**
+ * @var \PpdBlockInternal the innermost block this option resides in
+ */
+ public $block = false;
+}
+
+class PpdOption
+{
+ public $option;
+ public $optionTranslation;
+ public $lineOffset;
+ public $lineLen;
+ public $multiLine = false;
+
+ public function __construct($lineOffset, $lineLen, $option, $optionTranslation)
+ {
+ $this->option = $option;
+ $this->optionTranslation = $optionTranslation;
+ $this->lineOffset = $lineOffset;
+ $this->lineLen = $lineLen;
+ }
+}
+
+/**
+ * Class PpdBlock represents a Group, SubGroup, or UI block
+ */
+class PpdBlockInternal
+{
+ public $id;
+ public $translation;
+ public $type;
+ /**
+ * @var \PpdBlockInternal[]
+ */
+ public $childBlocks = array();
+ /**
+ * @var \PpdBlockInternal
+ */
+ public $parent;
+
+ /**
+ * @var int start byte in ppd
+ */
+ public $start;
+
+ /**
+ * @var int|bool end byte in ppd, false if block is not closed
+ */
+ public $end = false;
+
+ /**
+ * @var string value of opening line for block, e.g. 'PickOne' for OpenUI
+ */
+ public $value = false;
+
+ public function __construct($id, $translation, $type, $parent, $start)
+ {
+ $this->id = $id;
+ $this->translation = $translation;
+ $this->type = $type;
+ $this->parent = $parent;
+ $this->start = $start;
+ }
+
+ /**
+ * @return bool true if this is a UI block
+ */
+ public function isUi()
+ {
+ return $this->type == 'UI' || $this->type === 'JCLUI';
+ }
+
+ /**
+ * @param \PpdBlockInternal $block some other PpdBlock instance
+ * @return bool true if this is a child of $block
+ */
+ public function isChildOf($block)
+ {
+ $parent = $this->parent;
+ while ($parent !== false) {
+ if ($parent === $block) {
+ return true;
+ }
+ $parent = $parent->parent;
+ }
+ return false;
+ }
+
+}
diff --git a/modules-available/webinterface/lang/de/messages.json b/modules-available/webinterface/lang/de/messages.json
new file mode 100644
index 00000000..24ca7d5f
--- /dev/null
+++ b/modules-available/webinterface/lang/de/messages.json
@@ -0,0 +1,6 @@
+{
+ "https-on-cert-missing": "HTTPS ist aktiviert, das Zertifikat ist jedoch nicht vorhanden. Bitte nehmen Sie die HTTPS-Konfiguration erneut vor.",
+ "https-used-without-cert": "HTTPS wird gerade verwendet, obwohl kein Zertifikat installiert ist. Falls Sie die Webserver-Konfiguration manuell angepasst haben, um HTTPS zu aktivieren beachten Sie bitte, dass die Konfiguration bei einem zuk\u00fcnftigen Server-Update ohne Nachfrage \u00fcberschrieben werden k\u00f6nnte.",
+ "https-want-off-is-used": "HTTPS wird gerade verwendet, obwohl es laut Einstellungen deaktiviert ist. Merkw\u00fcrdig.",
+ "https-want-redirect-is-plain": "Weiterleitung von HTTP auf HTTPS ist aktiviert, trotzdem scheint die Verbindung Ihres Browsers mit dem Server unverschl\u00fcsselt zu sein. Nehmen Sie die Konfiguration erneut vor und wenden Sie sich an den Support, wenn das Problem weiterhin besteht."
+} \ No newline at end of file
diff --git a/modules-available/webinterface/lang/de/template-tags.json b/modules-available/webinterface/lang/de/template-tags.json
index 3ac6186c..ea1074d2 100644
--- a/modules-available/webinterface/lang/de/template-tags.json
+++ b/modules-available/webinterface/lang/de/template-tags.json
@@ -1,17 +1,23 @@
{
- "lang_HttpsIsDisabled": "HTTPS ist derzeit deaktiviert",
"lang_applyingSettings": "Anwenden der Einstellungen",
"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_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_noHttps": "HTTPS wieder deaktivieren, aktuelles Zertifikat l\u00f6schen",
+ "lang_offSelected": "HTTPS ist derzeit deaktiviert.",
"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:",
"lang_randomCert": "Neues selbstsigniertes Zertifikat generieren",
- "lang_showPasswords": "Passw\u00f6rter anzeigen"
+ "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_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/messages.json b/modules-available/webinterface/lang/en/messages.json
new file mode 100644
index 00000000..803dc73f
--- /dev/null
+++ b/modules-available/webinterface/lang/en/messages.json
@@ -0,0 +1,6 @@
+{
+ "https-on-cert-missing": "HTTPS is enabled, but the certificate is missing. Please redo the configuration steps.",
+ "https-used-without-cert": "HTTPS is currently used, but there is no certificate installed. If you tweaked the web server's configuration manually to enable HTTPS bear in mind that a future server update might overwrite your modified configuration without asking.",
+ "https-want-off-is-used": "HTTPS is currently in use although it is disabled in the settings. Very weird indeed.",
+ "https-want-redirect-is-plain": "HTTP to HTTPS redirects are enabled, but the connection from your browser appears to be unencrypted. Please redo the HTTPS configuration and contact support if the problem persists."
+} \ 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 4d91e4b6..efe649cb 100644
--- a/modules-available/webinterface/lang/en/template-tags.json
+++ b/modules-available/webinterface/lang/en/template-tags.json
@@ -1,17 +1,23 @@
{
- "lang_HttpsIsDisabled": "HTTPS is currently disabled",
"lang_applyingSettings": "Applying settings",
"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_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 chose if you want to use a random self signed certificate, or supply your own.",
+ "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_noHttps": "Disable HTTPS, delete current certificate",
+ "lang_offSelected": "HTTPS is currently disabled.",
"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:",
"lang_randomCert": "Generate new self-signed certificate",
- "lang_showPasswords": "Show passwords"
-}
+ "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_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 3c4304cd..93d659f0 100644
--- a/modules-available/webinterface/page.inc.php
+++ b/modules-available/webinterface/page.inc.php
@@ -3,6 +3,9 @@
class Page_WebInterface extends Page
{
+ const PROP_REDIRECT = 'webinterface.https-redirect';
+ const PROP_TYPE = 'webinterface.https-type';
+
protected function doPreprocess()
{
User::load();
@@ -23,9 +26,11 @@ class Page_WebInterface extends Page
private function actionConfigureHttps()
{
$task = false;
+ $off = '';
switch (Request::post('mode')) {
case 'off':
$task = $this->setHttpsOff();
+ $off = '&hsts=off';
break;
case 'random':
$task = $this->setHttpsRandomCert();
@@ -33,13 +38,17 @@ class Page_WebInterface extends Page
case 'custom':
$task = $this->setHttpsCustomCert();
break;
+ default:
+ $task = $this->setRedirectMode();
+ break;
}
if (isset($task['id'])) {
Session::set('https-id', $task['id']);
- Util::redirect('?do=WebInterface&show=httpsupdate');
+ Util::redirect('?do=WebInterface&show=httpsupdate' . $off);
}
+ Util::redirect('?do=WebInterface');
}
-
+
private function actionShowHidePassword()
{
Property::setPasswordFieldType(Request::post('mode') === 'show' ? 'text' : 'password');
@@ -48,10 +57,57 @@ class Page_WebInterface extends Page
protected function doRender()
{
+ //
+ // HTTPS
+ //
if (Request::get('show') === 'httpsupdate') {
Render::addTemplate('httpd-restart', array('taskid' => Session::get('https-id')));
}
- Render::addTemplate('https', array('httpsEnabled' => file_exists('/etc/lighttpd/server.pem')));
+ $type = Property::get(self::PROP_TYPE);
+ $force = Property::get(self::PROP_REDIRECT) === 'True';
+ $https = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
+ $exists = file_exists('/etc/lighttpd/server.pem');
+ $data = array(
+ 'httpsUsed' => $https,
+ 'redirect_checked' => ($force ? 'checked' : '')
+ );
+ // Type should be 'off', 'generated', 'supplied'
+ if ($type === 'off') {
+ if ($exists) {
+ // HTTPS is set to off, but a certificate exists
+ if ($https) {
+ // User is using https, just warn to prevent lockout
+ Message::addWarning('https-want-off-is-used');
+ } else {
+ // User is not using https, try to delete stray certificate
+ $this->setHttpsOff();
+ }
+ } elseif ($https) {
+ // Set to off, no cert found, but still using HTTPS apparently
+ // Admin might have modified web server config in another way
+ Message::addWarning('https-used-without-cert');
+ }
+ } elseif ($type === 'generated' || $type === 'supplied') {
+ $data['httpsEnabled'] = true;
+ if ($force && !$https) {
+ Message::addWarning('https-want-redirect-is-plain');
+ }
+ if (!$exists) {
+ Message::addWarning('https-on-cert-missing');
+ }
+ } else {
+ // Unknown config - maybe upgraded old install that doesn't keep track
+ if ($exists || $https) {
+ $type = 'unknown'; // Legacy fallback
+ } else {
+ $type = 'off';
+ }
+ }
+ $data[$type . 'Selected'] = true;
+ Render::addTemplate('https', $data);
+ //
+ // Password fields
+ //
$data = array();
if (Property::getPasswordFieldType() === 'text')
$data['selected_show'] = 'checked';
@@ -62,23 +118,49 @@ class Page_WebInterface extends Page
private function setHttpsOff()
{
+ Property::set(self::PROP_TYPE, 'off');
+ Header('Strict-Transport-Security: max-age=0', true);
return Taskmanager::submit('LighttpdHttps', array());
}
private function setHttpsRandomCert()
{
+ $force = Request::post('httpsredirect', false, 'string') === 'on';
+ Property::set(self::PROP_TYPE, 'generated');
+ Property::set(self::PROP_REDIRECT, $force ? 'True' : 'False');
return Taskmanager::submit('LighttpdHttps', array(
- 'proxyip' => Property::getServerIp()
+ 'proxyip' => Property::getServerIp(),
+ 'redirect' => $force,
));
}
private function setHttpsCustomCert()
{
+ $force = Request::post('httpsredirect', false, 'string') === 'on';
+ Property::set(self::PROP_TYPE, 'supplied');
+ Property::set(self::PROP_REDIRECT, $force ? 'True' : 'False');
return Taskmanager::submit('LighttpdHttps', array(
'importcert' => Request::post('certificate', 'bla'),
'importkey' => Request::post('privatekey', 'bla'),
- 'importchain' => Request::post('cachain', '')
+ 'importchain' => Request::post('cachain', ''),
+ 'redirect' => $force,
+ ));
+ }
+
+ private function setRedirectMode()
+ {
+ $force = Request::post('httpsredirect', false, 'string') === 'on';
+ Property::set(self::PROP_REDIRECT, $force ? 'True' : 'False');
+ if (Property::get(self::PROP_TYPE) === 'off') {
+ // Don't bother running the task if https isn't enabled - just
+ // update the state in DB
+ return false;
+ }
+ return Taskmanager::submit('LighttpdHttps', array(
+ 'redirectOnly' => true,
+ 'redirect' => $force,
));
}
}
+
diff --git a/modules-available/webinterface/templates/httpd-restart.html b/modules-available/webinterface/templates/httpd-restart.html
index cc84aafb..ac4e726b 100644
--- a/modules-available/webinterface/templates/httpd-restart.html
+++ b/modules-available/webinterface/templates/httpd-restart.html
@@ -1,6 +1,42 @@
<div class="panel panel-default">
<div class="panel-heading">{{lang_applyingSettings}}</div>
<div class="panel-body">
- <div data-tm-id="{{taskid}}" data-tm-log="error">{{lang_installAndRestart}}</div>
+ <div data-tm-id="{{taskid}}" data-tm-log="error" data-tm-callback="slxRestartCb">{{lang_installAndRestart}}</div>
</div>
</div>
+<script type="application/javascript"><!--
+
+var slxRedirTimeout = 0;
+var slxRedirTimer = false;
+
+function slxRestartCb(task) {
+ if (!task || !task.statusCode)
+ return;
+ if (task.statusCode === 'TASK_WAITING' || task.statusCode === 'TASK_PROCESSING') {
+ // Polling still works, reset counter
+ console.log('Resetting because ' + task.statusCode);
+ slxRedirTimeout = 0;
+ } else {
+ console.log('Disabling because ' + task.statusCode);
+ clearInterval(slxRedirTimer);
+ window.location.replace(window.location.href.replace('&show=httpsupdate', ''));
+ }
+}
+
+slxRedirTimer = setInterval(function() {
+ // Didn't get status update from TM for 6 seconds - try to switch protocols
+ if (++slxRedirTimeout > 6) {
+ console.log('TIMEOUT REACHED');
+ clearInterval(slxRedirTimer);
+ var url = window.location.href.split(':', 2)[1];
+ if (window.location.protocol === 'https:') {
+ url = 'http:' + url;
+ } else {
+ url = 'https:' + url;
+ }
+ console.log('REDIRECT TO ' + url);
+ window.location.replace(url);
+ }
+}, 1000);
+
+//--></script> \ No newline at end of file
diff --git a/modules-available/webinterface/templates/https.html b/modules-available/webinterface/templates/https.html
index dfd2a3fe..77585ddf 100644
--- a/modules-available/webinterface/templates/https.html
+++ b/modules-available/webinterface/templates/https.html
@@ -5,9 +5,26 @@
<div class="panel-heading">{{lang_httpsSettings}}</div>
<div class="panel-body">
<p>{{lang_httpsDescription}}</p>
- {{^httpsEnabled}}
- <p>{{lang_HttpsIsDisabled}}</p>
- {{/httpsEnabled}}
+ {{^httpsUsed}}
+ {{lang_youreNotUsingHttps}}
+ {{/httpsUsed}}
+ {{#httpsUsed}}
+ {{lang_youreUsingHttps}}
+ {{/httpsUsed}}
+ <div class="text-info slx-bold">
+ {{#offSelected}}
+ <p>{{lang_offSelected}}</p>
+ {{/offSelected}}
+ {{#unknownSelected}}
+ <p>{{lang_unknownSelected}}</p>
+ {{/unknownSelected}}
+ {{#generatedSelected}}
+ <p>{{lang_generatedSelected}}</p>
+ {{/generatedSelected}}
+ {{#suppliedSelected}}
+ <p>{{lang_suppliedSelected}}</p>
+ {{/suppliedSelected}}
+ </div>
{{#httpsEnabled}}
<div class="input-group" onclick="$('#moff').prop('checked', true);
$('#wcustom').hide()">
@@ -31,6 +48,7 @@
{{lang_customCert}}
</span>
</div>
+
<div class="well well-sm" style="display:none" id="wcustom">
{{lang_certificate}}
<pre class="small">
@@ -52,6 +70,16 @@ MIIFfTCCA...
<textarea name="cachain" class="form-control small" cols="101" rows="10"></textarea>
<hr>
</div>
+
+ <br>
+ <div class="input-group">
+ <span class="input-group-addon"><input id="httpsredirect" type="checkbox" name="httpsredirect" value="on" {{redirect_checked}}></span>
+ <span class="form-control" onclick="$('#httpsredirect').prop('checked', !$('#httpsredirect').prop('checked'))">
+ {{lang_httpsRedirect}}
+ </span>
+ </div>
+ <br>
+
<div class="pull-right">
<button type="submit" class="btn btn-primary">{{lang_save}}</button>
</div>
diff --git a/modules-available/webinterface/templates/passwords.html b/modules-available/webinterface/templates/passwords.html
index 1f23dfc4..8481d884 100644
--- a/modules-available/webinterface/templates/passwords.html
+++ b/modules-available/webinterface/templates/passwords.html
@@ -17,6 +17,7 @@
{{lang_hidePasswords}}
</span>
</div>
+ <br>
<div class="pull-right">
<button type="submit" class="btn btn-primary">{{lang_save}}</button>
</div>