summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--apis/getconfig.inc.php56
-rw-r--r--apis/update.inc.php38
-rw-r--r--inc/database.inc.php2
-rw-r--r--inc/defaultdata.inc.php24
-rw-r--r--inc/location.inc.php148
-rw-r--r--lang/de/messages.json7
-rw-r--r--lang/de/settings/cat_setting.json3
-rw-r--r--lang/de/settings/setting.json5
-rw-r--r--lang/de/templates/locations/location-subnets.json18
-rw-r--r--lang/de/templates/locations/locations.json10
-rw-r--r--lang/de/templates/locations/subnets.json7
-rw-r--r--lang/de/templates/main-menu.json1
-rw-r--r--lang/en/messages.json7
-rw-r--r--lang/en/settings/cat_setting.json3
-rw-r--r--lang/en/settings/setting.json5
-rw-r--r--lang/en/templates/locations/location-subnets.json18
-rw-r--r--lang/en/templates/locations/locations.json10
-rw-r--r--lang/en/templates/locations/subnets.json7
-rw-r--r--lang/en/templates/main-menu.json1
-rw-r--r--lang/pt/settings/cat_setting.json3
-rw-r--r--lang/pt/templates/locations/location-subnets.json3
-rw-r--r--lang/pt/templates/locations/locations.json3
-rw-r--r--lang/pt/templates/locations/subnets.json3
-rw-r--r--modules/locations.inc.php348
-rw-r--r--style/default.css10
-rw-r--r--templates/locations/location-subnets.html73
-rw-r--r--templates/locations/locations.html96
-rw-r--r--templates/locations/subnets.html35
-rw-r--r--templates/main-menu.html1
29 files changed, 934 insertions, 11 deletions
diff --git a/apis/getconfig.inc.php b/apis/getconfig.inc.php
index 5d5dbca8..2447d622 100644
--- a/apis/getconfig.inc.php
+++ b/apis/getconfig.inc.php
@@ -1,5 +1,10 @@
<?php
+$ip = $_SERVER['REMOTE_ADDR'];
+if (substr($ip, 0, 7) === '::ffff:') {
+ $ip = substr($ip, 7);
+}
+
/**
* Escape given string so it is a valid string in sh that can be surrounded
* by single quotes ('). This basically turns _'_ into _'"'"'_
@@ -12,13 +17,59 @@ function escape($string)
return str_replace("'", "'\"'\"'", $string);
}
+// Location handling: figure out location
+$locationId = false; // TODO: machine specific mapping
+if ($locationId === false) {
+ // Fallback to subnets
+ $locationId = Location::getFromIp($ip);
+}
+$matchingLocations = array();
+if ($locationId !== false) {
+ // Get all parents
+ settype($locationId, 'integer');
+ $locations = Location::getLocationsAssoc();
+ $find = $locationId;
+ while (isset($locations[$find]) && !in_array($find, $matchingLocations)) {
+ $matchingLocations[] = $find;
+ $find = (int)$locations[$find]['parentlocationid'];
+ }
+ echo "SLX_LOCATIONS='" . escape(implode(' ', $matchingLocations)) . "'\n";
+}
+
+$configVars = array();
+// Query location specific settings (from bottom to top)
+if (!empty($matchingLocations)) {
+ // First get all settings for all locations we're in
+ $list = implode(',', $matchingLocations);
+ $res = Database::simpleQuery("SELECT locationid, setting, value FROM setting_location WHERE locationid IN ($list)");
+ $tmp = array();
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ $tmp[(int)$row['locationid']][$row['setting']] = $row['value'];
+ }
+ // $matchingLocations contains the location ids sorted from closest to furthest, so we use it to make sure the order
+ // in which they override is correct (closest setting wins, e.g. room setting beats department setting)
+ foreach ($matchingLocations as $lid) {
+ if (!isset($tmp[$lid])) continue;
+ foreach ($tmp[$lid] as $setting => $value) {
+ if (!isset($configVars[$setting])) {
+ $configVars[$setting] = $value;
+ }
+ }
+ }
+ unset($tmp);
+}
+
// Dump config from DB
$res = Database::simpleQuery('SELECT setting.setting, setting.defaultvalue, tbl.value
FROM setting
LEFT JOIN setting_global AS tbl USING (setting)
ORDER BY setting ASC'); // TODO: Add setting groups and sort order
while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
- if (is_null($row['value'])) $row['value'] = $row['defaultvalue'];
+ if (isset($configVars[$row['setting']])) {
+ $row['value'] = $configVars[$row['setting']];
+ } elseif (is_null($row['value'])) {
+ $row['value'] = $row['defaultvalue'];
+ }
echo $row['setting'] . "='" . escape($row['value']) . "'\n";
}
@@ -47,6 +98,7 @@ if (is_array($vmstore)) {
}
}
+
// For quick testing or custom extensions: Include external file that should do nothing
// more than outputting more key-value-pairs. It's expected in the webroot of slxadmin
-if (file_exists('client_config_additional.php')) @include('client_config_additional.php'); \ No newline at end of file
+if (file_exists('client_config_additional.php')) @include('client_config_additional.php');
diff --git a/apis/update.inc.php b/apis/update.inc.php
index a7212961..0c91ab2d 100644
--- a/apis/update.inc.php
+++ b/apis/update.inc.php
@@ -52,6 +52,7 @@ if ($list === false) {
Message::addSuccess('db-update-done');
if (tableExists('eventlog'))
EventLog::info("Database updated to version $currentVersion");
+DefaultData::populate();
Util::redirect('index.php?do=Main');
////////////////
@@ -256,7 +257,7 @@ function update_10()
}
Database::exec("CREATE TABLE IF NOT EXISTS `machine` (
`machineuuid` char(36) CHARACTER SET ascii NOT NULL,
- `roomid` int(10) unsigned DEFAULT NULL,
+ `locationid` int(11) DEFAULT NULL,
`macaddr` char(17) CHARACTER SET ascii NOT NULL,
`clientip` varchar(45) CHARACTER SET ascii NOT NULL,
`firstseen` int(10) unsigned NOT NULL,
@@ -281,10 +282,43 @@ function update_10()
KEY `mbram` (`mbram`),
KEY `kvmstate` (`kvmstate`),
KEY `id44mb` (`id44mb`),
- KEY `roomid` (`roomid`),
+ KEY `locationid` (`locationid`),
KEY `lastseen` (`lastseen`),
KEY `cpumodel` (`cpumodel`),
KEY `systemmodel` (`systemmodel`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8");
return true;
}
+
+function update_11()
+{
+ if (tableHasColumn('machine', 'roomid')) {
+ Database::exec("ALTER TABLE `machine` CHANGE `roomid` `locationid` INT(11) DEFAULT NULL");
+ }
+ Database::exec("CREATE TABLE IF NOT EXISTS `setting_location` (
+ `locationid` int(11) NOT NULL,
+ `setting` varchar(28) NOT NULL,
+ `value` text NOT NULL,
+ `displayvalue` text NOT NULL,
+ PRIMARY KEY (`locationid`,`setting`),
+ KEY `setting` (`setting`)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8");
+ Database::exec("CREATE TABLE IF NOT EXISTS `location` (
+ `locationid` int(11) NOT NULL AUTO_INCREMENT,
+ `parentlocationid` int(11) NOT NULL,
+ `locationname` varchar(100) NOT NULL,
+ PRIMARY KEY (`locationid`),
+ KEY `locationname` (`locationname`),
+ KEY `parentlocationid` (`parentlocationid`)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8;");
+ Database::exec("CREATE TABLE IF NOT EXISTS `subnet` (
+ `subnetid` int(11) NOT NULL AUTO_INCREMENT,
+ `startaddr` decimal(39,0) unsigned NOT NULL,
+ `endaddr` decimal(39,0) unsigned NOT NULL,
+ `locationid` int(11) NOT NULL,
+ PRIMARY KEY (`subnetid`),
+ KEY `startaddr` (`startaddr`,`endaddr`),
+ KEY `locationid` (`locationid`)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8;");
+ return true;
+} \ No newline at end of file
diff --git a/inc/database.inc.php b/inc/database.inc.php
index 978f0bfd..735b2181 100644
--- a/inc/database.inc.php
+++ b/inc/database.inc.php
@@ -20,7 +20,7 @@ class Database
*/
public static function getExpectedSchemaVersion()
{
- return 11;
+ return 12;
}
public static function needSchemaUpdate()
diff --git a/inc/defaultdata.inc.php b/inc/defaultdata.inc.php
index 82ed1165..9bd31628 100644
--- a/inc/defaultdata.inc.php
+++ b/inc/defaultdata.inc.php
@@ -21,11 +21,12 @@ class DefaultData
{
$cats = array(
1 => 30, // Inactivity/Shutdown
- 2 => 20, // Internet access
+ 2 => 50, // Internet access
3 => 100, // Timesync
4 => 10, // System config
//5 => 15, // Public Shared folder
6 => 20000, // Unassigned/no category
+ 7 => 20,
);
foreach ($cats as $cat => $sort) {
Database::exec("INSERT IGNORE INTO cat_setting (catid, sortval) VALUES (:catid, :sortval)", array(
@@ -169,6 +170,27 @@ class DefaultData
'permissions' => '2',
'validator' => ''
),
+ array(
+ 'setting' => 'SLX_VMCHOOSER_TAB',
+ 'catid' => '7',
+ 'defaultvalue' => 'AUTO',
+ 'permissions' => '2',
+ 'validator' => 'list:0|1|2|AUTO'
+ ),
+ array(
+ 'setting' => 'SLX_VMCHOOSER_TEMPLATES',
+ 'catid' => '7',
+ 'defaultvalue' => 'IGNORE',
+ 'permissions' => '2',
+ 'validator' => 'list:IGNORE|BUMP'
+ ),
+ array(
+ 'setting' => 'SLX_VMCHOOSER_FORLOCATION',
+ 'catid' => '7',
+ 'defaultvalue' => 'BUMP',
+ 'permissions' => '2',
+ 'validator' => 'list:IGNORE|BUMP|EXCLUSIVE'
+ ),
);
foreach ($data as $entry) {
Database::exec("INSERT IGNORE INTO setting (setting, catid, defaultvalue, permissions, validator)"
diff --git a/inc/location.inc.php b/inc/location.inc.php
new file mode 100644
index 00000000..39ecdf67
--- /dev/null
+++ b/inc/location.inc.php
@@ -0,0 +1,148 @@
+<?php
+
+class Location
+{
+
+ private static $flatLocationCache = false;
+ private static $assocLocationCache = false;
+
+ public static function queryLocations()
+ {
+ $res = Database::simpleQuery("SELECT locationid, parentlocationid, locationname FROM location");
+ $rows = array();
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ $rows[] = $row;
+ }
+ return $rows;
+ }
+
+ public static function getLocationsAssoc()
+ {
+ if (self::$assocLocationCache === false) {
+ $rows = self::queryLocations();
+ $rows = self::buildTree($rows);
+ self::$assocLocationCache = self::flattenTreeAssoc($rows);
+ }
+ return self::$assocLocationCache;
+ }
+
+ private static function flattenTreeAssoc($tree, $depth = 0)
+ {
+ $output = array();
+ foreach ($tree as $node) {
+ $output[(int)$node['locationid']] = array(
+ 'parentlocationid' => (int)$node['parentlocationid'],
+ 'locationname' => $node['locationname'],
+ 'depth' => $depth
+ );
+ if (!empty($node['children'])) {
+ $output += self::flattenTreeAssoc($node['children'], $depth + 1);
+ }
+ }
+ return $output;
+ }
+
+ public static function getLocations($default = 0, $excludeId = 0, $addNoParent = false)
+ {
+ if (self::$flatLocationCache === false) {
+ $rows = self::queryLocations();
+ $rows = self::buildTree($rows);
+ $rows = self::flattenTree($rows);
+ self::$flatLocationCache = $rows;
+ } else {
+ $rows = self::$flatLocationCache;
+ }
+ $del = false;
+ unset($row);
+ foreach ($rows as $key => &$row) {
+ if ($del === false && $row['locationid'] == $excludeId) {
+ $del = $row['depth'];
+ } elseif ($del !== false && $row['depth'] <= $del) {
+ $del = false;
+ }
+ if ($del !== false) {
+ unset($rows[$key]);
+ continue;
+ }
+ if ($row['locationid'] == $default) {
+ $row['selected'] = true;
+ }
+ }
+ if ($addNoParent) {
+ array_unshift($rows, array(
+ 'locationid' => 0,
+ 'locationname' => '-----',
+ 'selected' => $default == 0
+ ));
+ }
+ return array_values($rows);
+ }
+
+ public static function buildTree($elements, $parentId = 0)
+ {
+ $branch = array();
+ $sort = array();
+ foreach ($elements as $element) {
+ if ($element['locationid'] == 0 || $element['locationid'] == $parentId)
+ continue;
+ if ($element['parentlocationid'] == $parentId) {
+ $children = self::buildTree($elements, $element['locationid']);
+ if (!empty($children)) {
+ $element['children'] = $children;
+ }
+ $branch[] = $element;
+ $sort[] = $element['locationname'];
+ }
+ }
+ array_multisort($sort, SORT_ASC, $branch);
+ return $branch;
+ }
+
+ private static function flattenTree($tree, $depth = 0)
+ {
+ $output = array();
+ foreach ($tree as $node) {
+ $output[] = array(
+ 'locationid' => $node['locationid'],
+ 'locationname' => $node['locationname'],
+ 'locationpad' => str_repeat('--', $depth),
+ 'locationspan1' => $depth + 1,
+ 'locationspan2' => 10 - $depth,
+ 'depth' => $depth
+ );
+ if (!empty($node['children'])) {
+ $output = array_merge($output, self::flattenTree($node['children'], $depth + 1));
+ }
+ }
+ return $output;
+ }
+
+ public static function extractIds($tree)
+ {
+ $ids = array();
+ foreach ($tree as $node) {
+ $ids[] = $node['locationid'];
+ if (!empty($node['children'])) {
+ $ids = array_merge($ids, self::extractIds($node['children']));
+ }
+ }
+ return $ids;
+ }
+
+ public static function getFromIp($ip)
+ {
+ $locationId = false;
+ $long = sprintf('%u', ip2long($ip));
+ $net = Database::simpleQuery('SELECT locationid FROM subnet'
+ . ' WHERE :ip BETWEEN startaddr AND endaddr', array('ip' => $long));
+ while ($row = $net->fetch(PDO::FETCH_ASSOC)) {
+ $locations = self::getLocationsAssoc();
+ $id = (int)$row['locationid'];
+ if (!isset($locations[$id])) continue;
+ if ($locationId !== false && $locations[$id]['depth'] <= $locations[$locationId]['depth']) continue;
+ $locationId = $id;
+ }
+ return $locationId;
+ }
+
+}
diff --git a/lang/de/messages.json b/lang/de/messages.json
index 4d2859b2..1d3a40bd 100644
--- a/lang/de/messages.json
+++ b/lang/de/messages.json
@@ -1,5 +1,6 @@
{
"ad-config-failed": "Wiederherstellen der Active Directory Konfiguration fehlgeschlagen",
+ "added-x-entries": "Eintr\u00e4ge hinzugef\u00fcgt: {{0}}",
"adduser-disabled": "Keine ausreichenden Rechte, um weitere Benutzer hinzuzuf\u00fcgen",
"adduser-success": "Benutzer erfolgreich hinzugef\u00fcgt",
"backup-failed": "Sicherung fehlgeschlagen!",
@@ -25,6 +26,8 @@
"invalid-ip": "Kein Interface ist auf die Adresse {{0}} konfiguriert",
"invalid-path": "Ung\u00fcltiger Pfad.",
"invalid-template": "Ausgew\u00e4hlte Template ist nicht g\u00fcltig",
+ "location-deleted": "Location wurde gel\u00f6scht (Locations: {{0}}, Subnets: {{1}})",
+ "location-updated": "Location {{0}} wurde aktualisiert",
"loginfail": "Benutzername oder Kennwort falsch",
"mail-config-saved": "Mail-Konfiguration gespeichert",
"missing-file": "Es wurde keine Datei ausgew\u00e4hlt!",
@@ -42,6 +45,7 @@
"no-image": "Unter der angegebenen URL konnte kein SVG-Bild gefunden werden",
"no-permission": "Keine ausreichenden Rechte, um auf diese Seite zuzugreifen",
"notes-saved": "Anmerkungen gespeichert",
+ "parameter-missing": "Fehlender Parameter: {{0}}",
"password-mismatch": "Passwort und Passwortbest\u00e4tigung stimmen nicht \u00fcberein",
"reboot-unconfirmed": "Sicherheitsabfrage zum Reboot nicht best\u00e4tigt",
"remote-parse-failed": "Parsen der empfangenen Daten fehlgeschlagen ({{0}})",
@@ -50,6 +54,9 @@
"replacing-module": "Ersetzen von Modul {{0}}",
"restore-done": "Wiederherstellung abgeschlossen",
"settings-updated": "Einstellungen wurden aktualisiert",
+ "subnets-created": "Subnetze angelegt: {{0}}",
+ "subnets-deleted": "Subnetze gel\u00f6scht: {{0}}",
+ "subnets-updated": "Subnetze aktualisiert: {{0}}",
"task-error": "Ausf\u00fchrung fehlgeschlagen: {{0}}",
"taskmanager-error": "Verbindung zum Taskmanager fehlgeschlagen",
"taskmanager-format": "Taskmanager hat ung\u00fcltige Daten zur\u00fcckgeliefert",
diff --git a/lang/de/settings/cat_setting.json b/lang/de/settings/cat_setting.json
index 0a8050b5..b8cb6935 100644
--- a/lang/de/settings/cat_setting.json
+++ b/lang/de/settings/cat_setting.json
@@ -4,5 +4,6 @@
"cat_3": "Zeitsynchronisation",
"cat_4": "Grundsystem",
"cat_5": "Gemeinsames Netzlaufwerk",
- "cat_6": "Unkategorisiert"
+ "cat_6": "Unkategorisiert",
+ "cat_7": "vmchooser"
} \ No newline at end of file
diff --git a/lang/de/settings/setting.json b/lang/de/settings/setting.json
index 46ead11b..af1007be 100644
--- a/lang/de/settings/setting.json
+++ b/lang/de/settings/setting.json
@@ -16,5 +16,8 @@
"SLX_REMOTE_LOG_SESSIONS": "Legt fest, ob Logins und Logouts der Benutzer an den Satelliten gemeldet werden sollen.\r\n*yes* = Mit Benutzerkennung loggen\r\n*anonymous* = Anonym loggen\r\n*no* = Nicht loggen",
"SLX_ROOT_PASS": "Das root-Passwort des Grundsystems. Wird nur f\u00fcr Diagnosezwecke am Client ben\u00f6tigt.\r\nFeld leer lassen, um root-Logins zu verbieten.\r\n\/Hinweis\/: Das Passwort wird im Klartext in der lokalen Datenbank hinterlegt, jedoch immer gehasht an die Clients \u00fcbermittelt (SHA-512 mit Salt). Wenn Sie das Passwort auch im Satelliten nicht im Klartext speichern wollen, k\u00f6nnen Sie hier auch ein vorgehashtes Passwort eintragen (im *$6$....*-Format).",
"SLX_SHUTDOWN_SCHEDULE": "Feste Uhrzeit, zu der sich die Rechner ausschalten, auch wenn noch ein Benutzer aktiv ist.Mehrere Zeitpunkte k\u00f6nnen durch Leerzeichen getrennt angegeben werden.",
- "SLX_SHUTDOWN_TIMEOUT": "Zeit in Sekunden, nach dem ein Rechner abgeschaltet wird, sofern kein Benutzer angemeldet ist.Feld leer lassen, um die Funktion zu deaktivieren."
+ "SLX_SHUTDOWN_TIMEOUT": "Zeit in Sekunden, nach dem ein Rechner abgeschaltet wird, sofern kein Benutzer angemeldet ist.Feld leer lassen, um die Funktion zu deaktivieren.",
+ "SLX_VMCHOOSER_FORLOCATION": "Legt das Verhalten fest, wenn es Veranstaltungen gibt, die an einen bestimmten Ort\/Raum gebunden sind.\r\n*IGNORE*: Mit den restlichen, globalen Veranstaltungen alphabetisch sortiert auflisten.\r\n*BUMP*: Die spezifischen Veranstaltungen oben auflisten, die globalen darunter.\r\n*EXCLUSIVE*: Spezifische Veranstaltungen oben auflisten, globale Veranstaltungen zun\u00e4chst ausblenden. Die globalen Veranstaltungen befinden sich unter einem eingeklappten Listenknoten.",
+ "SLX_VMCHOOSER_TAB": "Bestimmt, welcher Karteireiter im vmchooser standardm\u00e4\u00dfig ausgew\u00e4hlt wird.\r\n*0*: Native Linux-Sessions\r\n*1*: Nutzerspezifische Kurse\r\n*2*: Alle Kurse\r\n*AUTO*: Hat der Rechner beschr\u00e4nkte Ressourcen, werden die Linux-Sitzungen angezeigt, sonst alle Kurse\r\n\r\nHat der Benutzer ein persistentes Home-Verzeichnis, wirkt sich diese Einstellung nur beim ersten Anmelden aus. Bei sp\u00e4teren Sitzungen markiert der vmchooser die zuletzt gestartete Sitzung und wechselt zum entsprechenden Karteireiter.",
+ "SLX_VMCHOOSER_TEMPLATES": "Legt fest, wie Veranstaltungen in der Sortierung behandelt werden, welche auf eine VM linken, die eine Vorlage ist.\r\n*IGNORE*: Wie regul\u00e4re Veranstaltungen behandeln\r\n*BUMP*: Weiter oben in der Liste einsortieren"
} \ No newline at end of file
diff --git a/lang/de/templates/locations/location-subnets.json b/lang/de/templates/locations/location-subnets.json
new file mode 100644
index 00000000..6caa1991
--- /dev/null
+++ b/lang/de/templates/locations/location-subnets.json
@@ -0,0 +1,18 @@
+{
+ "lang_addNewSubnet": "Neues Subnetz hinzuf\u00fcgen",
+ "lang_assignSubnetExplanation": "Rechner, die in einen der hier aufgef\u00fchrten Adressbereiche fallen, werden diesem Ort zugeschrieben und erhalten damit z.B. f\u00fcr diesen Raum angepasste Veranstaltungslisten.",
+ "lang_assignedSubnets": "Zugeordnete Subnetze bzw. IP-Bereiche",
+ "lang_deleteChildLocations": "Untergeordnete Orte ebenfalls l\u00f6schen",
+ "lang_deleteLocation": "Ort l\u00f6schen",
+ "lang_deleteSubnet": "Bereich l\u00f6schen",
+ "lang_endAddress": "Endadresse",
+ "lang_locationInfo": "Details zu diesem Ort",
+ "lang_locationSettings": "Raum\/Ort bearbeiten",
+ "lang_matchingMachines": "Enthaltene Rechner",
+ "lang_name": "Name",
+ "lang_parentLocation": "\u00dcbergeordneter Ort",
+ "lang_referencingLectures": "Veranstaltungen",
+ "lang_save": "Speichern",
+ "lang_startAddress": "Startadresse",
+ "lang_subnet": "IP-Bereich"
+} \ No newline at end of file
diff --git a/lang/de/templates/locations/locations.json b/lang/de/templates/locations/locations.json
new file mode 100644
index 00000000..3e25ef45
--- /dev/null
+++ b/lang/de/templates/locations/locations.json
@@ -0,0 +1,10 @@
+{
+ "lang_areYouSureNoUndo": "Sind Sie sicher? Diese Aktion kann nicht r\u00fcckg\u00e4ngig gemacht werden.",
+ "lang_edit": "Bearbeiten",
+ "lang_location": "Ort",
+ "lang_locationName": "Name",
+ "lang_locationsMainHeading": "Verwaltung von R\u00e4umen\/Orten",
+ "lang_noParent": "Kein \u00fcbergeordneter Ort",
+ "lang_save": "Speichern",
+ "lang_thisListBySubnet": "Nach Subnetzen auflisten"
+} \ No newline at end of file
diff --git a/lang/de/templates/locations/subnets.json b/lang/de/templates/locations/subnets.json
new file mode 100644
index 00000000..b57f87ce
--- /dev/null
+++ b/lang/de/templates/locations/subnets.json
@@ -0,0 +1,7 @@
+{
+ "lang_endAddress": "Ende",
+ "lang_listOfSubnets": "Liste der Subnetze",
+ "lang_location": "Ort",
+ "lang_startAddress": "Start",
+ "lang_thisListByLocation": "Zur Ortsansicht"
+} \ No newline at end of file
diff --git a/lang/de/templates/main-menu.json b/lang/de/templates/main-menu.json
index 3ed43eff..67ae974a 100644
--- a/lang/de/templates/main-menu.json
+++ b/lang/de/templates/main-menu.json
@@ -10,6 +10,7 @@
"lang_internetAccess": "Internetzugriff",
"lang_language": "Sprachen",
"lang_localization": "Lokalisierung + Integration",
+ "lang_locations": "R\u00e4ume\/Orte",
"lang_login": "Anmelden",
"lang_logout": "Abmelden",
"lang_needsSetup": "Einrichtung unvollst\u00e4ndig",
diff --git a/lang/en/messages.json b/lang/en/messages.json
index aed10cce..7d60575d 100644
--- a/lang/en/messages.json
+++ b/lang/en/messages.json
@@ -1,5 +1,6 @@
{
"ad-config-failed": "Rebuilding the Active Directory configuration failed",
+ "added-x-entries": "Entries added: {{0}}",
"adduser-disabled": "Insufficient privileges to add more users",
"adduser-success": "User successfully added",
"backup-failed": "Backup failed!",
@@ -25,6 +26,8 @@
"invalid-ip": "No interface is configured with the address {{0}}",
"invalid-path": "Invalid path.",
"invalid-template": "Selected template is not valid",
+ "location-deleted": "Location has been deleted (locations: {{0}}, subnets: {{1}})",
+ "location-updated": "Location {{0}} has been updated",
"loginfail": "Username or Password incorrect",
"mail-config-saved": "Mail config saved",
"missing-file": "There was no file selected!",
@@ -42,6 +45,7 @@
"no-image": "Could not find an SVG-image at the given URL",
"no-permission": "No sufficient privileges to access this page",
"notes-saved": "Notes have been saved",
+ "parameter-missing": "Missing parameter: {{0}}",
"password-mismatch": "Password and password confirmation do not match",
"reboot-unconfirmed": "Confirmation prompt to reboot not confirmed",
"remote-parse-failed": "Parsing the received data failed ({{0}})",
@@ -50,6 +54,9 @@
"replacing-module": "Replace module {{0}}",
"restore-done": "Restore done",
"settings-updated": "Settings have been updated",
+ "subnets-created": "{{0}} subnet(s) created",
+ "subnets-deleted": "{{0}} subnet(s) deleted",
+ "subnets-updated": "{{0}} subnet(s) updated",
"task-error": "Execution failed: {{0}}",
"taskmanager-error": "Failed to connect to the Task Manager",
"taskmanager-format": "Task Manager has returned invalid data",
diff --git a/lang/en/settings/cat_setting.json b/lang/en/settings/cat_setting.json
index 856775a9..7c0ab654 100644
--- a/lang/en/settings/cat_setting.json
+++ b/lang/en/settings/cat_setting.json
@@ -4,5 +4,6 @@
"cat_3": "Time Synchronization",
"cat_4": "Basic System",
"cat_5": "Common network share",
- "cat_6": "Uncategorized"
+ "cat_6": "Uncategorized",
+ "cat_7": "vmchooser"
} \ No newline at end of file
diff --git a/lang/en/settings/setting.json b/lang/en/settings/setting.json
index 11a34f17..36ac6e8c 100644
--- a/lang/en/settings/setting.json
+++ b/lang/en/settings/setting.json
@@ -16,5 +16,8 @@
"SLX_REMOTE_LOG_SESSIONS": "Determines whether logins and logouts of the users should be reported to the satellite.\r\n*yes* = log with user ID\r\n*anonymous* = anonymous logging\r\n*no* = no logging",
"SLX_ROOT_PASS": "The root password of the client system. Only required for diagnostic purposes on the client.Leave field blank to disallow root logins.\r\n\/Hint\/: The password SHA-512-with-salt hashed before it's being sent to the client. It's only stored in clear text on the Satellite Server. If you want to have it hashed on the server too, you can supply a pre-hashed passoword in \/$6$...$...\/-format.",
"SLX_SHUTDOWN_SCHEDULE": "Fixed time to turn off the computer, even if there is a user active.Several times can be specified, separated by spaces.",
- "SLX_SHUTDOWN_TIMEOUT": "Time in seconds after which a computer is switched off, if no user is logged on.Leave blank to disable the function."
+ "SLX_SHUTDOWN_TIMEOUT": "Time in seconds after which a computer is switched off, if no user is logged on.Leave blank to disable the function.",
+ "SLX_VMCHOOSER_FORLOCATION": "Defines how lectures special to the user's location are handled in the vmchooser.\r\n*IGNORE*: Sort them alphabetically among the global lectures.\r\n*BUMP*: Put them atop the global lectures.\r\n*EXCLUSIVE*: Put them atop the global lectures and aditionally collapse the node which contains the global lectures.",
+ "SLX_VMCHOOSER_TAB": "Defines which tab is show by default, if the user doesn't have stored a last used session on his persistent home directory.\r\n*0*: Native Linux sessions\r\n*1*: User specific lectures\r\n*2*: All lectures\r\n*AUTO*: If the computer has low system specs, show the Linux sessions, otherwise, show all lectures",
+ "SLX_VMCHOOSER_TEMPLATES": "Defines how lectures that link to template VMs are treated wrt sorting.\r\n*IGNORE*: Sort among regular lectures\r\n*BUMP*: Move to top of list"
} \ No newline at end of file
diff --git a/lang/en/templates/locations/location-subnets.json b/lang/en/templates/locations/location-subnets.json
new file mode 100644
index 00000000..2ba94384
--- /dev/null
+++ b/lang/en/templates/locations/location-subnets.json
@@ -0,0 +1,18 @@
+{
+ "lang_addNewSubnet": "Add new subnet",
+ "lang_assignSubnetExplanation": "Client machines which fall into an IP range listed below will be assigned to this location and will see an according lecture list (e.g. they will see lectures that are exclusively assigned to this location).",
+ "lang_assignedSubnets": "Assigned subnets \/ IP ranges",
+ "lang_deleteChildLocations": "Delete child locations aswell",
+ "lang_deleteLocation": "Delete location",
+ "lang_deleteSubnet": "Delete range",
+ "lang_endAddress": "End address",
+ "lang_locationInfo": "Location details",
+ "lang_locationSettings": "Edit this room or location",
+ "lang_matchingMachines": "Matching clients",
+ "lang_name": "Name",
+ "lang_parentLocation": "Parent location",
+ "lang_referencingLectures": "Assigned Lectures",
+ "lang_save": "Save",
+ "lang_startAddress": "Start address",
+ "lang_subnet": "IP range"
+} \ No newline at end of file
diff --git a/lang/en/templates/locations/locations.json b/lang/en/templates/locations/locations.json
new file mode 100644
index 00000000..db4fd0a7
--- /dev/null
+++ b/lang/en/templates/locations/locations.json
@@ -0,0 +1,10 @@
+{
+ "lang_areYouSureNoUndo": "Are you sure? This cannot be undone!",
+ "lang_edit": "Edit",
+ "lang_location": "Ort",
+ "lang_locationName": "Name",
+ "lang_locationsMainHeading": "Manage rooms and locations",
+ "lang_noParent": "No parent",
+ "lang_save": "Save",
+ "lang_thisListBySubnet": "List by subnet"
+} \ No newline at end of file
diff --git a/lang/en/templates/locations/subnets.json b/lang/en/templates/locations/subnets.json
new file mode 100644
index 00000000..65da254b
--- /dev/null
+++ b/lang/en/templates/locations/subnets.json
@@ -0,0 +1,7 @@
+{
+ "lang_endAddress": "End",
+ "lang_listOfSubnets": "List of subnets",
+ "lang_location": "Location",
+ "lang_startAddress": "Start",
+ "lang_thisListByLocation": "List by location"
+} \ No newline at end of file
diff --git a/lang/en/templates/main-menu.json b/lang/en/templates/main-menu.json
index be9cefed..d22f90d7 100644
--- a/lang/en/templates/main-menu.json
+++ b/lang/en/templates/main-menu.json
@@ -10,6 +10,7 @@
"lang_internetAccess": "Internet access",
"lang_language": "Language",
"lang_localization": "Localization",
+ "lang_locations": "Rooms\/Locations",
"lang_login": "Login",
"lang_logout": "Logout",
"lang_needsSetup": "Setup incomplete",
diff --git a/lang/pt/settings/cat_setting.json b/lang/pt/settings/cat_setting.json
index 772575fc..b04839e8 100644
--- a/lang/pt/settings/cat_setting.json
+++ b/lang/pt/settings/cat_setting.json
@@ -2,5 +2,6 @@
"cat_1": "Inatividade e Desligamento",
"cat_2": "Acesso \u00e0 Internet",
"cat_3": "Sincroniza\u00e7\u00e3o de Tempo",
- "cat_4": "Sistema B\u00e1sico"
+ "cat_4": "Sistema B\u00e1sico",
+ "cat_7": "vmchooser"
} \ No newline at end of file
diff --git a/lang/pt/templates/locations/location-subnets.json b/lang/pt/templates/locations/location-subnets.json
new file mode 100644
index 00000000..c44dc44f
--- /dev/null
+++ b/lang/pt/templates/locations/location-subnets.json
@@ -0,0 +1,3 @@
+[
+
+] \ No newline at end of file
diff --git a/lang/pt/templates/locations/locations.json b/lang/pt/templates/locations/locations.json
new file mode 100644
index 00000000..c44dc44f
--- /dev/null
+++ b/lang/pt/templates/locations/locations.json
@@ -0,0 +1,3 @@
+[
+
+] \ No newline at end of file
diff --git a/lang/pt/templates/locations/subnets.json b/lang/pt/templates/locations/subnets.json
new file mode 100644
index 00000000..c44dc44f
--- /dev/null
+++ b/lang/pt/templates/locations/subnets.json
@@ -0,0 +1,3 @@
+[
+
+] \ No newline at end of file
diff --git a/modules/locations.inc.php b/modules/locations.inc.php
new file mode 100644
index 00000000..d4d0c85f
--- /dev/null
+++ b/modules/locations.inc.php
@@ -0,0 +1,348 @@
+<?php
+
+class Page_Locations extends Page
+{
+
+ private $action;
+
+ /*
+ * Action handling
+ */
+
+ protected function doPreprocess()
+ {
+ User::load();
+ if (!User::hasPermission('superadmin')) {
+ Message::addError('no-permission');
+ Util::redirect('?do=Main');
+ }
+ $this->action = Request::post('action');
+ if ($this->action === 'updatelocation') {
+ $this->updateLocation();
+ } elseif ($this->action === 'addlocations') {
+ $this->addLocations();
+ }
+ }
+
+ private function addLocations()
+ {
+ $names = Request::post('newlocation', false);
+ $parents = Request::post('newparent', false);
+ if (!is_array($names) || !is_array($parents)) {
+ Message::addError('empty-field');
+ Util::redirect('?do=Locations');
+ }
+ $locs = Location::getLocations();
+ $count = 0;
+ foreach ($names as $idx => $name) {
+ $name = trim($name);
+ if (empty($name)) continue;
+ $parent = isset($parents[$idx]) ? (int)$parents[$idx] : 0;
+ if ($parent !== 0) {
+ $ok = false;
+ foreach ($locs as $loc) {
+ if ($loc['locationid'] == $parent) {
+ $ok = true;
+ }
+ }
+ if (!$ok) {
+ Message::addWarning('value-invalid', 'parentlocationid', $parent);
+ continue;
+ }
+ }
+ Database::exec("INSERT INTO location (parentlocationid, locationname)"
+ . " VALUES (:parent, :name)", array(
+ 'parent' => $parent,
+ 'name' => $name
+ ));
+ $count++;
+ }
+ Message::addSuccess('added-x-entries', $count);
+ Util::redirect('?do=Locations');
+ }
+
+ private function updateLocation()
+ {
+ $locationId = Request::post('locationid', false, 'integer');
+ $del = Request::post('deletelocation', false, 'integer');
+ if ($locationId === false) {
+ Message::addError('parameter-missing', 'locationid');
+ Util::redirect('?do=Locations');
+ }
+ $location = Database::queryFirst('SELECT locationid, parentlocationid, locationname FROM location'
+ . ' WHERE locationid = :lid', array('lid' => $locationId));
+ if ($location === false) {
+ Message::addError('value-invalid', 'locationid', $locationId);
+ Util::redirect('?do=Locations');
+ }
+ // Delete location?
+ if ($locationId === $del) {
+ $this->deleteLocation($location);
+ }
+ // Update subnets
+ $this->updateLocationSubnets($location);
+ // Insert subnets
+ $this->addNewLocationSubnets($location); // TODO
+ // Update location!
+ $this->updateLocationData($location);
+ Util::redirect('?do=Locations');
+ }
+
+ private function deleteLocation($location)
+ {
+ $locationId = (int)$location['locationid'];
+ $ids = $locationId;
+ if (Request::post('recursive', false) === 'on') {
+ $rows = Location::queryLocations();
+ $rows = Location::buildTree($rows, $locationId);
+ $rows = Location::extractIds($rows);
+ if (!empty($rows)) {
+ $ids .= ',' . implode(',', $rows);
+ }
+ }
+ $subs = Database::exec("DELETE FROM subnet WHERE locationid IN ($ids)");
+ $locs = Database::exec("DELETE FROM location WHERE locationid IN ($ids)");
+ Database::exec('UPDATE location SET parentlocationid = :newparent WHERE parentlocationid = :oldparent', array(
+ 'newparent' => $location['parentlocationid'],
+ 'oldparent' => $location['locationid']
+ ));
+ Message::addSuccess('location-deleted', $locs, $subs);
+ Util::redirect('?do=Locations');
+ }
+
+ private function updateLocationData($location)
+ {
+ $locationId = (int)$location['locationid'];
+ $newParent = Request::post('parentlocationid', false, 'integer');
+ $newName = Request::post('locationname', false, 'string');
+ if ($newName === false || preg_match('/^\s*$/', $newName)) {
+ if ($newName !== false) {
+ Message::addWarning('value-invalid', 'location name', $newName);
+ }
+ $newName = $location['locationname'];
+ }
+ if ($newParent === false) {
+ $newParent = $location['parentlocationid'];
+ } else if ($newParent !== 0) {
+ $rows = Location::queryLocations();
+ $all = Location::extractIds(Location::buildTree($rows));
+ if (!in_array($newParent, $all) || $newParent === $locationId) {
+ Message::addWarning('value-invalid', 'parent', $newParent);
+ $newParent = $location['parentlocationid'];
+ } else {
+ $rows = Location::extractIds(Location::buildTree($rows, $locationId));
+ if (in_array($newParent, $rows)) {
+ Message::addWarning('value-invalid', 'parent', $newParent);
+ $newParent = $location['parentlocationid'];
+ }
+ }
+ }
+ $ret = Database::exec('UPDATE location SET parentlocationid = :parent, locationname = :name'
+ . ' WHERE locationid = :lid', array(
+ 'lid' => $locationId,
+ 'parent' => $newParent,
+ 'name' => $newName
+ ));
+ if ($ret > 0) {
+ Message::addSuccess('location-updated', $newName);
+ }
+ }
+
+ private function updateLocationSubnets($location)
+ {
+ $locationId = (int)$location['locationid'];
+ // Deletion first
+ $dels = Request::post('deletesubnet', false);
+ if (is_array($dels)) {
+ $count = 0;
+ $stmt = Database::prepare('DELETE FROM subnet WHERE subnetid = :id');
+ foreach ($dels as $key => $value) {
+ if (!is_numeric($key) || $value !== 'on') continue;
+ if ($stmt->execute(array('id' => $key))) {
+ $count += $stmt->rowCount();
+ }
+ }
+ if ($count > 0) {
+ Message::addInfo('subnets-deleted', $count);
+ }
+ }
+ // Now actual updates
+ // TODO: Warn on mismatch/overlap (should lie entirely in parent's subnet, not overlap with others)
+ $starts = Request::post('startaddr', false);
+ $ends = Request::post('endaddr', false);
+ if (!is_array($starts) || !is_array($ends)) {
+ return;
+ }
+ $count = 0;
+ $stmt = Database::prepare('UPDATE subnet SET startaddr = :start, endaddr = :end'
+ . ' WHERE subnetid = :id');
+ foreach ($starts as $key => $start) {
+ if (!isset($ends[$key]) || !is_numeric($key)) continue;
+ $end = $ends[$key];
+ list($startLong, $endLong) = $this->rangeToLong($start, $end);
+ if ($startLong === false) {
+ Message::addWarning('value-invalid', 'start addr', $start);
+ }
+ if ($endLong === false) {
+ Message::addWarning('value-invalid', 'end addr', $start);
+ }
+ if ($startLong === false || $endLong === false) continue;
+ if ($startLong > $endLong) {
+ Message::addWarning('value-invalid', 'range', $start . ' - ' . $end);
+ continue;
+ }
+ if ($stmt->execute(array('id' => $key, 'start' => $startLong, 'end' => $endLong))) {
+ $count += $stmt->rowCount();
+ }
+ }
+ if ($count > 0) {
+ Message::addInfo('subnets-updated', $count);
+ }
+ }
+
+ private function addNewLocationSubnets($location)
+ {
+ $locationId = (int)$location['locationid'];
+ $starts = Request::post('newstartaddr', false);
+ $ends = Request::post('newendaddr', false);
+ if (!is_array($starts) || !is_array($ends)) {
+ return;
+ }
+ $count = 0;
+ $stmt = Database::prepare('INSERT INTO subnet SET startaddr = :start, endaddr = :end, locationid = :location');
+ foreach ($starts as $key => $start) {
+ if (!isset($ends[$key]) || !is_numeric($key)) continue;
+ $end = $ends[$key];
+ list($startLong, $endLong) = $this->rangeToLong($start, $end);
+ if ($startLong === false) {
+ Message::addWarning('value-invalid', 'new start addr', $start);
+ }
+ if ($endLong === false) {
+ Message::addWarning('value-invalid', 'new end addr', $start);
+ }
+ if ($startLong === false || $endLong === false) continue;
+ if ($startLong > $endLong) {
+ Message::addWarning('value-invalid', 'range', $start . ' - ' . $end);
+ continue;
+ }
+ if ($stmt->execute(array('location' => $locationId, 'start' => $startLong, 'end' => $endLong))) {
+ $count += $stmt->rowCount();
+ }
+ }
+ if ($count > 0) {
+ Message::addInfo('subnets-created', $count);
+ }
+ }
+
+ /*
+ * Rendering normal pages
+ */
+
+ protected function doRender()
+ {
+ //Render::setTitle(Dictionary::translate('lang_titleBackup'));
+ $getAction = Request::get('action');
+ if (empty($getAction)) {
+ // Until we have a main landing page?
+ Util::redirect('?do=Locations&action=showlocations');
+ }
+ if ($getAction === 'showsubnets') {
+ $res = Database::simpleQuery("SELECT subnetid, startaddr, endaddr, locationid FROM subnet");
+ $rows = array();
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ $row['startaddr'] = long2ip($row['startaddr']);
+ $row['endaddr'] = long2ip($row['endaddr']);
+ $row['locations'] = Location::getLocations($row['locationid']);
+ $rows[] = $row;
+ }
+ Render::addTemplate('locations/subnets', array('list' => $rows));
+ } elseif ($getAction === 'showlocations') {
+ $locs = Location::getLocations();
+ Render::addTemplate('locations/locations', array('list' => $locs));
+ }
+ }
+
+ /*
+ * Ajax
+ */
+
+ protected function doAjax()
+ {
+ User::load();
+ if (!User::isLoggedIn()) {
+ die('Unauthorized');
+ }
+ $action = Request::any('action');
+ if ($action === 'showlocation') {
+ $this->ajaxShowLocation();
+ }
+ }
+
+ private function ajaxShowLocation()
+ {
+ $locationId = Request::any('locationid', 0, 'integer');
+ $loc = Database::queryFirst('SELECT locationid, parentlocationid, locationname FROM location WHERE locationid = :lid',
+ array('lid' => $locationId));
+ if ($loc === false) {
+ die('Unknown locationid');
+ }
+ $res = Database::simpleQuery("SELECT subnetid, startaddr, endaddr FROM subnet WHERE locationid = :lid",
+ array('lid' => $locationId));
+ $rows = array();
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ $row['startaddr'] = long2ip($row['startaddr']);
+ $row['endaddr'] = long2ip($row['endaddr']);
+ $rows[] = $row;
+ }
+ $data = array(
+ 'locationid' => $loc['locationid'],
+ 'locationname' => $loc['locationname'],
+ 'list' => $rows,
+ 'parents' => Location::getLocations($loc['parentlocationid'], $locationId, true)
+ );
+ // if (moduleEnabled(DOZMOD) {
+ $lectures = Database::queryFirst('SELECT Count(*) AS cnt FROM sat.lecture l '
+ . ' INNER JOIN sat.lecture_x_location ll ON (l.lectureid = ll.lectureid AND ll.locationid = :lid)',
+ array('lid' => $locationId));
+ $data['lectures'] = $lectures['cnt'];
+ // }
+ // Get clients matching this location's subnet(s)
+ $mres = Database::simpleQuery("SELECT lastseen, logintime FROM machine"
+ . " INNER JOIN subnet ON (INET_ATON(machine.clientip) BETWEEN startaddr AND endaddr)"
+ . " WHERE subnet.locationid = :lid OR machine.locationid = :lid", array('lid' => $locationId));
+ $count = $online = $used = 0;
+ $DL = time() - 605;
+ while ($row = $mres->fetch(PDO::FETCH_ASSOC)) {
+ $count++;
+ if ($row['lastseen'] > $DL) {
+ $online++;
+ if ($row['logintime'] != 0) {
+ $used++;
+ }
+ }
+ }
+ $data['machines'] = $count;
+ $data['machines_online'] = $online;
+ $data['machines_used'] = $used;
+ $data['used_percent'] = round(100 * $used / $online);
+ echo Render::parse('locations/location-subnets', $data);
+ }
+
+ /*
+ * Helpers
+ */
+
+ private function rangeToLong($start, $end)
+ {
+ $startLong = ip2long($start);
+ $endLong = ip2long($end);
+ if ($startLong !== false) {
+ $startLong = sprintf("%u", $startLong);
+ }
+ if ($endLong !== false) {
+ $endLong = sprintf("%u", $endLong);
+ }
+ return array($startLong, $endLong);
+ }
+
+}
diff --git a/style/default.css b/style/default.css
index 73e4effa..ebbe3618 100644
--- a/style/default.css
+++ b/style/default.css
@@ -232,3 +232,13 @@ input[readonly] {
overflow: hidden;
}
+.slx-well {
+ border: 1px solid #E3E3E3;
+ border-top: none;
+ box-shadow: rgba(0,0,0,0.047) 0px 1px 1px 0px inset;
+ border-radius: 4px;
+ border-top-left-radius: 0px;
+ border-top-right-radius: 0px;
+ margin: 0px;
+ padding: 19px;
+} \ No newline at end of file
diff --git a/templates/locations/location-subnets.html b/templates/locations/location-subnets.html
new file mode 100644
index 00000000..76b7442a
--- /dev/null
+++ b/templates/locations/location-subnets.html
@@ -0,0 +1,73 @@
+<div class="slx-well">
+ <div class="slx-bold">{{lang_locationSettings}}</div>
+ <form method="post" action="?do=Locations">
+ <input type="hidden" name="token" value="{{token}}">
+ <input type="hidden" name="action" value="updatelocation">
+ <input type="hidden" name="locationid" value="{{locationid}}">
+ <div style="display:none">
+ <button type="submit" class="btn btn-primary">Save</button>
+ </div>
+ <div class="row">
+ <div class="col-sm-6">
+ <div class="input-group">
+ <span class="input-group-addon slx-ga2">{{lang_name}}</span>
+ <input class="form-control" type="text" name="locationname" value="{{locationname}}" pattern=".*\S.*">
+ </div>
+ </div>
+ <div class="col-sm-6">
+ <div class="input-group">
+ <span class="input-group-addon slx-ga2">{{lang_parentLocation}}</span>
+ <select class="form-control" name="parentlocationid">
+ {{#parents}}
+ <option value="{{locationid}}" {{#selected}}selected="selected"{{/selected}}>{{locationpad}} {{locationname}}</option>
+ {{/parents}}
+ </select>
+ </div>
+ </div>
+ </div>
+ <div>
+ <div class="pull-right">
+ <label><input type="checkbox" name="recursive" value="on"> {{lang_deleteChildLocations}}</label>
+ <button type="submit" class="btn btn-sm btn-danger" name="deletelocation" value="{{locationid}}" onclick="return slxConfirm()">{{lang_deleteLocation}}</button>
+ </div>
+ <div class="clearfix"></div>
+ </div>
+ <br>
+ <div class="slx-bold">{{lang_assignedSubnets}}</div>
+ <div><i>{{lang_assignSubnetExplanation}}</i></div>
+ <table class="table table-condensed table-striped">
+ <tr>
+ <th>#</th>
+ <th>{{lang_startAddress}}</th>
+ <th>{{lang_endAddress}}</th>
+ <th title="{{lang_deleteSubnet}}"><span class="glyphicon glyphicon-trash"></span></th>
+ </tr>
+ {{#list}}
+ <tr>
+ <td>{{subnetid}}</td>
+ <td><input class="form-control" type="text" name="startaddr[{{subnetid}}]" value="{{startaddr}}" pattern="\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"></td>
+ <td><input class="form-control" type="text" name="endaddr[{{subnetid}}]" value="{{endaddr}}" pattern="\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"></td>
+ <td class="danger" align="center"><input type="checkbox" name="deletesubnet[{{subnetid}}]" value="on"></td>
+ </tr>
+ {{/list}}
+ <tr id="loc-sub-{{locationid}}">
+ <td colspan="2">
+ <button class="btn btn-success btn-sm" type="button" onclick="slxAddSubnetRow(this, {{locationid}})" title="{{lang_addNewSubnet}}">
+ <span class="glyphicon glyphicon-plus-sign"></span> {{lang_subnet}}
+ </button>
+ </td>
+ <td colspan="2" align="right">
+ <button type="submit" class="btn btn-primary">{{lang_save}}</button>
+ </div>
+ </tr>
+ </table>
+ </form>
+ <br>
+ <div class="slx-bold">{{lang_locationInfo}}</div>
+ <div>
+ <span class="slx-ga2">{{lang_referencingLectures}}:</span> {{lectures}}
+ </div>
+ <div>
+ <span class="slx-ga2">{{lang_matchingMachines}}:</span> <a href="?do=Statistics&amp;filter=location&amp;argument={{locationid}}">{{machines}} / {{machines_online}} / {{machines_used}} ({{used_percent}}%)</a>
+ </div>
+</div> \ No newline at end of file
diff --git a/templates/locations/locations.html b/templates/locations/locations.html
new file mode 100644
index 00000000..76c8f97c
--- /dev/null
+++ b/templates/locations/locations.html
@@ -0,0 +1,96 @@
+<div>
+ <div class="pull-right">
+ <a href="?do=Locations&amp;action=showsubnets">{{lang_thisListBySubnet}}</a>
+ </div>
+ <h1>{{lang_locationsMainHeading}}</h1>
+ <table class="table table-condensed" style="margin-bottom:0px">
+ <tr>
+ <th>#</th>
+ <th width="100%">{{lang_locationName}}</th>
+ <th></th>
+ </tr>
+ {{#list}}
+ <tr>
+ <td>{{locationid}}</td>
+ <td><div style="display:inline-block;width:{{depth}}em"></div>{{locationname}}</td>
+ <td align="right">
+ <a class="btn btn-success btn-xs" onclick="slxOpenLocation(this, {{locationid}})"><span class="glyphicon glyphicon-edit"></span> {{lang_edit}}</a>
+ </td>
+ </tr>
+ {{/list}}
+ </table>
+ <form method="post" action="?do=Locations">
+ <input type="hidden" name="token" value="{{token}}">
+ <input type="hidden" name="action" value="addlocations">
+ <table class="table table-condensed table-hover">
+ <tr id="lasttr">
+ <td>
+ <button class="btn btn-success btn-sm" type="button" onclick="slxAddLocationRow()">
+ <span class="glyphicon glyphicon-plus-sign"></span> {{lang_location}}
+ </button>
+ </td>
+ <td width="80%">&emsp;</td>
+ <td width="20%" align="right">
+ <button type="submit" class="btn btn-primary">{{lang_save}}</button>
+ </td>
+ </tr>
+ </table>
+ </form>
+</div>
+<script type="text/javascript"><!--
+var slxAddCounter = 0;
+var slxLastLocation = false;
+
+function slxAddLocationRow() {
+ var tr = $('#lasttr');
+ tr.before('<tr>\
+ <td>#</td>\
+ <td><input class="form-control" type="text" name="newlocation[' + slxAddCounter + ']" placeholder="{{lang_locationName}}" pattern=".*\\S.*"></td>\
+ <td><select class="form-control" name="newparent[' + slxAddCounter + ']">\
+ <option value="0">{{lang_noParent}}</option>\
+ {{#list}}<option value="{{locationid}}">{{locationpad}} {{locationname}}</option>{{/list}}\
+ </select></td>\
+ </tr>');
+ slxAddCounter++;
+}
+
+function slxOpenLocation(e, lid) {
+ if (slxLastLocation !== false) {
+ slxLastLocation.hide();
+ $(slxLastLocation).prev().removeClass('active slx-bold');
+ }
+ var existing = $('#location-details-' + lid);
+ if (existing.length > 0) {
+ if (existing.is(slxLastLocation)) {
+ slxLastLocation = false;
+ } else {
+ existing.show();
+ $(e).closest('tr').addClass('active slx-bold');
+ slxLastLocation = existing;
+ }
+ return;
+ }
+ var td = $('<td>').attr('colspan', '12').css('padding', '0px 0px 12px');
+ var tr = $('<tr>').attr('id', 'location-details-' + lid);
+ tr.append(td);
+ $(e).closest('tr').addClass('active slx-bold').after(tr);
+ td.load('?do=Locations&action=showlocation&locationid=' + lid);
+ slxLastLocation = tr;
+}
+
+function slxAddSubnetRow(e, lid) {
+ var tr = $('#loc-sub-' + lid);
+ tr.before('<tr>\
+ <td>#</td>\
+ <td><input class="form-control" type="text" name="newstartaddr[' + slxAddCounter + ']" pattern="\\d{1,3}\.\\d{1,3}\.\\d{1,3}\.\\d{1,3}"></td>\
+ <td><input class="form-control" type="text" name="newendaddr[' + slxAddCounter + ']" pattern="\\d{1,3}\.\\d{1,3}\.\\d{1,3}\.\\d{1,3}"></td>\
+ <td></td>\
+ </tr>');
+ slxAddCounter++;
+}
+
+function slxConfirm() {
+ return confirm('{{lang_areYouSureNoUndo}}');
+}
+ // -->
+</script>
diff --git a/templates/locations/subnets.html b/templates/locations/subnets.html
new file mode 100644
index 00000000..2294f42b
--- /dev/null
+++ b/templates/locations/subnets.html
@@ -0,0 +1,35 @@
+<div>
+ <div class="pull-right">
+ <a href="?do=Locations&amp;action=showlocations">{{lang_thisListByLocation}}</a>
+ </div>
+ <h1>{{lang_listOfSubnets}}</h1>
+ <form method="post" action="?do=Locations">
+ <input type="hidden" name="token" value="{{token}}">
+ <input type="hidden" name="action" value="updatesubnets">
+ <table class="table table-condensed table-striped">
+ <tr>
+ <th>#</th>
+ <th>{{lang_startAddress}}</th>
+ <th>{{lang_endAddress}}</th>
+ <th>{{lang_location}}</th>
+ </tr>
+ {{#list}}
+ <tr>
+ <td>{{subnetid}}</td>
+ <td><input class="form-control" type="text" name="startaddr[{{subnetid}}]" value="{{startaddr}}"></td>
+ <td><input class="form-control" type="text" name="endaddr[{{subnetid}}]" value="{{endaddr}}"></td>
+ <td>
+ <select class="form-control" name="location[{{subnetid}}]">
+ {{#locations}}
+ <option value="{{locationid}}" {{#selected}}selected="selected"{{/selected}}>{{locationpad}} {{locationname}}</option>
+ {{/locations}}
+ </select>
+ </td>
+ </tr>
+ {{/list}}
+ </table>
+ <div>
+ <button type="submit" class="btn btn-primary">Späschohn (geht noch nicht!)</button>
+ </div>
+ </form>
+</div>
diff --git a/templates/main-menu.html b/templates/main-menu.html
index 3e3a1d90..7852dd39 100644
--- a/templates/main-menu.html
+++ b/templates/main-menu.html
@@ -19,6 +19,7 @@
<li><a href="?do=SysConfig">{{lang_localization}}</a></li>
<li><a href="?do=MiniLinux">bwLehrpool Mini-Linux</a></li>
<li><a href="?do=BaseConfig">{{lang_configurationVariables}}</a></li>
+ <li><a href="?do=Locations">{{lang_locations}}</a></li>
<li class="divider"></li>
<li class="dropdown-header">{{lang_server}}</li>
<li><a href="?do=ServerSetup">{{lang_configurationBasic}}</a></li>