From 2aa709e968482756c0343dbecf079913cd16ba52 Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Mon, 20 Mar 2017 21:30:33 +0100 Subject: [webinterface] Remember last HTTPS config; add redirect to HTTPS setting Functionality in the LighttpdHttps task is still missing, so the new redirect setting doesn't really do anything yet. This refs #3058 @2h --- .../webinterface/lang/de/messages.json | 6 ++ .../webinterface/lang/de/template-tags.json | 10 ++- .../webinterface/lang/en/messages.json | 6 ++ .../webinterface/lang/en/template-tags.json | 12 ++- modules-available/webinterface/page.inc.php | 87 +++++++++++++++++++++- .../webinterface/templates/https.html | 32 +++++++- .../webinterface/templates/passwords.html | 1 + 7 files changed, 142 insertions(+), 12 deletions(-) create mode 100644 modules-available/webinterface/lang/de/messages.json create mode 100644 modules-available/webinterface/lang/en/messages.json (limited to 'modules-available') 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..cdf2b920 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_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..35e14dc5 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(); @@ -33,13 +36,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'); } - + private function actionShowHidePassword() { Property::setPasswordFieldType(Request::post('mode') === 'show' ? 'text' : 'password'); @@ -48,10 +55,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 +116,48 @@ class Page_WebInterface extends Page private function setHttpsOff() { + Property::set(self::PROP_TYPE, 'off'); 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/https.html b/modules-available/webinterface/templates/https.html index dfd2a3fe..294abe49 100644 --- a/modules-available/webinterface/templates/https.html +++ b/modules-available/webinterface/templates/https.html @@ -5,9 +5,24 @@
{{lang_httpsSettings}}

{{lang_httpsDescription}}

- {{^httpsEnabled}} -

{{lang_HttpsIsDisabled}}

- {{/httpsEnabled}} + {{^httpsUsed}} + {{lang_youreNotUsingHttps}} + {{/httpsUsed}} + {{#httpsUsed}} + {{lang_youreUsingHttps}} + {{/httpsUsed}} + {{#offSelected}} +

{{lang_offSelected}}

+ {{/offSelected}} + {{#unknownSelected}} +

{{lang_unknownSelected}}

+ {{/unknownSelected}} + {{#generatedSelected}} +

{{lang_generatedSelected}}

+ {{/generatedSelected}} + {{#suppliedSelected}} +

{{lang_suppliedSelected}}

+ {{/suppliedSelected}} {{#httpsEnabled}}
@@ -31,6 +46,7 @@ {{lang_customCert}}
+ + +
+
+ + + {{lang_httpsRedirect}} + +
+
+
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}}
+
-- cgit v1.2.3-55-g7522 From c415a9374872c6235fe822a5e038546522817e3a Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Tue, 21 Mar 2017 12:18:47 +0100 Subject: Fix a couple of warnings and notices from error.log --- apis/cron.inc.php | 2 +- inc/property.inc.php | 8 ++++---- inc/util.inc.php | 2 +- modules-available/serversetup-bwlp/page.inc.php | 9 +++++++-- modules-available/statistics_reporting/inc/remotereport.inc.php | 8 ++++---- 5 files changed, 17 insertions(+), 12 deletions(-) (limited to 'modules-available') diff --git a/apis/cron.inc.php b/apis/cron.inc.php index a0042e61..0bcd2490 100644 --- a/apis/cron.inc.php +++ b/apis/cron.inc.php @@ -50,7 +50,7 @@ foreach (Hook::load('cron') as $hook) { continue; } } - $value = $hook . '|' . time(); + $value = $hook->moduleId . '|' . time(); Property::addToList(CRON_KEY_STATUS, $value, 1800); handleModule($hook->file); Property::removeFromList(CRON_KEY_STATUS, $value); 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..f5e10ebc 100644 --- a/inc/util.inc.php +++ b/inc/util.inc.php @@ -375,7 +375,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/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_reporting/inc/remotereport.inc.php b/modules-available/statistics_reporting/inc/remotereport.inc.php index 7aad8b3a..a2234849 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,14 +63,14 @@ 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); -- cgit v1.2.3-55-g7522 From 09d0d4957f0013c0b83843ccf5e254d0e395518e Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Tue, 21 Mar 2017 14:42:11 +0100 Subject: [statistics_reporting] cron: Write message to event log on successful submit --- modules-available/statistics_reporting/hooks/cron.inc.php | 2 ++ 1 file changed, 2 insertions(+) (limited to 'modules-available') diff --git a/modules-available/statistics_reporting/hooks/cron.inc.php b/modules-available/statistics_reporting/hooks/cron.inc.php index a48f74c2..45f39719 100644 --- a/modules-available/statistics_reporting/hooks/cron.inc.php +++ b/modules-available/statistics_reporting/hooks/cron.inc.php @@ -18,6 +18,8 @@ if (RemoteReport::isReportingEnabled()) { if ($code != 200) { EventLog::warning("Statistics Reporting failed: " . $code, $result); + } else { + EventLog::info('Statistics report sent to ' . CONFIG_REPORTING_URL); } } } \ No newline at end of file -- cgit v1.2.3-55-g7522 From 2b47f58833aa431c6c211483ab5136798b66ec2a Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Thu, 23 Mar 2017 13:58:19 +0100 Subject: [sysconfig] Add class for reading and writing PPD files --- modules-available/sysconfig/inc/ppd.inc.php | 1162 +++++++++++++++++++++++++++ 1 file changed, 1162 insertions(+) create mode 100644 modules-available/sysconfig/inc/ppd.inc.php (limited to 'modules-available') 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 @@ + '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: 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: '"); + } 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/SubGroup + */ + 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; + } + +} -- cgit v1.2.3-55-g7522 From 3747e24cbead81527ddee8aa455f8d3133cb429c Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Tue, 28 Mar 2017 13:51:16 +0200 Subject: [roomplanner] Add "fit to content" zoom button Implements #2965 --- modules-available/roomplanner/js/grid.js | 58 +++++++++++++++++++---- modules-available/roomplanner/js/init.js | 4 ++ modules-available/roomplanner/style.css | 7 ++- modules-available/roomplanner/templates/page.html | 5 +- 4 files changed, 61 insertions(+), 13 deletions(-) (limited to 'modules-available') 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 @@
+ +
+
@@ -311,7 +314,7 @@
-
+
{{lang_managerIp}}
-- cgit v1.2.3-55-g7522 From da8b3748640560893e00f1004fb8d32a27f908db Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Tue, 28 Mar 2017 14:03:41 +0200 Subject: [sysconfig] Fix broken title "{{config-module}}" Fixes #3074 --- modules-available/sysconfig/addmodule_branding.inc.php | 2 +- modules-available/sysconfig/addmodule_custommodule.inc.php | 2 +- modules-available/sysconfig/addmodule_sshconfig.inc.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) (limited to 'modules-available') 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', )); } -- cgit v1.2.3-55-g7522 From e12ac023773dfa9b95afcc11afdeb18e0a5785cf Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Wed, 29 Mar 2017 11:00:48 +0200 Subject: [webinterface] Make line reporting current setting more visible --- .../webinterface/lang/en/template-tags.json | 2 +- .../webinterface/templates/https.html | 26 ++++++++++++---------- 2 files changed, 15 insertions(+), 13 deletions(-) (limited to 'modules-available') diff --git a/modules-available/webinterface/lang/en/template-tags.json b/modules-available/webinterface/lang/en/template-tags.json index cdf2b920..efe649cb 100644 --- a/modules-available/webinterface/lang/en/template-tags.json +++ b/modules-available/webinterface/lang/en/template-tags.json @@ -5,7 +5,7 @@ "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", diff --git a/modules-available/webinterface/templates/https.html b/modules-available/webinterface/templates/https.html index 294abe49..77585ddf 100644 --- a/modules-available/webinterface/templates/https.html +++ b/modules-available/webinterface/templates/https.html @@ -11,18 +11,20 @@ {{#httpsUsed}} {{lang_youreUsingHttps}} {{/httpsUsed}} - {{#offSelected}} -

{{lang_offSelected}}

- {{/offSelected}} - {{#unknownSelected}} -

{{lang_unknownSelected}}

- {{/unknownSelected}} - {{#generatedSelected}} -

{{lang_generatedSelected}}

- {{/generatedSelected}} - {{#suppliedSelected}} -

{{lang_suppliedSelected}}

- {{/suppliedSelected}} +
+ {{#offSelected}} +

{{lang_offSelected}}

+ {{/offSelected}} + {{#unknownSelected}} +

{{lang_unknownSelected}}

+ {{/unknownSelected}} + {{#generatedSelected}} +

{{lang_generatedSelected}}

+ {{/generatedSelected}} + {{#suppliedSelected}} +

{{lang_suppliedSelected}}

+ {{/suppliedSelected}} +
{{#httpsEnabled}}
-- cgit v1.2.3-55-g7522 From 70487200af1582c9b5e59e9eb644dc5c9ddb0812 Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Wed, 29 Mar 2017 13:17:18 +0200 Subject: [statistics] Fetch client log by UUID, not IP --- modules-available/statistics/page.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'modules-available') diff --git a/modules-available/statistics/page.inc.php b/modules-available/statistics/page.inc.php index 6a9acd14..87c8e7c4 100644 --- a/modules-available/statistics/page.inc.php +++ b/modules-available/statistics/page.inc.php @@ -806,7 +806,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; -- cgit v1.2.3-55-g7522 From 134a933f87b82d49b741b987d67de0eb44c903c6 Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Wed, 29 Mar 2017 13:22:08 +0200 Subject: [webinterface] Try to redirect back to HTTP when HTTPS gets disabled --- modules-available/webinterface/page.inc.php | 5 ++- .../webinterface/templates/httpd-restart.html | 38 +++++++++++++++++++++- 2 files changed, 41 insertions(+), 2 deletions(-) (limited to 'modules-available') diff --git a/modules-available/webinterface/page.inc.php b/modules-available/webinterface/page.inc.php index 35e14dc5..93d659f0 100644 --- a/modules-available/webinterface/page.inc.php +++ b/modules-available/webinterface/page.inc.php @@ -26,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(); @@ -42,7 +44,7 @@ class Page_WebInterface extends Page } 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'); } @@ -117,6 +119,7 @@ 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()); } 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 @@
{{lang_applyingSettings}}
-
{{lang_installAndRestart}}
+
{{lang_installAndRestart}}
+ \ No newline at end of file -- cgit v1.2.3-55-g7522 From 4fa03f0b32fde4bada013f60bc65ed3f4664075c Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Thu, 30 Mar 2017 12:14:07 +0200 Subject: [exams] Extend "upcoming lectures" from 5 to 30 days; collapse list if > 5 Closes #3099 --- modules-available/exams/page.inc.php | 9 +++++++-- modules-available/exams/templates/page-upcoming-lectures.html | 11 ++++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) (limited to 'modules-available') 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 @@ {{lang_actions}} {{#pending_lectures}} - + {{displayname}}
@@ -30,5 +30,14 @@ {{/pending_lectures}} + {{#decollapse}} + + + + + + + + {{/decollapse}}
\ No newline at end of file -- cgit v1.2.3-55-g7522 From 62b5d166578c227f375c406d1c11c5d81ebb30eb Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Mon, 3 Apr 2017 12:36:17 +0200 Subject: [statistics_reporting] Don't return so many NULL fields from queries --- modules-available/statistics_reporting/inc/queries.inc.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'modules-available') diff --git a/modules-available/statistics_reporting/inc/queries.inc.php b/modules-available/statistics_reporting/inc/queries.inc.php index 3e944c92..bf28a592 100644 --- a/modules-available/statistics_reporting/inc/queries.inc.php +++ b/modules-available/statistics_reporting/inc/queries.inc.php @@ -8,7 +8,7 @@ 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 @@ -30,7 +30,7 @@ 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 -- cgit v1.2.3-55-g7522 From 3cd3abab1a33459d3613fa342cbd8fd9135b1202 Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Mon, 3 Apr 2017 13:51:34 +0200 Subject: [statistics_reporting] Convert NULL to 0 so stupidtable doesn't mess up --- modules-available/statistics_reporting/inc/getdata.inc.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) (limited to 'modules-available') diff --git a/modules-available/statistics_reporting/inc/getdata.inc.php b/modules-available/statistics_reporting/inc/getdata.inc.php index f65ee868..a167c2e5 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 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) -- cgit v1.2.3-55-g7522 From 136ba2c3736f8e5d47ebc4f8b5f44a16da57784c Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Mon, 3 Apr 2017 13:56:34 +0200 Subject: [statistics_reporting] Sort by first sortable column by default --- modules-available/statistics_reporting/templates/columnChooser.html | 2 ++ 1 file changed, 2 insertions(+) (limited to 'modules-available') 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) { -- cgit v1.2.3-55-g7522 From 0b0579fd2fdfd2d1f19e2d6ff51ddc49401da604 Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Mon, 3 Apr 2017 13:58:13 +0200 Subject: [statistics_reporting] Fallback to ip if client has no hostname --- modules-available/statistics_reporting/inc/queries.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'modules-available') diff --git a/modules-available/statistics_reporting/inc/queries.inc.php b/modules-available/statistics_reporting/inc/queries.inc.php index bf28a592..f00138ec 100644 --- a/modules-available/statistics_reporting/inc/queries.inc.php +++ b/modules-available/statistics_reporting/inc/queries.inc.php @@ -15,7 +15,7 @@ class Queries 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 LEFT JOIN location ON machine.locationid = location.locationid -- cgit v1.2.3-55-g7522 From 3a94f3629aada7197caafc696a86eada598648f1 Mon Sep 17 00:00:00 2001 From: Udo Walter Date: Tue, 4 Apr 2017 16:11:03 +0200 Subject: [statistics_reporting] fixed clients/locations without offline-length entries not showing --- modules-available/statistics_reporting/inc/getdata.inc.php | 2 +- modules-available/statistics_reporting/inc/queries.inc.php | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) (limited to 'modules-available') diff --git a/modules-available/statistics_reporting/inc/getdata.inc.php b/modules-available/statistics_reporting/inc/getdata.inc.php index a167c2e5..da3a9a26 100644 --- a/modules-available/statistics_reporting/inc/getdata.inc.php +++ b/modules-available/statistics_reporting/inc/getdata.inc.php @@ -124,7 +124,7 @@ class GetData return $data; } - private function nullToZero(&$row) + private static function nullToZero(&$row) { foreach ($row as &$field) { if (is_null($field)) { diff --git a/modules-available/statistics_reporting/inc/queries.inc.php b/modules-available/statistics_reporting/inc/queries.inc.php index f00138ec..2269e764 100644 --- a/modules-available/statistics_reporting/inc/queries.inc.php +++ b/modules-available/statistics_reporting/inc/queries.inc.php @@ -11,13 +11,13 @@ class Queries $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 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 @@ -33,14 +33,14 @@ class Queries $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 -- cgit v1.2.3-55-g7522 From 4a230b9a4843ade82ff12fda31b771a5a9e58977 Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Thu, 6 Apr 2017 17:30:12 +0200 Subject: [statistics] Handle client screens, manage projectors --- modules-available/statistics/api.inc.php | 73 ++++++++++++++++++++++ .../statistics/hooks/config-tgz.inc.php | 32 ++++++++++ .../statistics/inc/devicetype.inc.php | 6 ++ modules-available/statistics/install.inc.php | 55 +++++++++++++++- modules-available/statistics/page.inc.php | 70 +++++++++++++++++++++ .../statistics/templates/machine-main.html | 30 +++++++++ .../statistics/templates/projector-list.html | 21 +++++++ 7 files changed, 284 insertions(+), 3 deletions(-) create mode 100644 modules-available/statistics/hooks/config-tgz.inc.php create mode 100644 modules-available/statistics/inc/devicetype.inc.php create mode 100644 modules-available/statistics/templates/projector-list.html (limited to 'modules-available') 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 @@ + '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 @@ + $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 diff --git a/modules-available/statistics/templates/machine-main.html b/modules-available/statistics/templates/machine-main.html index 0b333a27..56454e78 100644 --- a/modules-available/statistics/templates/machine-main.html +++ b/modules-available/statistics/templates/machine-main.html @@ -130,6 +130,36 @@ {{lang_64bitSupport}} {{kvmstate}} + + {{lang_screens}} + +
+ + +
+
+ + +
+ {{#screens}} +
+
+ {{#projector}} + {{lang_projector}} + + {{/projector}} + {{^projector}} + + {{/projector}} +
+ {{connector}}: {{hwname}} {{resolution}} +
+
+ {{/screens}} + +

{{lang_devices}}

{{#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 @@ +
+
{{lang_projectors}}
+
+
+ + +

{{lang_thoseAreProjectors}}

+ {{#projectors}} +
+ + {{hwname}} +
+ {{/projectors}} + {{^projectors}} +
{{lang_noProjectorsDefined}}
+ {{/projectors}} +
+
+
\ No newline at end of file -- cgit v1.2.3-55-g7522 From 016ebfe0affc86e14115afe34d415bc58f9164b3 Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Fri, 7 Apr 2017 14:47:44 +0200 Subject: [statistics] Better errormsg in install, link to projectors added --- modules-available/statistics/install.inc.php | 15 ++++++++++----- modules-available/statistics/templates/machine-main.html | 2 +- 2 files changed, 11 insertions(+), 6 deletions(-) (limited to 'modules-available') diff --git a/modules-available/statistics/install.inc.php b/modules-available/statistics/install.inc.php index 5d8ce1bc..79346f99 100644 --- a/modules-available/statistics/install.inc.php +++ b/modules-available/statistics/install.inc.php @@ -201,16 +201,21 @@ if ($addTrigger) { } if ($machineHwCreate === UPDATE_DONE) { - $ret = true; $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') && $ret; + 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') && $ret; + 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') && $ret; + ADD CONSTRAINT `statistic_hw_prop_ibfk_1` FOREIGN KEY (`hwid`) REFERENCES `statistic_hw` (`hwid`) ON DELETE CASCADE'); if ($ret === false) { - finalResponse(UPDATE_FAILED, 'Adding foreign key machineuuid to hardware* failed: ' . Database::lastError()); + finalResponse(UPDATE_FAILED, 'Adding constraint to statistic_hw_prop failed: ' . Database::lastError()); } } diff --git a/modules-available/statistics/templates/machine-main.html b/modules-available/statistics/templates/machine-main.html index 56454e78..bdc51167 100644 --- a/modules-available/statistics/templates/machine-main.html +++ b/modules-available/statistics/templates/machine-main.html @@ -145,7 +145,7 @@
{{#projector}} - {{lang_projector}} + {{lang_projector}} {{/projector}} -- cgit v1.2.3-55-g7522 From 3dedcaefe5d99bc62619a327025d7ca368a76e92 Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Fri, 7 Apr 2017 14:48:19 +0200 Subject: Update translations --- modules-available/dozmod/lang/de/template-tags.json | 1 - modules-available/dozmod/lang/en/template-tags.json | 5 ++--- modules-available/statistics/lang/de/template-tags.json | 5 +++++ modules-available/statistics/lang/en/template-tags.json | 5 +++++ 4 files changed, 12 insertions(+), 4 deletions(-) (limited to 'modules-available') 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/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", -- cgit v1.2.3-55-g7522 From 6b1a519b152d6a7a92c527220569f53ed0b95a70 Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Mon, 10 Apr 2017 12:32:37 +0200 Subject: [statistics_reporting] Don't include perUser and perClient in remote report --- modules-available/statistics_reporting/inc/remotereport.inc.php | 2 -- 1 file changed, 2 deletions(-) (limited to 'modules-available') diff --git a/modules-available/statistics_reporting/inc/remotereport.inc.php b/modules-available/statistics_reporting/inc/remotereport.inc.php index a2234849..4c5f604b 100644 --- a/modules-available/statistics_reporting/inc/remotereport.inc.php +++ b/modules-available/statistics_reporting/inc/remotereport.inc.php @@ -73,8 +73,6 @@ class RemoteReport 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; -- cgit v1.2.3-55-g7522 From 106bc16250bf7fb99a451d64783f705aa6ac212d Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Mon, 10 Apr 2017 14:45:33 +0200 Subject: [statistics_reporting] Send backlogged reports in cronjob --- modules-available/statistics_reporting/hooks/cron.inc.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'modules-available') diff --git a/modules-available/statistics_reporting/hooks/cron.inc.php b/modules-available/statistics_reporting/hooks/cron.inc.php index 45f39719..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); @@ -21,5 +21,6 @@ if (RemoteReport::isReportingEnabled()) { } else { EventLog::info('Statistics report sent to ' . CONFIG_REPORTING_URL); } + $nextReporting = strtotime("+7 days", $nextReporting); } } \ No newline at end of file -- cgit v1.2.3-55-g7522 From 5a922e7d360f7aa2ea3c4a84a5610940f06cd037 Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Wed, 12 Apr 2017 12:30:51 +0200 Subject: [rebootcontrol] New module for shutting down and rebooting clients Squashed commit of the following: commit 00a9af598b949dec748f2b6ddb42f124f2352451 Author: Simon Rettberg Date: Wed Apr 12 12:28:18 2017 +0200 [rebootcontrol] Enforce POST for keypair regen triggering commit fb5da4d50c364ec53086b7da7d9f9f094ac0b87e Author: Simon Rettberg Date: Wed Apr 12 12:00:59 2017 +0200 [rebootcontrol] Resolve lecture uuids in client list commit 0fc97f2c7b4a085efcd86262afe351784d4a6743 Author: Simon Rettberg Date: Wed Apr 12 11:43:17 2017 +0200 [rebootcontrol] Support REBOOT_AT state commit 8383db423d5ef86da49d0ed1ae719254bcd65d79 Author: Simon Rettberg Date: Tue Apr 11 15:15:20 2017 +0200 [rebootcontrol] Add REBOOT_AT translation commit d19b59015f383f04369fb89ac67abae4872c3d19 Author: Simon Rettberg Date: Mon Apr 10 15:58:55 2017 +0200 [rebootcontrol] Remove redundant location queries; improve pages a bit Rows are now marked using the designated active class, a client can be selected clicking anywhere in the last column. commit 674053e18b9e800f113471c19050ab468f73f233 Merge: 2210fbb 3dedcae Author: Simon Rettberg Date: Mon Apr 10 11:32:55 2017 +0200 Merge branch 'master' into reboot-control commit 2210fbb4947643988adef716594d79cf58482b87 Author: Simon Rettberg Date: Sat Apr 8 11:22:44 2017 +0200 [rebootcontrol] Adapt to changed task structure; handle clients by UUID commit 0ca01982028e08aa99e896d47c669ec77ec7dada Author: Simon Rettberg Date: Fri Apr 7 15:45:36 2017 +0200 [rebootcontrol] Fix config.tgz hook commit 0bb4ae9291452112895ec3f3703bc868ee8d80d2 Author: Simon Rettberg Date: Tue Mar 14 19:54:53 2017 +0100 [rebootcontrol] Fix race condition when multiple config.tgz are generated Since the same file got created and deleted in a rapid succession, this could lead to the RecompressArchive task failing with a "file not found" error. commit 74e735de601961620fd65500c3f2375af16d61fa Author: Udo Walter Date: Mon Mar 13 12:32:36 2017 +0100 [reboot-control] added status column commit 06d52b3c106d9db37b877297531da0f36452b81b Author: Udo Walter Date: Thu Mar 9 13:15:05 2017 +0100 [reboot-control] added config-tgz hook to include the ssh key commit 83abeda07ab995b7439011829fb98caf94b5c63e Author: Christian Hofmaier Date: Mon Mar 6 16:58:14 2017 +0100 [reboot-control] made sortable by checkbox column commit 207c78e4431689eb3e2944f6a3657522806abf89 Merge: 712a9a5 deaa6b2 Author: Udo Walter Date: Mon Mar 6 15:41:37 2017 +0100 Merge remote-tracking branch 'origin/master' into reboot-control commit 712a9a5f2d51e3a866e31df94722919279ed7fc2 Author: Udo Walter Date: Mon Mar 6 15:36:59 2017 +0100 [reboot-control] added possibility to generate a new ssh public private keypair commit d40de6044d4d63b4c25a1a950c4a126c6becc1ca Author: Christian Hofmaier Date: Thu Mar 2 15:02:12 2017 +0100 [reboot-control] disabled possibility to reboot/shutdown if no clients selected commit d008eb6adb486857b898609e1e89adf4473be50c Merge: 5259dd5 7b198f9 Author: Udo Walter Date: Thu Mar 2 14:42:28 2017 +0100 Merge remote-tracking branch 'origin/master' into reboot-control commit 5259dd5957a328fe4b12c9cf0e47f7c90c9d8bed Merge: 3c81c7e 1ed086a Author: Udo Walter Date: Thu Mar 2 14:30:43 2017 +0100 Merge branch 'reboot-control' of git.openslx.org:openslx-ng/slx-admin into reboot-control commit 3c81c7eb35b878c01e1d4b70000dab6c76559012 Author: Christian Hofmaier Date: Thu Mar 2 13:49:09 2017 +0100 [reboot-control] bugfix @ locationid=none commit 1925d0469626ee7cf4dd7baa9c05274c1d243c7c Author: Christian Hofmaier Date: Thu Mar 2 13:40:11 2017 +0100 [reboot-control] if no clients are selected, redirect back, not to status page commit 0cd8ec5787baac16f4169b11cf9ce46733baa124 Author: Christian Hofmaier Date: Mon Feb 27 18:11:09 2017 +0100 [reboot-control] second (last) part javascript revision commit 1133a5c8b488c8381762e727dbeab31a1860e1eb Author: Christian Hofmaier Date: Thu Feb 16 16:10:29 2017 +0100 [reboot-control] first small part javascript revision commit ace5ab7e45b232a54299d9a3c41c60fa839dfbc1 Author: Udo Walter Date: Tue Feb 14 15:44:20 2017 +0100 [reboot-control] pass ssh private key to the reboot task commit 5df5cb3b8ae35d56eefe94e8557aa13cb1178391 Author: Christian Hofmaier Date: Tue Feb 14 14:36:26 2017 +0100 [reboot-control] added comments commit ff3cbfbb8100071bc748ce15e4c37b1d028cbb0d Author: Christian Hofmaier Date: Tue Feb 14 14:14:01 2017 +0100 [reboot-control] added api commit afce01b6f629b20e195a6e6ee4cd581d7ef8c079 Author: Christian Hofmaier Date: Mon Feb 13 18:54:45 2017 +0100 [reboot-control] fixed warning commit ff7c072d1462ed23de8441ebdb06d6435606a2b1 Author: Christian Hofmaier Date: Mon Feb 13 18:27:56 2017 +0100 [reboot-control] small css change commit 55195c89b7846c25d246badf96863bd24a9b8d2d Author: Christian Hofmaier Date: Mon Feb 13 18:23:51 2017 +0100 [reboot-control] deleted slider, using textbox now for shutdown-timer commit 23864e7e658cd0e061875c5c353497c92ab77bc9 Author: Christian Hofmaier Date: Mon Feb 13 17:17:39 2017 +0100 [reboot-control] fixed typo. (extremely) small design change. commit 821501e290abd89c8ce78e764a997655779a71b1 Author: Udo Walter Date: Thu Feb 9 16:54:55 2017 +0100 [reboot-control] added glyphicons to the buttons commit be5f54a29cde1b2de7577b022ec97032104d8437 Author: Udo Walter Date: Thu Feb 9 15:52:09 2017 +0100 [reboot-control] fixed bugs caused by renaming the module commit 89bb0068d2a232c9a2dc4ebf23e84f626db6e1a9 Author: Udo Walter Date: Thu Feb 9 15:21:52 2017 +0100 [reboot-control] fixed class name commit 60d080b30e02cf42395e1fe1b4ca7df11d4e3ff1 Author: Udo Walter Date: Thu Feb 9 15:11:15 2017 +0100 [reboot-control] renamed reboot_control to rebootcontrol commit dfc9a48705501c73c611674b0c7f2bfa3178a38c Author: Udo Walter Date: Wed Feb 8 17:28:14 2017 +0100 [reboot-control] added status page and reorganized status update commit f7850d060dd32682ddc6795b2836ba45e9e6fe44 Author: Christian Hofmaier Date: Tue Feb 7 15:58:48 2017 +0100 [reboot-control] variable language consistency commit 24757e1e2395ae70b8e06f9c72a6b3b1050895b3 Author: Udo Walter Date: Fri Feb 3 17:52:53 2017 +0100 [reboot-control] improved (un)select all button commit f611bd2b594f0e0ffe8f81dfe0260758f86419ec Author: Christian Hofmaier Date: Fri Feb 3 17:27:02 2017 +0100 [reboot-control] removed warning commit 1e58de43049dd6e2fa0ce0d32df0b1ed2e90cd63 Author: Christian Hofmaier Date: Fri Feb 3 16:56:53 2017 +0100 [reboot-control] added stylesheet, Show hand-cursor for sortable columns, rows are now marked when chosen commit 6661568437cb36328b346730a3e0d5dc57e85cfc Author: Udo Walter Date: Tue Jan 31 16:06:53 2017 +0100 [reboot-control] added option to shutdown clients in future (server-side) commit 4a0aa0bc53508206ff473aa9589c5160db3f85d5 Author: Christian Hofmaier Date: Tue Jan 31 15:13:58 2017 +0100 [reboot-control] added option to shutdown clients in future commit 0411d3f2a19d639229dba6a58bf5dbaccac607b3 Author: Christian Hofmaier Date: Tue Jan 31 13:06:28 2017 +0100 [reboot-control] added shutdown option/button commit 9155820fd6ebceae70d98e23479358229457868d Author: Udo Walter Date: Fri Jan 27 13:06:06 2017 +0100 [reboot-control] added status translation commit 2715df67d51f07a7f80cb02759b1726fd8b0ddd3 Author: Christian Hofmaier Date: Fri Jan 27 13:01:18 2017 +0100 [reboot-control] added table sorting (based on stupidtable plugin) commit 959fb45b039db04af759efe96d6c9b8eafda6227 Author: Udo Walter Date: Fri Jan 27 11:58:09 2017 +0100 [reboot-control] added reboot functionality commit b54f970b8e229a976ac9938626bce1b065f741a8 Author: Christian Hofmaier Date: Fri Jan 27 11:28:07 2017 +0100 [reboot-control] reboot buttons, style changes commit 4a5a933a91fad7a936790d57c5eea42d0f53f614 Author: Christian Hofmaier Date: Tue Jan 24 16:20:29 2017 +0100 [reboot-control] warningfix commit f9a6212eca2bbb9902827c8824d853f042ffa4f1 Author: Christian Hofmaier Date: Tue Jan 24 16:19:30 2017 +0100 [reboot-control] user interface commit 59e425ac93dfa181891cac92a435ef34972e90a7 Author: Christian Hofmaier Date: Tue Jan 17 16:05:12 2017 +0100 [reboot-control] created branch and empty files to work with commit 1ed086a17b248016882383a99c85356ee33342b2 Author: Christian Hofmaier Date: Thu Mar 2 13:49:09 2017 +0100 [reboot-control] bugfix @ locationid=none commit ee36e147133d8b1a8ae3fe406677f3483bd1db02 Author: Christian Hofmaier Date: Thu Mar 2 13:40:11 2017 +0100 [reboot-control] if no clients are selected, redirect back, not to status page commit 3c6d7946fd4e9e10649d8d41ab89bc90f83112ff Author: Christian Hofmaier Date: Mon Feb 27 18:11:09 2017 +0100 [reboot-control] second (last) part javascript revision commit 550aa8a776cb1a328693f1fee5722f10c1ff07b8 Author: Christian Hofmaier Date: Thu Feb 16 16:10:29 2017 +0100 [reboot-control] first small part javascript revision commit 67f851c63fd937ffca2f8a1e6174982591cded2a Merge: e1701e0 a7b1a42 Author: Udo Walter Date: Tue Feb 14 15:46:05 2017 +0100 Merge remote-tracking branch 'origin/reboot-control' into reboot-control commit e1701e01065131777f8643b015b32832f21e864c Author: Udo Walter Date: Tue Feb 14 15:44:20 2017 +0100 [reboot-control] pass ssh private key to the reboot task commit a7b1a42a00742b995a7ef6a8d28fa661966ec5be Author: Christian Hofmaier Date: Tue Feb 14 14:36:26 2017 +0100 [reboot-control] added comments commit 7af992b2fcee946b04a1fdea4e7cc28b70436bf8 Author: Christian Hofmaier Date: Tue Feb 14 14:14:01 2017 +0100 [reboot-control] added api commit 57beae14c82cd1a39a6b266963c78d81aa50d494 Author: Christian Hofmaier Date: Mon Feb 13 18:54:45 2017 +0100 [reboot-control] fixed warning commit 1a4842c22d7f8e85033f793bd801e2d82ec351c4 Author: Christian Hofmaier Date: Mon Feb 13 18:27:56 2017 +0100 [reboot-control] small css change commit da57d7491ad17a6d64be0c4624472ffa80efb43a Author: Christian Hofmaier Date: Mon Feb 13 18:23:51 2017 +0100 [reboot-control] deleted slider, using textbox now for shutdown-timer commit a1b16068d4f56223587084897b7c6ca5a4c54805 Author: Christian Hofmaier Date: Mon Feb 13 17:17:39 2017 +0100 [reboot-control] fixed typo. (extremely) small design change. commit a84f2f50eb4e0d6853020212f162780e23f0a696 Author: Udo Walter Date: Thu Feb 9 16:54:55 2017 +0100 [reboot-control] added glyphicons to the buttons commit 06f52dfc9518ffda317cea2dab5cf16dbe66524c Author: Udo Walter Date: Thu Feb 9 15:52:09 2017 +0100 [reboot-control] fixed bugs caused by renaming the module commit e5edd1b1d4291ac403587e938200c24b3bcebb22 Author: Udo Walter Date: Thu Feb 9 15:21:52 2017 +0100 [reboot-control] fixed class name commit 1c2b3afc26dec0d9fefb7692b4670e80cd16362a Author: Udo Walter Date: Thu Feb 9 15:11:15 2017 +0100 [reboot-control] renamed reboot_control to rebootcontrol commit f2e8624ecf9858276f63be10c279633fd45bf9d9 Author: Udo Walter Date: Wed Feb 8 17:28:14 2017 +0100 [reboot-control] added status page and reorganized status update commit 0d918fead580748f5a3cb1db6ab754e34c844192 Author: Christian Hofmaier Date: Tue Feb 7 15:58:48 2017 +0100 [reboot-control] variable language consistency commit 54fe25485bd1f09d332f50b3d0645b166e8012d7 Author: Udo Walter Date: Fri Feb 3 17:52:53 2017 +0100 [reboot-control] improved (un)select all button commit 852261f194eb235e68f7256d3d526f5ae3936e13 Author: Christian Hofmaier Date: Fri Feb 3 17:27:02 2017 +0100 [reboot-control] removed warning commit 63bbeb4f684eeb8e36a462cda76b3534f96a99e6 Author: Christian Hofmaier Date: Fri Feb 3 16:56:53 2017 +0100 [reboot-control] added stylesheet, Show hand-cursor for sortable columns, rows are now marked when chosen commit d42b680196e727a0a67e5aaa954ba3a723c300f7 Author: Udo Walter Date: Tue Jan 31 16:06:53 2017 +0100 [reboot-control] added option to shutdown clients in future (server-side) commit 49f8d204439d091970911d1528f9d3b839442d27 Author: Christian Hofmaier Date: Tue Jan 31 15:13:58 2017 +0100 [reboot-control] added option to shutdown clients in future commit 666b61b9d5786ffedc01dc4a20285dd607b443ba Author: Christian Hofmaier Date: Tue Jan 31 13:06:28 2017 +0100 [reboot-control] added shutdown option/button commit d866d5e36fe7f5ef6f6fd7a9bd661b471a8b040d Author: Udo Walter Date: Fri Jan 27 13:06:06 2017 +0100 [reboot-control] added status translation commit d56d8361c4113fc9ccaf575b372ea4c7ec0d4123 Author: Christian Hofmaier Date: Fri Jan 27 13:01:18 2017 +0100 [reboot-control] added table sorting (based on stupidtable plugin) commit bac9bcbd84f3fc6d6b593e5ae4113b23f538ff6c Author: Udo Walter Date: Fri Jan 27 11:58:09 2017 +0100 [reboot-control] added reboot functionality commit adfc00ebb64c8001927f5d60459c71b5da1b2dca Author: Christian Hofmaier Date: Fri Jan 27 11:28:07 2017 +0100 [reboot-control] reboot buttons, style changes commit 3eac6c54c9999f810f17c05c460d086ce30fd6d8 Author: Christian Hofmaier Date: Tue Jan 24 16:20:29 2017 +0100 [reboot-control] warningfix commit 00f90f2d27593a18108e89daad48a99d5c150dea Author: Christian Hofmaier Date: Tue Jan 24 16:19:30 2017 +0100 [reboot-control] user interface commit b576fc14ca48f47793c2cea2a701ae22cd3110a6 Author: Christian Hofmaier Date: Tue Jan 17 16:05:12 2017 +0100 [reboot-control] created branch and empty files to work with --- modules-available/rebootcontrol/api.inc.php | 36 +++ modules-available/rebootcontrol/clientscript.js | 22 ++ modules-available/rebootcontrol/config.json | 4 + .../rebootcontrol/hooks/config-tgz.inc.php | 18 ++ .../rebootcontrol/inc/rebootqueries.inc.php | 44 ++++ modules-available/rebootcontrol/inc/sshkey.inc.php | 40 ++++ .../rebootcontrol/lang/de/messages.json | 4 + .../rebootcontrol/lang/de/module.json | 5 + .../rebootcontrol/lang/de/template-tags.json | 31 +++ .../rebootcontrol/lang/en/messages.json | 4 + .../rebootcontrol/lang/en/module.json | 5 + .../rebootcontrol/lang/en/template-tags.json | 31 +++ .../rebootcontrol/lang/pt/template-tags.json | 30 +++ modules-available/rebootcontrol/page.inc.php | 106 +++++++++ modules-available/rebootcontrol/style.css | 47 ++++ .../rebootcontrol/templates/_page.html | 245 +++++++++++++++++++++ .../rebootcontrol/templates/status.html | 62 ++++++ 17 files changed, 734 insertions(+) create mode 100644 modules-available/rebootcontrol/api.inc.php create mode 100644 modules-available/rebootcontrol/clientscript.js create mode 100644 modules-available/rebootcontrol/config.json create mode 100644 modules-available/rebootcontrol/hooks/config-tgz.inc.php create mode 100644 modules-available/rebootcontrol/inc/rebootqueries.inc.php create mode 100644 modules-available/rebootcontrol/inc/sshkey.inc.php create mode 100644 modules-available/rebootcontrol/lang/de/messages.json create mode 100644 modules-available/rebootcontrol/lang/de/module.json create mode 100644 modules-available/rebootcontrol/lang/de/template-tags.json create mode 100644 modules-available/rebootcontrol/lang/en/messages.json create mode 100644 modules-available/rebootcontrol/lang/en/module.json create mode 100644 modules-available/rebootcontrol/lang/en/template-tags.json create mode 100644 modules-available/rebootcontrol/lang/pt/template-tags.json create mode 100644 modules-available/rebootcontrol/page.inc.php create mode 100644 modules-available/rebootcontrol/style.css create mode 100644 modules-available/rebootcontrol/templates/_page.html create mode 100644 modules-available/rebootcontrol/templates/status.html (limited to 'modules-available') 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 @@ + $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(' '); + }); +}); \ 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 @@ +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 @@ += 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 @@ + 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 @@ +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 @@ +
+ +
+
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + {{#data}} + + + + + + + + + {{/data}} + +
{{lang_client}}{{lang_ip}}{{lang_status}}{{lang_session}}{{lang_user}}{{lang_selected}}
+ {{hostname}} + {{^hostname}}{{clientip}}{{/hostname}} + {{clientip}} + {{#status}} + {{lang_on}} + {{/status}} + {{^status}} + {{lang_off}} + {{/status}} + {{#status}}{{currentsession}}{{/status}}{{#status}}{{currentuser}}{{/status}} +
+ + +
+
+
+
+ + + + + + + + + +
+ + + \ 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 @@ +
+
+ {{lang_location}}: {{locationName}} + + + +
+
+ +
+ +
+ + + + + + + + + + + {{#clients}} + + + + + + {{/clients}} + +
{{lang_client}}{{lang_ip}} + {{lang_status}} +
{{machineuuid}}{{clientip}}
+
+ + -- cgit v1.2.3-55-g7522