summaryrefslogtreecommitdiffstats
path: root/modules-available
diff options
context:
space:
mode:
authorSimon Rettberg2017-04-11 21:34:11 +0200
committerSimon Rettberg2017-04-11 21:34:11 +0200
commitaac1902ca71a48a3b6e745e77da077099ccf2479 (patch)
tree7d0e67d9d3dc90306a37ef65e9080341299367e0 /modules-available
parentMerge branch 'location-info-panel' of git.openslx.org:openslx-ng/slx-admin in... (diff)
parent[statistics_reporting] Send backlogged reports in cronjob (diff)
downloadslx-admin-aac1902ca71a48a3b6e745e77da077099ccf2479.tar.gz
slx-admin-aac1902ca71a48a3b6e745e77da077099ccf2479.tar.xz
slx-admin-aac1902ca71a48a3b6e745e77da077099ccf2479.zip
Merge branch 'master' into location-info-panel
Diffstat (limited to 'modules-available')
-rw-r--r--modules-available/dozmod/lang/de/template-tags.json6
-rw-r--r--modules-available/dozmod/lang/en/template-tags.json6
-rw-r--r--modules-available/dozmod/page.inc.php49
-rw-r--r--modules-available/dozmod/templates/orglist.html2
-rw-r--r--modules-available/dozmod/templates/runtimeconfig.html35
-rw-r--r--modules-available/dozmod/templates/userlist.html2
-rw-r--r--modules-available/exams/page.inc.php9
-rw-r--r--modules-available/exams/templates/page-upcoming-lectures.html11
-rw-r--r--modules-available/js_stupidtable/clientscript.js30
-rw-r--r--modules-available/js_stupidtable/config.json1
-rw-r--r--modules-available/main/hooks/cron.inc.php15
-rw-r--r--modules-available/main/install.inc.php8
-rw-r--r--modules-available/main/page.inc.php7
-rw-r--r--modules-available/roomplanner/inc/pvsgenerator.inc.php19
-rw-r--r--modules-available/roomplanner/js/grid.js58
-rw-r--r--modules-available/roomplanner/js/init.js4
-rw-r--r--modules-available/roomplanner/style.css7
-rw-r--r--modules-available/roomplanner/templates/page.html5
-rw-r--r--modules-available/serversetup-bwlp/page.inc.php9
-rw-r--r--modules-available/statistics/api.inc.php73
-rw-r--r--modules-available/statistics/hooks/config-tgz.inc.php32
-rw-r--r--modules-available/statistics/inc/devicetype.inc.php6
-rw-r--r--modules-available/statistics/install.inc.php60
-rw-r--r--modules-available/statistics/lang/de/template-tags.json5
-rw-r--r--modules-available/statistics/lang/en/template-tags.json5
-rw-r--r--modules-available/statistics/page.inc.php72
-rw-r--r--modules-available/statistics/templates/machine-main.html30
-rw-r--r--modules-available/statistics/templates/projector-list.html21
-rw-r--r--modules-available/statistics_reporting/config.json4
-rw-r--r--modules-available/statistics_reporting/hooks/cron.inc.php26
-rw-r--r--modules-available/statistics_reporting/hooks/main-warning.inc.php5
-rw-r--r--modules-available/statistics_reporting/inc/getdata.inc.php157
-rw-r--r--modules-available/statistics_reporting/inc/queries.inc.php217
-rw-r--r--modules-available/statistics_reporting/inc/remotereport.inc.php82
-rw-r--r--modules-available/statistics_reporting/lang/de/messages.json4
-rw-r--r--modules-available/statistics_reporting/lang/de/module.json10
-rw-r--r--modules-available/statistics_reporting/lang/de/template-tags.json22
-rw-r--r--modules-available/statistics_reporting/lang/en/messages.json4
-rw-r--r--modules-available/statistics_reporting/lang/en/module.json10
-rw-r--r--modules-available/statistics_reporting/lang/en/template-tags.json22
-rw-r--r--modules-available/statistics_reporting/lang/pt/template-tags.json3
-rw-r--r--modules-available/statistics_reporting/page.inc.php279
-rw-r--r--modules-available/statistics_reporting/style.css42
-rw-r--r--modules-available/statistics_reporting/templates/columnChooser.html181
-rw-r--r--modules-available/statistics_reporting/templates/table-client.html30
-rw-r--r--modules-available/statistics_reporting/templates/table-location.html24
-rw-r--r--modules-available/statistics_reporting/templates/table-total.html22
-rw-r--r--modules-available/statistics_reporting/templates/table-user.html16
-rw-r--r--modules-available/statistics_reporting/templates/table-vm.html19
-rw-r--r--modules-available/sysconfig/addmodule_adauth.inc.php3
-rw-r--r--modules-available/sysconfig/addmodule_branding.inc.php2
-rw-r--r--modules-available/sysconfig/addmodule_custommodule.inc.php2
-rw-r--r--modules-available/sysconfig/addmodule_ldapauth.inc.php3
-rw-r--r--modules-available/sysconfig/addmodule_sshconfig.inc.php2
-rw-r--r--modules-available/sysconfig/hooks/cron.inc.php3
-rw-r--r--modules-available/sysconfig/inc/configmodulebaseldap.inc.php3
-rw-r--r--modules-available/sysconfig/inc/configtgz.inc.php239
-rw-r--r--modules-available/sysconfig/inc/ppd.inc.php1162
-rw-r--r--modules-available/sysconfig/lang/de/template-tags.json8
-rw-r--r--modules-available/sysconfig/lang/en/template-tags.json8
-rw-r--r--modules-available/sysconfig/templates/ad_ldap-homedir.html60
-rw-r--r--modules-available/sysconfig/templates/branding-check.html7
-rw-r--r--modules-available/systemstatus/lang/de/template-tags.json1
-rw-r--r--modules-available/systemstatus/lang/en/template-tags.json1
-rw-r--r--modules-available/systemstatus/page.inc.php32
-rw-r--r--modules-available/systemstatus/templates/_page.html9
-rw-r--r--modules-available/webinterface/lang/de/messages.json6
-rw-r--r--modules-available/webinterface/lang/de/template-tags.json10
-rw-r--r--modules-available/webinterface/lang/en/messages.json6
-rw-r--r--modules-available/webinterface/lang/en/template-tags.json14
-rw-r--r--modules-available/webinterface/page.inc.php92
-rw-r--r--modules-available/webinterface/templates/httpd-restart.html38
-rw-r--r--modules-available/webinterface/templates/https.html34
-rw-r--r--modules-available/webinterface/templates/passwords.html1
74 files changed, 3293 insertions, 219 deletions
diff --git a/modules-available/dozmod/lang/de/template-tags.json b/modules-available/dozmod/lang/de/template-tags.json
index a7a91e11..141ac68d 100644
--- a/modules-available/dozmod/lang/de/template-tags.json
+++ b/modules-available/dozmod/lang/de/template-tags.json
@@ -1,8 +1,11 @@
{
"lang_actionTarget": "Aktionsziel",
+ "lang_allowLoginByDefault": "Login standardm\u00e4\u00dfig erlauben",
+ "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",
"lang_currentFilter": "Aktueller Filter",
"lang_defaultImagePermissionAdmin": "Administrieren",
@@ -42,6 +45,7 @@
"lang_maxImageValidity": "G\u00fcltigkeitsdauer neuer VM-Versionen (Tage)",
"lang_maxLectureVisibility": "Sp\u00e4testes Enddatum einer Veranstaltung (Tage in der Zukunft)",
"lang_maxTransfers": "Maximale Zahl gleichzeitiger Up-\/Downloads pro Benutzer",
+ "lang_miscOptions": "Verschiedene Einstellungen",
"lang_name": "Name",
"lang_organization": "Einrichtung",
"lang_organizationList": "Liste der Einrichtungen",
diff --git a/modules-available/dozmod/lang/en/template-tags.json b/modules-available/dozmod/lang/en/template-tags.json
index 6e1a3996..5532bdcb 100644
--- a/modules-available/dozmod/lang/en/template-tags.json
+++ b/modules-available/dozmod/lang/en/template-tags.json
@@ -1,8 +1,11 @@
{
"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",
@@ -42,6 +45,7 @@
"lang_maxImageValidity": "New VM validity (days)",
"lang_maxLectureVisibility": "Max time lecture end date may lie in the future (days)",
"lang_maxTransfers": "Max concurrent transfers per user",
+ "lang_miscOptions": "Misc options",
"lang_name": "Name",
"lang_organization": "Organization",
"lang_organizationList": "List of organizations",
diff --git a/modules-available/dozmod/page.inc.php b/modules-available/dozmod/page.inc.php
index 6c0c8a6b..7f1e6ee3 100644
--- a/modules-available/dozmod/page.inc.php
+++ b/modules-available/dozmod/page.inc.php
@@ -89,29 +89,32 @@ class Page_DozMod extends Page
if ($section === 'runtimeconfig') {
// Runtime config
$runtimeConf = Database::queryFirst('SELECT value FROM sat.configuration WHERE parameter = :param', array('param' => 'runtimelimits'));
- if ($runtimeConf != null) {
+ if ($runtimeConf !== false) {
$runtimeConf = json_decode($runtimeConf['value'], true);
/* convert some value to corresponding "selected" texts */
if ($runtimeConf['defaultLecturePermissions']['edit']) {
- $runtimeConf['defaultLecturePermissions']['edit'] = 'checked="checked"';
+ $runtimeConf['defaultLecturePermissions']['edit'] = 'checked';
}
if ($runtimeConf['defaultLecturePermissions']['admin']) {
- $runtimeConf['defaultLecturePermissions']['admin'] = 'checked="checked"';
+ $runtimeConf['defaultLecturePermissions']['admin'] = 'checked';
}
if ($runtimeConf['defaultImagePermissions']['edit']) {
- $runtimeConf['defaultImagePermissions']['edit'] = 'checked="checked"';
+ $runtimeConf['defaultImagePermissions']['edit'] = 'checked';
}
if ($runtimeConf['defaultImagePermissions']['admin']) {
- $runtimeConf['defaultImagePermissions']['admin'] = 'checked="checked"';
+ $runtimeConf['defaultImagePermissions']['admin'] = 'checked';
}
if ($runtimeConf['defaultImagePermissions']['link']) {
- $runtimeConf['defaultImagePermissions']['link'] = 'checked="checked"';
+ $runtimeConf['defaultImagePermissions']['link'] = 'checked';
}
if ($runtimeConf['defaultImagePermissions']['download']) {
- $runtimeConf['defaultImagePermissions']['download'] = 'checked="checked"';
+ $runtimeConf['defaultImagePermissions']['download'] = 'checked';
}
+ if ($runtimeConf['allowLoginByDefault']) {
+ $runtimeConf['allowLoginByDefault'] = 'checked';
+ }
}
Render::addTemplate('runtimeconfig', $runtimeConf);
}
@@ -310,14 +313,29 @@ class Page_DozMod extends Page
$data['defaultLecturePermissions'] = Request::post('defaultLecturePermissions', NULL, "array");
$data['defaultImagePermissions'] = Request::post('defaultImagePermissions', NULL, "array");
- $intParams = [
- 'maxImageValidityDays' => array('min' => 7, 'max' => 999),
- 'maxLectureValidityDays' => array('min' => 7, 'max' => 999),
- 'maxTransfers' => array('min' => 1, 'max' => 10),
+ $params = [
+ 'int' => [
+ 'maxImageValidityDays' => array('min' => 7, 'max' => 999),
+ 'maxLectureValidityDays' => array('min' => 7, 'max' => 999),
+ 'maxTransfers' => array('min' => 1, 'max' => 10),
+ ],
+ 'bool' => [
+ 'allowLoginByDefault' => array('default' => true)
+ ],
];
- foreach($intParams as $field => $limits) {
- $value = Request::post($field, 0, 'int');
- $data[$field] = max(min($value, $limits['max']), $limits['min']);
+ foreach ($params as $type => $list) {
+ foreach ($list as $field => $limits) {
+ $default = isset($limits['default']) ? $limits['default'] : false;
+ $value = Request::post($field, $default);
+ settype($value, $type);
+ if (isset($limits['min']) && $value < $limits['min']) {
+ $value = $limits['min'];
+ }
+ if (isset($limits['max']) && $value > $limits['max']) {
+ $value = $limits['max'];
+ }
+ $data[$field] = $value;
+ }
}
/* ensure types */
@@ -327,9 +345,6 @@ class Page_DozMod extends Page
settype($data['defaultImagePermissions']['admin'], 'boolean');
settype($data['defaultImagePermissions']['link'], 'boolean');
settype($data['defaultImagePermissions']['download'], 'boolean');
- settype($data['maxImageValidityDays'], 'int');
- settype($data['maxLectureValidityDays'], 'int');
- settype($data['maxTransfers'], 'int');
$data = json_encode($data);
Database::exec('INSERT INTO sat.configuration (parameter, value)'
diff --git a/modules-available/dozmod/templates/orglist.html b/modules-available/dozmod/templates/orglist.html
index 482864de..21495bba 100644
--- a/modules-available/dozmod/templates/orglist.html
+++ b/modules-available/dozmod/templates/orglist.html
@@ -10,7 +10,7 @@
<thead>
<tr>
<th>{{lang_organization}}</th>
- <th><span class="glyphicon glyphicon-ok" title="{{lang_canLogin}}"></span></th>
+ <th><span class="glyphicon glyphicon-ok" title="{{lang_canLoginOrganization}}"></span></th>
</tr>
</thead>
<tbody>
diff --git a/modules-available/dozmod/templates/runtimeconfig.html b/modules-available/dozmod/templates/runtimeconfig.html
index e538dee8..9bdc44b0 100644
--- a/modules-available/dozmod/templates/runtimeconfig.html
+++ b/modules-available/dozmod/templates/runtimeconfig.html
@@ -91,6 +91,17 @@
</table>
</fieldset>
+ <fieldset>
+ <h3>{{lang_miscOptions}}</h3>
+ <div class="checkbox">
+ <input type="hidden" name="allowLoginByDefault" value="0">
+ <input type="checkbox" name="allowLoginByDefault" value="1" {{allowLoginByDefault}} id ="allowLoginByDefault" class="form-control">
+ <label>
+ {{lang_allowLoginByDefault}}
+ </label>
+ <p><i>{{lang_allowLoginDescription}}</i></p>
+ </div>
+ </fieldset>
<br>
<input type="hidden" name="token" value="{{token}}">
@@ -99,27 +110,3 @@
</form>
</div>
</div>
-
-<script type="text/javascript"><!--
-function slxTestConfig() {
- $('#test-button').prop('disabled', true);
- $('#test-spin').css('display', '');
- var str = $('#mailconf').serialize();
- str += '&button=test';
- console.log(str);
- $.post('?do=DozMod', str).done(function(data) {
- console.log('Success');
- console.log(data);
- checkRes(data);
- }).fail(function() {
- checkRes('DozMod refused the connection');
- }).always(function() {
- $('#test-button').prop('disabled', false);
- $('#test-spin').css('display', 'none');
- });
- }
-
- function checkRes(text) {
- $('#test-output').css('display', '').text(text);
- }
-// --> </script>
diff --git a/modules-available/dozmod/templates/userlist.html b/modules-available/dozmod/templates/userlist.html
index 93ef7b41..a4f415e1 100644
--- a/modules-available/dozmod/templates/userlist.html
+++ b/modules-available/dozmod/templates/userlist.html
@@ -16,7 +16,7 @@
<th>{{lang_email}}</th>
<th><span class="glyphicon glyphicon-envelope" title="{{lang_emailNotifications}}"></span></th>
<th><span class="glyphicon glyphicon-king" title="{{lang_superUser}}"></span></th>
- <th><span class="glyphicon glyphicon-ok" title="{{lang_canLogin}}"></span></th>
+ <th><span class="glyphicon glyphicon-ok" title="{{lang_canLoginUser}}"></span></th>
</tr>
</thead>
<tbody>
diff --git a/modules-available/exams/page.inc.php b/modules-available/exams/page.inc.php
index 49b48bb6..930ba62c 100644
--- a/modules-available/exams/page.inc.php
+++ b/modules-available/exams/page.inc.php
@@ -165,7 +165,8 @@ class Page_Exams extends Page
{
$out = [];
$now = time();
- $cutoff = strtotime('+ 5 day');
+ $cutoff = strtotime('+30 day');
+ $theCount = 0;
foreach ($this->lectures as $lecture) {
if ($lecture['endtime'] < $now || $lecture['starttime'] > $cutoff)
continue;
@@ -179,6 +180,9 @@ class Page_Exams extends Page
if ($duration < 86400) {
$entry['duration_s'] = gmdate('H:i', $duration);
}
+ if (++$theCount > 5) {
+ $entry['class'] = 'collapse';
+ }
$out[] = $entry;
}
return $out;
@@ -356,7 +360,8 @@ class Page_Exams extends Page
Message::addInfo('no-upcoming-lecture-exams');
} else {
Render::addTemplate('page-upcoming-lectures', [
- 'pending_lectures' => $upcoming
+ 'pending_lectures' => $upcoming,
+ 'decollapse' => array_key_exists('class', end($upcoming))
]);
}
// Vis.js timeline
diff --git a/modules-available/exams/templates/page-upcoming-lectures.html b/modules-available/exams/templates/page-upcoming-lectures.html
index 4a62bc29..a1867444 100644
--- a/modules-available/exams/templates/page-upcoming-lectures.html
+++ b/modules-available/exams/templates/page-upcoming-lectures.html
@@ -8,7 +8,7 @@
<th>{{lang_actions}}</th>
</tr>
{{#pending_lectures}}
- <tr>
+ <tr class="{{class}}">
<td>
{{displayname}}
<div class="small">
@@ -30,5 +30,14 @@
</td>
</tr>
{{/pending_lectures}}
+ {{#decollapse}}
+ <tr class="slx-decollapse">
+ <td colspan="3">
+ <span class="btn-group btn-group-justified">
+ <span class="btn btn-default btn-sm"><span class="glyphicon glyphicon-menu-down"></span></span>
+ </span>
+ </td>
+ </tr>
+ {{/decollapse}}
</table>
</div> \ No newline at end of file
diff --git a/modules-available/js_stupidtable/clientscript.js b/modules-available/js_stupidtable/clientscript.js
new file mode 100644
index 00000000..bfbc9112
--- /dev/null
+++ b/modules-available/js_stupidtable/clientscript.js
@@ -0,0 +1,30 @@
+/*
+ Stupid jQuery table plugin.
+
+ https://github.com/joequery/Stupid-Table-Plugin
+
+ Copyright (c) 2012 Joseph McCullough
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+ */
+
+(function(c){c.fn.stupidtable=function(b){return this.each(function(){var a=c(this);b=b||{};b=c.extend({},c.fn.stupidtable.default_sort_fns,b);a.data("sortFns",b);a.on("click.stupidtable","thead th",function(){c(this).stupidsort()})})};c.fn.stupidsort=function(b){var a=c(this),g=0,f=c.fn.stupidtable.dir,e=a.closest("table"),k=a.data("sort")||null;if(null!==k){a.parents("tr").find("th").slice(0,c(this).index()).each(function(){var a=c(this).attr("colspan")||1;g+=parseInt(a,10)});var d;1==arguments.length?
+ d=b:(d=b||a.data("sort-default")||f.ASC,a.data("sort-dir")&&(d=a.data("sort-dir")===f.ASC?f.DESC:f.ASC));if(a.data("sort-dir")!==d)return a.data("sort-dir",d),e.trigger("beforetablesort",{column:g,direction:d}),e.css("display"),setTimeout(function(){var b=[],l=e.data("sortFns")[k],h=e.children("tbody").children("tr");h.each(function(a,d){var e=c(d).children().eq(g),f=e.data("sort-value");"undefined"===typeof f&&(f=e.text(),e.data("sort-value",f));b.push([f,d])});b.sort(function(a,b){return l(a[0],
+ b[0])});d!=f.ASC&&b.reverse();h=c.map(b,function(a){return a[1]});e.children("tbody").append(h);e.find("th").data("sort-dir",null).removeClass("sorting-desc sorting-asc");a.data("sort-dir",d).addClass("sorting-"+d);e.trigger("aftertablesort",{column:g,direction:d});e.css("display")},10),a}};c.fn.updateSortVal=function(b){var a=c(this);a.is("[data-sort-value]")&&a.attr("data-sort-value",b);a.data("sort-value",b);return a};c.fn.stupidtable.dir={ASC:"asc",DESC:"desc"};c.fn.stupidtable.default_sort_fns=
+ {"int":function(b,a){return parseInt(b,10)-parseInt(a,10)},"float":function(b,a){return parseFloat(b)-parseFloat(a)},string:function(b,a){return b.toString().localeCompare(a.toString())},"string-ins":function(b,a){b=b.toString().toLocaleLowerCase();a=a.toString().toLocaleLowerCase();return b.localeCompare(a)}}})(jQuery);
diff --git a/modules-available/js_stupidtable/config.json b/modules-available/js_stupidtable/config.json
new file mode 100644
index 00000000..9e26dfee
--- /dev/null
+++ b/modules-available/js_stupidtable/config.json
@@ -0,0 +1 @@
+{} \ No newline at end of file
diff --git a/modules-available/main/hooks/cron.inc.php b/modules-available/main/hooks/cron.inc.php
new file mode 100644
index 00000000..bab27287
--- /dev/null
+++ b/modules-available/main/hooks/cron.inc.php
@@ -0,0 +1,15 @@
+<?php
+
+switch (mt_rand(1, 10)) {
+case 2:
+ Database::exec("DELETE FROM property_list WHERE dateline <> 0 AND dateline < UNIX_TIMESTAMP()");
+ break;
+case 3:
+ Database::exec("DELETE FROM property WHERE dateline <> 0 AND dateline < UNIX_TIMESTAMP()");
+ break;
+case 4:
+ Database::exec("DELETE FROM callback WHERE (UNIX_TIMESTAMP() - dateline) > 86400");
+ break;
+}
+
+Trigger::checkCallbacks();
diff --git a/modules-available/main/install.inc.php b/modules-available/main/install.inc.php
index 4c9d4baa..e364a905 100644
--- a/modules-available/main/install.inc.php
+++ b/modules-available/main/install.inc.php
@@ -26,6 +26,14 @@ $res[] = tableCreate('property', "
KEY `dateline` (`dateline`)
");
+$res[] = tableCreate('property_list', "
+ `name` varchar(50) NOT NULL,
+ `dateline` int(10) unsigned NOT NULL DEFAULT '0',
+ `value` text NOT NULL,
+ KEY (`name`),
+ KEY `dateline` (`dateline`)
+");
+
$res[] = tableCreate('user', "
`userid` int(10) unsigned NOT NULL AUTO_INCREMENT,
`login` varchar(100) NOT NULL,
diff --git a/modules-available/main/page.inc.php b/modules-available/main/page.inc.php
index bd50a5d1..08e8b5a6 100644
--- a/modules-available/main/page.inc.php
+++ b/modules-available/main/page.inc.php
@@ -24,11 +24,8 @@ class Page_Main extends Page
// Warnings
$needSetup = false;
- foreach (glob('modules/*/hooks/main-warning.inc.php') as $file) {
- preg_match('#^modules/([^/]+)/#', $file, $out);
- if (!Module::isAvailable($out[1]))
- continue;
- include $file;
+ foreach (Hook::load('main-warning') as $hook) {
+ include $hook->file;
}
// Update warning state
diff --git a/modules-available/roomplanner/inc/pvsgenerator.inc.php b/modules-available/roomplanner/inc/pvsgenerator.inc.php
index c00d5439..d61e826b 100644
--- a/modules-available/roomplanner/inc/pvsgenerator.inc.php
+++ b/modules-available/roomplanner/inc/pvsgenerator.inc.php
@@ -16,6 +16,8 @@ class PvsGenerator
/* get all rooms */
$rooms = array();
+ // Use left joins everywhere so we still have the complete list of locations below
+ // for figuring out which locations are leafs and which aren't
$ret = Database::simpleQuery(
'SELECT l.locationid, l.parentlocationid, l.locationname, lr.locationid AS notnull, lr.managerip, lr.tutoruuid, m.clientip as tutorip '
. 'FROM location l '
@@ -27,12 +29,27 @@ class PvsGenerator
settype($row['parentlocationid'], 'int');
$rooms[$row['locationid']] = $row;
}
+ // Mark all non-leafs as skip
foreach ($rooms as &$room) {
if ($room['parentlocationid'] > 0 && isset($rooms[$room['parentlocationid']])) {
$rooms[$room['parentlocationid']]['skip'] = true; // Don't just unset, might be wrong order
}
}
- unset($room); // refd!
+ // Now un-mark all where there's at least one child without valid room plan
+ foreach ($rooms as &$room) {
+ if (!isset($room['skip']) && (is_null($room['notnull']) || empty($room['managerip']))) {
+ $room['skip'] = true;
+ $r2 =& $room;
+ while ($r2['parentlocationid'] > 0) {
+ $r2 =& $rooms[$r2['parentlocationid']];
+ if (!(is_null($room['notnull']) || empty($room['managerip']))) {
+ unset($r2['skip']);
+ break;
+ }
+ }
+ }
+ }
+ unset($room, $r2); // refd!
/* collect names and build room blocks - filter empty rooms while at it */
$roomNames = array();
diff --git a/modules-available/roomplanner/js/grid.js b/modules-available/roomplanner/js/grid.js
index cc09e21d..334057bf 100644
--- a/modules-available/roomplanner/js/grid.js
+++ b/modules-available/roomplanner/js/grid.js
@@ -17,6 +17,7 @@ if (!roomplanner) var roomplanner = {
},
settings: {
cellsep: 4,
+ cellsize: 25,
scale: 100,
room: {
width: 33,
@@ -363,6 +364,7 @@ if (!roomplanner) var roomplanner = {
});
roomplanner.grid.scale(roomplanner.settings.scale);
+ roomplanner.fitContent();
},
clear: function() {
$('#draw-element-area').html('');
@@ -393,20 +395,18 @@ roomplanner.grid = (function() {
$('#drawarea').css('background-size',num);
roomplanner.settings.scale = num;
$('#draw-element-area .ui-draggable').each(function(idx,item) {
- var h = $(item).attr('data-height') * roomplanner.getScaleFactor();
- var w = $(item).attr('data-width') * roomplanner.getScaleFactor();
+ var $item = $(item);
+ var h = $item.attr('data-height') * roomplanner.getScaleFactor();
+ var w = $item.attr('data-width') * roomplanner.getScaleFactor();
//var pos = roomplanner.getCelloffset()
+
+ var pos = roomplanner.getCellPositionFromGrid($item.attr('gridRow'),$item.attr('gridCol'));
- var l = parseInt($(item).css('left')) * roomplanner.getScaleFactor();
- var t = parseInt($(item).css('top')) * roomplanner.getScaleFactor();
-
- var pos = roomplanner.getCellPositionFromGrid($(item).attr('gridRow'),$(item).attr('gridCol'));
-
- $(item).css({width: w+"px", height: h+"px", left: pos[0]+"px", top: pos[1]+"px"});
- $(item).draggable("option","grid",[(roomplanner.settings.scale / 4), (roomplanner.settings.scale / 4)]);
+ $item.css({width: w+"px", height: h+"px", left: pos[0]+"px", top: pos[1]+"px"});
+ $item.draggable("option","grid",[(roomplanner.settings.scale / 4), (roomplanner.settings.scale / 4)]);
if (roomplanner.isElementResizable(item)) {
- $(item).resizable("option","grid",[(roomplanner.settings.scale / 4), (roomplanner.settings.scale / 4)]);
+ $item.resizable("option","grid",[(roomplanner.settings.scale / 4), (roomplanner.settings.scale / 4)]);
}
});
this.resize();
@@ -423,6 +423,44 @@ roomplanner.grid = (function() {
}
)();
+roomplanner.fitContent = function() {
+ var minX = 99999;
+ var minY = 99999;
+ var maxX = -99999;
+ var maxY = -99999;
+ $('#draw-element-area .ui-draggable').each(function(idx,item) {
+ var $item = $(item);
+
+ var l = parseInt($item.attr('gridcol')) * roomplanner.settings.cellsize;
+ var r = l + parseInt($item.attr('data-width'));
+ var t = parseInt($item.attr('gridrow')) * roomplanner.settings.cellsize;
+ var b = t + parseInt($item.attr('data-height'));
+
+ if (l < minX) minX = l;
+ if (t < minY) minY = t;
+ if (r > maxX) maxX = r;
+ if (b > maxY) maxY = b;
+ });
+ if (minX > maxX)
+ return;
+ var width = (maxX - minX) / $('#drawpanel .panel-body').width();
+ var height = (maxY - minY) / $('#drawpanel .panel-body').height();
+ var scale;
+ if (width > height) {
+ scale = Math.floor(100 / width);
+ } else {
+ scale = Math.floor(100 / height);
+ }
+ roomplanner.slider.slider('value', scale);
+ scale = roomplanner.settings.scale;
+ var opts = {
+ left: -(minX * (scale / 100)) + "px",
+ top: -(minY * (scale / 100)) + "px"
+ };
+
+ $('#drawarea').css(opts);
+};
+
$(document).ready(function(){
roomplanner.grid.init();
diff --git a/modules-available/roomplanner/js/init.js b/modules-available/roomplanner/js/init.js
index ef3d15a7..7cada0dd 100644
--- a/modules-available/roomplanner/js/init.js
+++ b/modules-available/roomplanner/js/init.js
@@ -59,6 +59,10 @@ function initRoomplanner() {
$('#zoom-in').click(function() {
roomplanner.slider.slider('value', roomplanner.settings.scale + 10);
});
+
+ $('#zoom-fit').click(function() {
+ roomplanner.fitContent();
+ });
}
var translation = {
diff --git a/modules-available/roomplanner/style.css b/modules-available/roomplanner/style.css
index 1460364a..6a68a444 100644
--- a/modules-available/roomplanner/style.css
+++ b/modules-available/roomplanner/style.css
@@ -40,7 +40,7 @@ body {
#scaleContainer {
position: absolute;
bottom: 5px;
- right: 30px;
+ right: 50px;
width: 15%;
z-index:1000;
}
@@ -48,7 +48,7 @@ body {
#scaleslider {
position:relative;}
-#zoom-out, #zoom-in {
+#zoom-out, #zoom-in, #zoom-fit {
cursor:pointer;
}
@@ -61,6 +61,9 @@ body {
#scaleContainer .glyphicon-zoom-out {
left:-20px;}
+#scaleContainer .glyphicon-move {
+ right:-40px;}
+
#scaleContainer .glyphicon-zoom-in {
right:-20px; }
diff --git a/modules-available/roomplanner/templates/page.html b/modules-available/roomplanner/templates/page.html
index 8bfa0ca4..e8544ce8 100644
--- a/modules-available/roomplanner/templates/page.html
+++ b/modules-available/roomplanner/templates/page.html
@@ -301,9 +301,12 @@
<div id="draw-element-area" style="width:100%; height:100%;"></div>
</div>
<div id="scaleContainer">
+ <span id="zoom-fit" class="glyphicon glyphicon-move" aria-hidden="true"></span>
+ <div>
<div id="scaleslider"></div>
<span id="zoom-out" class="glyphicon glyphicon-zoom-out" aria-hidden="true"></span>
<span id="zoom-in" class="glyphicon glyphicon-zoom-in" aria-hidden="true"></span>
+ </div>
</div>
</div>
@@ -311,7 +314,7 @@
</div>
<div class="pull-left">
- <div class="input-group" style="width:400px">
+ <div class="input-group" style="width:1px">
<div class="input-group-addon">{{lang_managerIp}}</div>
<input class="form-control" type="text" id="manager-ip" value="{{managerip}}" placeholder="1.2.3.4" style="width:120px">
<div class="input-group-addon checkbox"><input id="dedi-mgr" type="checkbox" {{dediMgrChecked}}> <label for="dedi-mgr">{{lang_dedicatedManager}}</label></div>
diff --git a/modules-available/serversetup-bwlp/page.inc.php b/modules-available/serversetup-bwlp/page.inc.php
index 9bea4b50..9d7d11ac 100644
--- a/modules-available/serversetup-bwlp/page.inc.php
+++ b/modules-available/serversetup-bwlp/page.inc.php
@@ -81,8 +81,13 @@ class Page_ServerSetup extends Page
return false;
}
- if ($this->taskStatus['statusCode'] === TASK_WAITING) { // TODO: Async if just displaying
- $this->taskStatus = Taskmanager::waitComplete($this->taskStatus['id']);
+ if (!Taskmanager::isFinished($this->taskStatus)) { // TODO: Async if just displaying
+ $this->taskStatus = Taskmanager::waitComplete($this->taskStatus['id'], 4000);
+ }
+
+ if (Taskmanager::isFailed($this->taskStatus) || !isset($this->taskStatus['data']['addresses'])) {
+ $this->taskStatus['data']['addresses'] = false;
+ return false;
}
$sortIp = array();
diff --git a/modules-available/statistics/api.inc.php b/modules-available/statistics/api.inc.php
index 2ac6e782..126c6e91 100644
--- a/modules-available/statistics/api.inc.php
+++ b/modules-available/statistics/api.inc.php
@@ -201,6 +201,79 @@ if ($type{0} === '~') {
}
}
Database::exec('UPDATE machine SET logintime = 0, lastseen = UNIX_TIMESTAMP(), lastboot = 0 WHERE machineuuid = :uuid', array('uuid' => $uuid));
+ } elseif ($type === '~screens') {
+ $screens = Request::post('screen', false, 'array');
+ if (is_array($screens)) {
+ // `devicetype`, `devicename`, `subid`, `machineuuid`
+ // Make sure all screens are in the general hardware table
+ $hwids = array();
+ foreach ($screens as $port => $screen) {
+ if (!array_key_exists('name', $screen))
+ continue;
+ if (array_key_exists($screen['name'], $hwids)) {
+ $hwid = $hwids[$screen['name']];
+ } else {
+ $hwid = (int)Database::insertIgnore('statistic_hw', 'hwid',
+ array('hwtype' => DeviceType::SCREEN, 'hwname' => $screen['name']));
+ $hwids[$screen['name']] = $hwid;
+ }
+ // Now add new entries
+ $machinehwid = Database::insertIgnore('machine_x_hw', 'machinehwid', array(
+ 'hwid' => $hwid,
+ 'machineuuid' => $uuid,
+ 'devpath' => $port,
+ ), array('disconnecttime' => 0));
+ $validProps = array();
+ if (count($screen) > 1) {
+ // Screen has additional properties (resolution, size, etc.)
+ unset($screen['name']);
+ foreach ($screen as $key => $value) {
+ if (!preg_match('/^[a-zA-Z0-9][\x21-\x7e]{0,15}$/', $key)) {
+ echo "No matsch '$key'\n";
+ continue; // Ignore evil key names
+ }
+ $validProps[] = $key;
+ Database::exec("INSERT INTO machine_x_hw_prop (machinehwid, prop, value)"
+ . " VALUES (:id, :key, :value) ON DUPLICATE KEY UPDATE value = VALUES(value)", array(
+ 'id' => $machinehwid,
+ 'key' => $key,
+ 'value' => $value,
+ ));
+ }
+ }
+ // Purge properties that might have existed in the past
+ if (empty($validProps)) {
+ Database::exec("DELETE FROM machine_x_hw_prop WHERE machinehwid = :machinehwid AND prop NOT LIKE '@%'",
+ array('machinehwid' => $machinehwid));
+ } else {
+ $qs = '?' . str_repeat(',?', count($validProps) - 1);
+ array_unshift($validProps, $machinehwid);
+ Database::exec("DELETE FROM machine_x_hw_prop"
+ . " WHERE machinehwid = ? AND prop NOT LIKE '@%' AND prop NOT IN ($qs)",
+ $validProps);
+ }
+ }
+ // Remove/disable stale entries
+ if (empty($hwids)) {
+ // No screens connected at all, purge all screen entries for this machine
+ Database::exec("UPDATE machine_x_hw x, statistic_hw h"
+ . " SET x.disconnecttime = UNIX_TIMESTAMP()"
+ . " WHERE x.machineuuid = :uuid AND x.hwid = h.hwid AND h.hwtype = :type AND x.disconnecttime = 0",
+ array('uuid' => $uuid, 'type' => DeviceType::SCREEN));
+ } else {
+ // Some screens connected, make sure old entries get removed
+ $params = array_values($hwids);
+ array_unshift($params, $uuid);
+ array_unshift($params, DeviceType::SCREEN);
+ $qs = '?' . str_repeat(',?', count($hwids) - 1);
+ Database::exec("UPDATE machine_x_hw x, statistic_hw h"
+ . " SET x.disconnecttime = UNIX_TIMESTAMP()"
+ . " WHERE h.hwid = x.hwid AND x.disconnecttime = 0 AND h.hwtype = ? AND x.machineuuid = ? AND x.hwid NOT IN ($qs)", $params);
+
+ }
+ }
+ } else {
+ die("INVALID ACTION '$type'");
}
die("OK. (RESULT=0)\n");
}
diff --git a/modules-available/statistics/hooks/config-tgz.inc.php b/modules-available/statistics/hooks/config-tgz.inc.php
new file mode 100644
index 00000000..1272a94f
--- /dev/null
+++ b/modules-available/statistics/hooks/config-tgz.inc.php
@@ -0,0 +1,32 @@
+<?php
+
+$res = Database::simpleQuery('SELECT h.hwname FROM statistic_hw h'
+ . " INNER JOIN statistic_hw_prop p ON (h.hwid = p.hwid AND p.prop = :projector)"
+ . " WHERE h.hwtype = :screen ORDER BY h.hwname ASC", array(
+ 'projector' => 'projector',
+ 'screen' => DeviceType::SCREEN,
+));
+
+$content = '';
+while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ $content .= $row['hwname'] . "=beamer\n";
+}
+
+if (!empty($content)) {
+ $tmpfile = '/tmp/bwlp-' . md5($content) . '.tar';
+ if (!is_file($tmpfile) || !is_readable($tmpfile) || filemtime($tmpfile) + 86400 < time()) {
+ if (file_exists($tmpfile)) {
+ unlink($tmpfile);
+ }
+ try {
+ $a = new PharData($tmpfile);
+ $a->addFromString("/opt/openslx/beamergui/beamer.conf", $content);
+ $file = $tmpfile;
+ } catch (Exception $e) {
+ EventLog::failure('Could not include beamer.conf in config.tgz', (string)$e);
+ unlink($tmpfile);
+ }
+ } elseif (is_file($tmpfile) && is_readable($tmpfile)) {
+ $file = $tmpfile;
+ }
+}
diff --git a/modules-available/statistics/inc/devicetype.inc.php b/modules-available/statistics/inc/devicetype.inc.php
new file mode 100644
index 00000000..41ee237d
--- /dev/null
+++ b/modules-available/statistics/inc/devicetype.inc.php
@@ -0,0 +1,6 @@
+<?php
+
+class DeviceType
+{
+ const SCREEN = 'SCREEN';
+}
diff --git a/modules-available/statistics/install.inc.php b/modules-available/statistics/install.inc.php
index 7baf046e..79346f99 100644
--- a/modules-available/statistics/install.inc.php
+++ b/modules-available/statistics/install.inc.php
@@ -12,7 +12,7 @@ $res[] = tableCreate('statistic', "
`dateline` int(10) unsigned NOT NULL,
`typeid` varchar(30) NOT NULL,
`clientip` varchar(40) NOT NULL,
- `machineuuid` varchar(36) CHARACTER SET ascii DEFAULT NULL,
+ `machineuuid` char(36) CHARACTER SET ascii DEFAULT NULL,
`username` varchar(30) NOT NULL,
`data` varchar(255) NOT NULL,
PRIMARY KEY (`logid`),
@@ -24,7 +24,7 @@ $res[] = tableCreate('statistic', "
// Main table containing all known clients
-$res[] = tableCreate('machine', "
+$res[] = $machineCreate = tableCreate('machine', "
`machineuuid` char(36) CHARACTER SET ascii NOT NULL,
`fixedlocationid` int(11) DEFAULT NULL COMMENT 'Manually set location (e.g. roomplanner)',
`subnetlocationid` int(11) DEFAULT NULL COMMENT 'Automatically determined location (e.g. from subnet match),
@@ -61,6 +61,40 @@ $res[] = tableCreate('machine', "
KEY `systemmodel` (`systemmodel`)
");
+$res[] = $machineHwCreate = tableCreate('machine_x_hw', "
+ `machinehwid` int(10) unsigned NOT NULL AUTO_INCREMENT,
+ `hwid` int(10) unsigned NOT NULL,
+ `machineuuid` char(36) CHARACTER SET ascii NOT NULL,
+ `devpath` char(50) CHARACTER SET ascii NOT NULL,
+ `disconnecttime` int(10) unsigned NOT NULL COMMENT 'time the device was not connected to the pc anymore for the first time, 0 if it is connected',
+ PRIMARY KEY (`machinehwid`),
+ UNIQUE KEY `hwid` (`hwid`,`machineuuid`,`devpath`),
+ KEY `machineuuid` (`machineuuid`,`hwid`),
+ KEY `disconnecttime` (`disconnecttime`)
+ ");
+
+$res[] = tableCreate('machine_x_hw_prop', "
+ `machinehwid` int(10) unsigned NOT NULL,
+ `prop` char(16) CHARACTER SET ascii NOT NULL,
+ `value` varchar(500) NOT NULL,
+ PRIMARY KEY (`machinehwid`,`prop`)
+");
+
+$res[] = tableCreate('statistic_hw', "
+ `hwid` int(10) unsigned NOT NULL AUTO_INCREMENT,
+ `hwtype` char(11) CHARACTER SET ascii NOT NULL,
+ `hwname` varchar(200) NOT NULL,
+ PRIMARY KEY (`hwid`),
+ UNIQUE KEY `hwtype` (`hwtype`,`hwname`)
+");
+
+$res[] = tableCreate('statistic_hw_prop', "
+ `hwid` int(10) unsigned NOT NULL,
+ `prop` char(16) CHARACTER SET ascii NOT NULL,
+ `value` varchar(500) NOT NULL,
+ PRIMARY KEY (`hwid`,`prop`)
+");
+
// PCI-ID cache
$res[] = tableCreate('pciid', "
@@ -71,7 +105,8 @@ $res[] = tableCreate('pciid', "
PRIMARY KEY (`category`,`id`)
");
-if (in_array(UPDATE_DONE, $res)) {
+// need trigger?
+if ($machineCreate === UPDATE_DONE) {
$addTrigger = true;
}
@@ -165,6 +200,25 @@ if ($addTrigger) {
}
}
+if ($machineHwCreate === UPDATE_DONE) {
+ $ret = Database::exec('ALTER TABLE `machine_x_hw`
+ ADD CONSTRAINT `machine_x_hw_ibfk_1` FOREIGN KEY (`hwid`) REFERENCES `statistic_hw` (`hwid`) ON DELETE CASCADE,
+ ADD CONSTRAINT `machine_x_hw_ibfk_2` FOREIGN KEY (`machineuuid`) REFERENCES `machine` (`machineuuid`) ON DELETE CASCADE');
+ if ($ret === false) {
+ finalResponse(UPDATE_FAILED, 'Adding constraints to machine_x_hw failed: ' . Database::lastError());
+ }
+ $ret = Database::exec('ALTER TABLE `machine_x_hw_prop`
+ ADD CONSTRAINT `machine_x_hw_prop_ibfk_1` FOREIGN KEY (`machinehwid`) REFERENCES `machine_x_hw` (`machinehwid`) ON DELETE CASCADE');
+ if ($ret === false) {
+ finalResponse(UPDATE_FAILED, 'Adding constraint to machine_x_hw_prop failed: ' . Database::lastError());
+ }
+ $ret = Database::exec('ALTER TABLE `statistic_hw_prop`
+ ADD CONSTRAINT `statistic_hw_prop_ibfk_1` FOREIGN KEY (`hwid`) REFERENCES `statistic_hw` (`hwid`) ON DELETE CASCADE');
+ if ($ret === false) {
+ finalResponse(UPDATE_FAILED, 'Adding constraint to statistic_hw_prop failed: ' . Database::lastError());
+ }
+}
+
// Create response
if (in_array(UPDATE_DONE, $res)) {
diff --git a/modules-available/statistics/lang/de/template-tags.json b/modules-available/statistics/lang/de/template-tags.json
index ca6c56a7..7274aef4 100644
--- a/modules-available/statistics/lang/de/template-tags.json
+++ b/modules-available/statistics/lang/de/template-tags.json
@@ -47,6 +47,7 @@
"lang_modelStats": "PC-Modelle",
"lang_more": "Mehr",
"lang_newMachines": "Neue Ger\u00e4te",
+ "lang_noProjectorsDefined": "Keine Beamer-Overrides definiert",
"lang_notes": "Anmerkungen",
"lang_onlineMachines": "Gestartete Clients",
"lang_partName": "Name",
@@ -55,17 +56,21 @@
"lang_partitionSize": "Gr\u00f6\u00dfe",
"lang_pendingSectors": "Potentiell defekte Sektoren",
"lang_powerOnTime": "Betriebszeit",
+ "lang_projector": "Beamer",
+ "lang_projectors": "Beamer",
"lang_ram": "Arbeitsspeicher",
"lang_ramSize": "Gr\u00f6\u00dfe",
"lang_ramSlots": "Speicher-Slots",
"lang_realCores": "Kerne",
"lang_reallocatedSectors": "Defekte Sektoren",
+ "lang_screens": "Bildschirme",
"lang_serialNo": "Serien-Nr",
"lang_showList": "Liste",
"lang_showVisualization": "Visualisierung",
"lang_sockets": "Sockel",
"lang_tempPart": "Temp. Partition",
"lang_tempPartStats": "Tempor\u00e4re Partition",
+ "lang_thoseAreProjectors": "Diese Modellnamen werden als Beamer behandelt, auch wenn die EDID-Informationen des Ger\u00e4tes anderes berichten.",
"lang_timebarDesc": "Visuelle Darstellung der letzten Tage. Rote Abschnitte zeigen, wann der Rechner belegt war, gr\u00fcne, wann er nicht verwendet wurde, aber eingeschaltet war. Die leicht abgedunkelten Abschnitte markieren N\u00e4chte (22 bis 8 Uhr).",
"lang_tmpGb": "HDD-Temp",
"lang_total": "Gesamt",
diff --git a/modules-available/statistics/lang/en/template-tags.json b/modules-available/statistics/lang/en/template-tags.json
index 55003ea9..4e135388 100644
--- a/modules-available/statistics/lang/en/template-tags.json
+++ b/modules-available/statistics/lang/en/template-tags.json
@@ -47,6 +47,7 @@
"lang_modelStats": "PC models",
"lang_more": "More",
"lang_newMachines": "New machines",
+ "lang_noProjectorsDefined": "No projector overrides defined",
"lang_notes": "Notes",
"lang_onlineMachines": "Online clients",
"lang_partName": "Name",
@@ -55,17 +56,21 @@
"lang_partitionSize": "Size",
"lang_pendingSectors": "Sectors pending reallocation",
"lang_powerOnTime": "Power on time",
+ "lang_projector": "Projector",
+ "lang_projectors": "Projectors",
"lang_ram": "Memory",
"lang_ramSize": "Size",
"lang_ramSlots": "Memory slots",
"lang_realCores": "Cores",
"lang_reallocatedSectors": "Bad sectors",
+ "lang_screens": "Screens",
"lang_serialNo": "Serial no",
"lang_showList": "Show list",
"lang_showVisualization": "Show visualization",
"lang_sockets": "Sockets",
"lang_tempPart": "Temp. partition",
"lang_tempPartStats": "Temporary partition",
+ "lang_thoseAreProjectors": "These model names will always be treated as beamers, even if the device's EDID data says otherwise.",
"lang_timebarDesc": "Visual representation of the last few days. Red parts mark periods where the client was occupied, green parts where the client was idle. Dimmed parts mark nights (10pm to 8am).",
"lang_tmpGb": "HDD temp",
"lang_total": "Total",
diff --git a/modules-available/statistics/page.inc.php b/modules-available/statistics/page.inc.php
index 6a9acd14..5ad8bc20 100644
--- a/modules-available/statistics/page.inc.php
+++ b/modules-available/statistics/page.inc.php
@@ -119,6 +119,53 @@ class Page_Statistics extends Page
/* TODO ... */
}
+ /*
+ * TODO: Move to separate unit... hardware configurator?
+ */
+
+ protected function handleProjector($action)
+ {
+ $hwid = Request::post('hwid', false, 'int');
+ if ($hwid === false) {
+ Util::traceError('Param hwid missing');
+ }
+ if ($action === 'addprojector') {
+ Database::exec('INSERT INTO statistic_hw_prop (hwid, prop, value)'
+ . ' VALUES (:hwid, :prop, :value)', array(
+ 'hwid' => $hwid,
+ 'prop' => 'projector',
+ 'value' => 'true',
+ ));
+ } else {
+ Database::exec('DELETE FROM statistic_hw_prop WHERE hwid = :hwid AND prop = :prop', array(
+ 'hwid' => $hwid,
+ 'prop' => 'projector',
+ ));
+ }
+ if (Module::isAvailable('sysconfig')) {
+ ConfigTgz::rebuildAllConfigs();
+ }
+ Util::redirect('?do=statistics&show=projectors');
+ }
+
+ protected function showProjectors()
+ {
+ $res = Database::simpleQuery('SELECT h.hwname, h.hwid FROM statistic_hw h'
+ . " INNER JOIN statistic_hw_prop p ON (h.hwid = p.hwid AND p.prop = :projector)"
+ . " WHERE h.hwtype = :screen ORDER BY h.hwname ASC", array(
+ 'projector' => 'projector',
+ 'screen' => DeviceType::SCREEN,
+ ));
+ $data = array(
+ 'projectors' => $res->fetchAll(PDO::FETCH_ASSOC)
+ );
+ Render::addTemplate('projector-list', $data);
+ }
+
+ /*
+ * End TODO
+ */
+
protected function doPreprocess()
{
$this->initConstants();
@@ -140,6 +187,8 @@ class Page_Statistics extends Page
));
Message::addSuccess('notes-saved');
Util::redirect('?do=Statistics&uuid=' . $uuid);
+ } elseif ($action === 'addprojector' || $action === 'delprojector') {
+ $this->handleProjector($action);
}
// Fix online state of machines that crashed -- TODO: Make cronjob for this
Database::exec("UPDATE machine SET lastboot = 0 WHERE lastseen < UNIX_TIMESTAMP() - 610");
@@ -174,6 +223,9 @@ class Page_Statistics extends Page
Render::closeTag('div');
$this->showMachineList($filterSet);
return;
+ } elseif ($show === 'projectors') {
+ $this->showProjectors();
+ return;
}
Render::openTag('div', array('class' => 'row'));
$this->showFilter('stat', $filterSet);
@@ -723,6 +775,24 @@ class Page_Statistics extends Page
}
$client['locations'] = $output;
}
+ // Screens TODO Move everything else to hw table instead of blob parsing above
+ // `devicetype`, `devicename`, `subid`, `machineuuid`
+ $res = Database::simpleQuery("SELECT m.hwid, h.hwname, m.devpath AS connector, m.disconnecttime,"
+ . " p.value AS resolution, q.prop AS projector FROM machine_x_hw m"
+ . " INNER JOIN statistic_hw h ON (m.hwid = h.hwid AND h.hwtype = :screen)"
+ . " LEFT JOIN machine_x_hw_prop p ON (m.machinehwid = p.machinehwid AND p.prop = 'resolution')"
+ . " LEFT JOIN statistic_hw_prop q ON (m.hwid = q.hwid AND q.prop = 'projector')"
+ . " WHERE m.machineuuid = :uuid",
+ array('screen' => DeviceType::SCREEN, 'uuid' => $uuid));
+ $client['screens'] = array();
+ $ports = array();
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ if ($row['disconnecttime'] != 0)
+ continue;
+ $ports[] = $row['connector'];
+ $client['screens'][] = $row;
+ }
+ array_multisort($ports, SORT_ASC, $client['screens']);
// Throw output at user
Render::addTemplate('machine-main', $client);
// Sessions
@@ -806,7 +876,7 @@ class Page_Statistics extends Page
// Client log
if (Module::get('syslog') !== false) {
$lres = Database::simpleQuery('SELECT logid, dateline, logtypeid, clientip, description, extra FROM clientlog'
- . ' WHERE clientip = :clientip ORDER BY logid DESC LIMIT 25', array('clientip' => $client['clientip']));
+ . ' WHERE machineuuid = :uuid ORDER BY logid DESC LIMIT 25', array('uuid' => $client['machineuuid']));
$today = date('d.m.Y');
$yesterday = date('d.m.Y', time() - 86400);
$count = 0;
diff --git a/modules-available/statistics/templates/machine-main.html b/modules-available/statistics/templates/machine-main.html
index 0b333a27..bdc51167 100644
--- a/modules-available/statistics/templates/machine-main.html
+++ b/modules-available/statistics/templates/machine-main.html
@@ -130,6 +130,36 @@
<td class="text-nowrap">{{lang_64bitSupport}}</td>
<td>{{kvmstate}}</td>
</tr>
+ <tr>
+ <td class="text-nowrap">{{lang_screens}}</td>
+ <td>
+ <form method="post" action="?do=statistics" id="delprojector">
+ <input type="hidden" name="token" value="{{token}}">
+ <input type="hidden" name="action" value="delprojector">
+ </form>
+ <form method="post" action="?do=statistics" id="addprojector">
+ <input type="hidden" name="token" value="{{token}}">
+ <input type="hidden" name="action" value="addprojector">
+ </form>
+ {{#screens}}
+ <div class="small">
+ <div class="pull-right btn-group btn-group-xs">
+ {{#projector}}
+ <a href="?do=statistics&amp;show=projectors" class="btn btn-default">{{lang_projector}}</a>
+ <button form="delprojector" type="submit" name="hwid" value="{{hwid}}"
+ class="btn btn-danger"><span class="glyphicon glyphicon-remove"></span></button>
+ {{/projector}}
+ {{^projector}}
+ <button form="addprojector" type="submit" name="hwid" value="{{hwid}}"
+ class="btn btn-success"><span class="glyphicon glyphicon-plus"></span> {{lang_projector}}</button>
+ {{/projector}}
+ </div>
+ {{connector}}: <b>{{hwname}}</b> {{resolution}}
+ <div class="clearfix"></div>
+ </div>
+ {{/screens}}
+ </td>
+ </tr>
</table>
<h4>{{lang_devices}}</h4>
{{#lspci1}}
diff --git a/modules-available/statistics/templates/projector-list.html b/modules-available/statistics/templates/projector-list.html
new file mode 100644
index 00000000..bc9ecdbd
--- /dev/null
+++ b/modules-available/statistics/templates/projector-list.html
@@ -0,0 +1,21 @@
+<div class="panel panel-default">
+ <div class="panel-heading">{{lang_projectors}}</div>
+ <div class="panel-body">
+ <form method="post" action="?do=statistics" id="delprojector">
+ <input type="hidden" name="token" value="{{token}}">
+ <input type="hidden" name="action" value="delprojector">
+ <p>{{lang_thoseAreProjectors}}</p>
+ {{#projectors}}
+ <div>
+ <button type="submit" name="hwid" value="{{hwid}}" class="btn btn-danger">
+ <span class="glyphicon glyphicon-remove"></span>
+ </button>
+ {{hwname}}
+ </div>
+ {{/projectors}}
+ {{^projectors}}
+ <div class="alert alert-info">{{lang_noProjectorsDefined}}</div>
+ {{/projectors}}
+ </form>
+ </div>
+</div> \ No newline at end of file
diff --git a/modules-available/statistics_reporting/config.json b/modules-available/statistics_reporting/config.json
new file mode 100644
index 00000000..f9627cdb
--- /dev/null
+++ b/modules-available/statistics_reporting/config.json
@@ -0,0 +1,4 @@
+{
+ "category": "main.content",
+ "dependencies": [ "statistics", "locations", "js_stupidtable", "js_jqueryui" ]
+}
diff --git a/modules-available/statistics_reporting/hooks/cron.inc.php b/modules-available/statistics_reporting/hooks/cron.inc.php
new file mode 100644
index 00000000..afb18a23
--- /dev/null
+++ b/modules-available/statistics_reporting/hooks/cron.inc.php
@@ -0,0 +1,26 @@
+<?php
+
+if (RemoteReport::isReportingEnabled()) {
+ $nextReporting = RemoteReport::getReportingTimestamp();
+
+ // It's time to generate a new report
+ while ($nextReporting <= time()) {
+ RemoteReport::writeNextReportingTimestamp();
+
+ $from = strtotime("-7 days", $nextReporting);
+ $to = $nextReporting;
+
+ $statisticsReport = json_encode(RemoteReport::generateReport($from, $to));
+
+ $params = array("action" => "statistics", "data" => $statisticsReport);
+
+ $result = Download::asStringPost(CONFIG_REPORTING_URL, $params, 30, $code);
+
+ if ($code != 200) {
+ EventLog::warning("Statistics Reporting failed: " . $code, $result);
+ } else {
+ EventLog::info('Statistics report sent to ' . CONFIG_REPORTING_URL);
+ }
+ $nextReporting = strtotime("+7 days", $nextReporting);
+ }
+} \ No newline at end of file
diff --git a/modules-available/statistics_reporting/hooks/main-warning.inc.php b/modules-available/statistics_reporting/hooks/main-warning.inc.php
new file mode 100644
index 00000000..33381c9f
--- /dev/null
+++ b/modules-available/statistics_reporting/hooks/main-warning.inc.php
@@ -0,0 +1,5 @@
+<?php
+
+if (!RemoteReport::isReportingEnabled()) {
+ Message::addInfo('statistics_reporting.remote-report-disabled', true);
+}
diff --git a/modules-available/statistics_reporting/inc/getdata.inc.php b/modules-available/statistics_reporting/inc/getdata.inc.php
new file mode 100644
index 00000000..da3a9a26
--- /dev/null
+++ b/modules-available/statistics_reporting/inc/getdata.inc.php
@@ -0,0 +1,157 @@
+<?php
+
+define('GETDATA_ANONYMOUS', 1);
+define('GETDATA_PRINTABLE', 2);
+
+class GetData
+{
+ public static $from;
+ public static $to;
+ public static $lowerTimeBound = 0;
+ public static $upperTimeBound = 24;
+ public static $salt;
+
+ // total
+ public static function total($flags = 0) {
+ $printable = 0 !== ($flags & GETDATA_PRINTABLE);
+ // total time online, average time online, total number of logins
+ $res = Queries::getOverallStatistics(self::$from, self::$to, self::$lowerTimeBound, self::$upperTimeBound);
+ $row = $res->fetch(PDO::FETCH_ASSOC);
+ $data = array('totalTime' => $row['sum'], 'medianSessionLength' => self::calcMedian($row['median']), 'longSessions' => $row['longSessions'], 'shortSessions' => $row['shortSessions']);
+
+ //total time offline
+ $res = Queries::getTotalOfflineStatistics(self::$from, self::$to, self::$lowerTimeBound, self::$upperTimeBound);
+ $row = $res->fetch(PDO::FETCH_ASSOC);
+ $data['totalOffTime'] = $row['timeOff'];
+
+ if ($printable) {
+ $data["totalTime_s"] = self::formatSeconds($data["totalTime"]);
+ $data["medianSessionLength_s"] = self::formatSeconds($data["medianSessionLength"]);
+ $data["totalOffTime_s"] = self::formatSeconds($data["totalOffTime"]);
+ }
+
+ return $data;
+ }
+
+ // per location
+ public static function perLocation($flags = 0) {
+ $anonymize = 0 !== ($flags & GETDATA_ANONYMOUS);
+ $printable = 0 !== ($flags & GETDATA_PRINTABLE);
+ $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']),
+ 'totalTime' => $row['timeSum'],
+ 'medianSessionLength' => $median,
+ 'totalOffTime' => $row['offlineSum'],
+ 'longSessions' => $row['longSessions'],
+ 'shortSessions' => $row['shortSessions']
+ );
+ if (!$anonymize) {
+ $entry['locationId'] = $row['locId'];
+ }
+ if ($printable) {
+ $entry['totalTime_s'] = self::formatSeconds($row['timeSum']);
+ $entry['medianSessionLength_s'] = self::formatSeconds($median);
+ $entry['totalOffTime_s'] = self::formatSeconds($row['offlineSum']);
+ }
+ $data[] = $entry;
+ }
+ return $data;
+ }
+
+ // per client
+ public static function perClient($flags = 0) {
+ $anonymize = 0 !== ($flags & GETDATA_ANONYMOUS);
+ $printable = 0 !== ($flags & GETDATA_PRINTABLE);
+ $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']),
+ 'totalTime' => $row['timeSum'],
+ 'medianSessionLength' => $median,
+ 'totalOffTime' => $row['offlineSum'],
+ 'lastStart' => $row['lastStart'],
+ 'lastLogout' => $row['lastLogout'],
+ 'longSessions' => $row['longSessions'],
+ 'shortSessions' => $row['shortSessions'],
+ 'location' => ($anonymize ? $row['locHash'] : $row['locName']),
+ );
+ if (!$anonymize) {
+ $entry['locationId'] = $row['locId'];
+ }
+ if ($printable) {
+ $entry['totalTime_s'] = self::formatSeconds($row['timeSum']);
+ $entry['medianSessionLength_s'] = self::formatSeconds($median);
+ $entry['totalOffTime_s'] = self::formatSeconds($row['offlineSum']);
+ $entry['lastStart_s'] = $row['lastStart'] == 0 ? "" : date(DATE_ISO8601, $row['lastStart']);
+ $entry['lastLogout_s'] = $row['lastLogout'] == 0 ? "" : date(DATE_ISO8601, $row['lastLogout']);
+ }
+ $data[] = $entry;
+ }
+ return $data;
+ }
+
+ // per user
+ public static function perUser($flags = 0) {
+ $anonymize = 0 !== ($flags & GETDATA_ANONYMOUS);
+ $res = Queries::getUserStatistics(self::$from, self::$to, self::$lowerTimeBound, self::$upperTimeBound);
+ $data = array();
+ $user = $anonymize ? 'userHash' : 'name';
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ $data[] = array('user' => $row[$user], 'sessions' => $row['count']);
+ }
+ return $data;
+ }
+
+
+ // per vm
+ public static function perVM($flags = 0) {
+ $anonymize = 0 !== ($flags & GETDATA_ANONYMOUS);
+ $res = Queries::getVMStatistics(self::$from, self::$to, self::$lowerTimeBound, self::$upperTimeBound);
+ $data = array();
+ $vm = $anonymize ? 'vmHash' : 'name';
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ self::nullToZero($row);
+ $data[] = array('vm' => $row[$vm], 'sessions' => $row['count']);
+ }
+ return $data;
+ }
+
+ private static function nullToZero(&$row)
+ {
+ foreach ($row as &$field) {
+ if (is_null($field)) {
+ $field = 0;
+ }
+ }
+ }
+
+ // Format $seconds into ".d .h .m .s" format (day, hour, minute, second)
+ private static function formatSeconds($seconds)
+ {
+ return sprintf('%dd, %02d:%02d:%02d', $seconds / (3600*24), ($seconds % (3600*24)) / 3600, ($seconds%3600) / 60, $seconds%60);
+ }
+
+ // Calculate Median
+ private static function calcMedian($string) {
+ $arr = explode(",", $string);
+ sort($arr, SORT_NUMERIC);
+ $count = count($arr); //total numbers in array
+ $middleval = floor(($count-1)/2); // find the middle value, or the lowest middle value
+ if($count % 2) { // odd number, middle is the median
+ $median = $arr[(int) $middleval];
+ } else { // even number, calculate avg of 2 medians
+ $low = $arr[(int) $middleval];
+ $high = $arr[(int) $middleval+1];
+ $median = (($low+$high)/2);
+ }
+ return round($median);
+ }
+} \ No newline at end of file
diff --git a/modules-available/statistics_reporting/inc/queries.inc.php b/modules-available/statistics_reporting/inc/queries.inc.php
new file mode 100644
index 00000000..2269e764
--- /dev/null
+++ b/modules-available/statistics_reporting/inc/queries.inc.php
@@ -0,0 +1,217 @@
+<?php
+
+
+class Queries
+{
+
+ // Client Data: Name, Time Online, Median Time Online, Time Offline, last start, last logout, Last Time Booted, Number of Sessions > 60Sec, Number of Sessions < 60Sec, name of location, id of location (anonymized), machine uuid (anonymized)
+ 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 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
+ 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
+ RIGHT JOIN machine ON offlineTable.machineuuid = machine.machineuuid
+ LEFT JOIN location ON machine.locationid = location.locationid
+ GROUP BY machine.machineuuid
+ ) t2
+ ON t1.uuid = t2.uuid", array("salt" => GetData::$salt));
+
+ return $res;
+ }
+
+ // Location Data: Name, ID (anonymized), Time Online, Median Time Online, Time Offline, Number of Sessions > 60Sec, Number of Sessions < 60Sec
+ 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 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
+ 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
+ RIGHT JOIN machine ON offlineTable.machineuuid = machine.machineuuid
+ LEFT JOIN location ON machine.locationid = location.locationid
+ GROUP BY machine.locationid
+ ) t2
+ ON t1.locId = t2.locId", array("salt" => GetData::$salt));
+ return $res;
+ }
+
+ // User Data: Name, Name(anonymized), Number of Logins
+ public static function getUserStatistics($from, $to, $lowerTimeBound = 0, $upperTimeBound = 24) {
+ $res = Database::simpleQuery("SELECT username AS name, IF(username = 'anonymous', 'anonymous', md5(CONCAT(username, :salt))) AS userHash, COUNT(*) AS 'count'
+ FROM statistic
+ WHERE typeid='.vmchooser-session-name' AND dateline >= $from and dateline <= $to
+ AND FROM_UNIXTIME(dateline, '%H') >= $lowerTimeBound AND FROM_UNIXTIME(dateline, '%H') < $upperTimeBound
+ GROUP BY username
+ ORDER BY 2 DESC", array("salt" => GetData::$salt));
+ return $res;
+ }
+
+ // Virtual Machine Data: Name, Number of Usages
+ public static function getVMStatistics($from, $to, $lowerTimeBound = 0, $upperTimeBound = 24) {
+ $res = Database::simpleQuery("SELECT data AS name, MD5(CONCAT(data, :salt)) AS vmHash, COUNT(*) AS 'count'
+ FROM statistic
+ WHERE typeid='.vmchooser-session-name' AND dateline >= $from and dateline <= $to
+ AND FROM_UNIXTIME(dateline, '%H') >= $lowerTimeBound AND FROM_UNIXTIME(dateline, '%H') < $upperTimeBound
+ GROUP BY data
+ ORDER BY 2 DESC", array("salt" => GetData::$salt));
+ return $res;
+ }
+
+ //Total Data: Time Online, Median Time Online, Number of Sessions > 60Sec, Number of Sessions < 60Sec
+ public static function getOverallStatistics ($from, $to, $lowerTimeBound = 0, $upperTimeBound = 24) {
+ Database::exec("SET SESSION group_concat_max_len = 1000000000");
+ $res = Database::simpleQuery("SELECT SUM(CAST(sessionTable.length AS UNSIGNED)) AS sum, GROUP_CONCAT(sessionTable.length) AS median, SUM(sessionTable.length >= 60) AS longSessions, SUM(sessionTable.length < 60) AS shortSessions
+ FROM ".self::getBoundedTableQueryString('~session-length', $from, $to, $lowerTimeBound, $upperTimeBound)." sessionTable");
+ return $res;
+ }
+
+ // Total Data(2): Time Offline
+ public static function getTotalOfflineStatistics($from, $to, $lowerTimeBound = 0, $upperTimeBound = 24) {
+ $res = Database::simpleQuery("SELECT SUM(CAST(offlineTable.length AS UNSIGNED)) AS timeOff
+ FROM ".self::getBoundedTableQueryString('~offline-length', $from, $to, $lowerTimeBound, $upperTimeBound)." offlineTable");
+ return $res;
+ }
+
+ // query string which provides table with time-cutoff and time-bounds
+ private static function getBoundedTableQueryString($typeid, $from, $to, $lowerTimeBound, $upperTimeBound)
+ {
+ // get Clients that are currently oflfine (the offline time is not yet recorded in the statistic table)
+ $union = $typeid == '~offline-length' ?
+ "union
+ select CAST(IF(lastseen < $from, $from, lastseen) as UNSIGNED) as start, $to as end,
+ '~offline-length' as typeid, machineuuid, 'machine'
+ from machine where lastseen <= $to and UNIX_TIMESTAMP() - lastseen >= 600" : "";
+
+
+ $lowerFormat = "'%y-%m-%d $lowerTimeBound:00:00'";
+ $upperFormat = "'%y-%m-%d ".($upperTimeBound-1).":59:59'";
+ $queryString = "
+ select
+
+ # The whole length of the session/offline time.
+ (end-start
+
+ # Now the time that is not within the daily time bounds is subtracted.
+ # This includes the time before the first daily bound, the time after the last daily bound
+ # and the time between the daily bounds (if a session/offline time spans multiple days)
+
+ # Time before the first daily bound is subtracted.
+ - IF(
+ start > startUpper,
+ UNIX_TIMESTAMP(FROM_UNIXTIME(start, $lowerFormat) + INTERVAL 1 DAY) - start,
+ IF(
+ start < startLower,
+ startLower - start,
+ 0
+ )
+ )
+
+ # Time after the last daily bound is subtracted.
+ - IF(
+ end > endUpper,
+ end - (endUpper + 1),
+ IF(
+ end < endLower,
+ end - (UNIX_TIMESTAMP(FROM_UNIXTIME(end, $upperFormat) - INTERVAL 1 DAY) + 1),
+ 0
+ )
+ )
+
+ # Time between the daily bounds is subtracted.
+ - ( daysDiff - 2
+ + IF(start <= startUpper, 1, 0)
+ + IF(end >= endLower, 1, 0)
+ ) * ((24 - ($upperTimeBound - $lowerTimeBound)) * 3600)
+
+ # If the session crossed a clock change (to/from daylight saving time), the last subtraction may have subtracted
+ # one hour too much or too little. This IF will correct this.
+ - IF(
+ innerStart < innerEnd,
+ IF(
+ timeDiff = 1 AND ($lowerTimeBound >= 2 OR $upperTimeBound <= 2),
+ 3600,
+ IF(timeDiff = -1 AND ($lowerTimeBound >= 3 OR $upperTimeBound <= 3), -3600, 0)
+ ),
+ 0
+ )
+
+ ) as 'length',
+
+ IF(end < endUpper AND end > endLower AND end < $to, end, 0) as endInBound,
+
+ machineuuid
+
+
+ # These nested selects are necessary because some things need to be calculated before others.
+ # (e.g. start is needed to calculate startLower)
+ from (
+ select
+ *,
+
+ # timeDiff is the clock change between innerStart and innerEnd. ( 0 = no clock change)
+ ((CAST(date_format(from_unixtime(innerStart), '%H') as SIGNED) -
+ CAST(date_format(convert_tz(from_unixtime(innerStart), @@session.time_zone, '+00:00'), '%H') as SIGNED) + 24) % 24
+ -
+ (CAST(date_format(from_unixtime(innerEnd), '%H') as SIGNED) -
+ CAST(date_format(convert_tz(from_unixtime(innerEnd), @@session.time_zone, '+00:00'), '%H') as SIGNED) + 24) % 24) as timeDiff
+ from (
+ select
+ *,
+
+ # innerStart and innerEnd are start and end but excluding the time before the first daily upper bound and after the last daily lower bound.
+ CAST(IF(start <= startUpper, startUpper, UNIX_TIMESTAMP(FROM_UNIXTIME(start, $upperFormat) + INTERVAL 1 DAY)) as UNSIGNED) as innerStart,
+ CAST(IF(end >= endLower, endLower, UNIX_TIMESTAMP(FROM_UNIXTIME(end, $lowerFormat) - INTERVAL 1 DAY)) as UNSIGNED) as innerEnd
+ from (
+ select
+ *,
+
+ # daysDiff = how many different days the start and end are apart (0 = start and end on the same day)
+ (TO_DAYS(FROM_UNIXTIME(end, '%y-%m-%d')) - TO_DAYS(FROM_UNIXTIME(start, '%y-%m-%d'))) as daysDiff,
+
+ # startLower = lower daily time bound on the starting day
+ CAST(UNIX_TIMESTAMP(FROM_UNIXTIME(start, $lowerFormat)) as UNSIGNED) as startLower,
+ # startUpper = upper daily time bound on the starting day
+ CAST(UNIX_TIMESTAMP(FROM_UNIXTIME(start, $upperFormat)) as UNSIGNED) as startUpper,
+ # endLower = lower daily time bound on the ending day
+ CAST(UNIX_TIMESTAMP(FROM_UNIXTIME(end, $lowerFormat)) as UNSIGNED) as endLower,
+ # endUpper = upper daily time bound on the ending day
+ CAST(UNIX_TIMESTAMP(FROM_UNIXTIME(end, $upperFormat)) as UNSIGNED) as endUpper
+ from (
+ # Statistic logs (combined with currently offline machines if offline times are requested) .
+ select CAST(IF(dateline < $from, $from, dateline) as UNSIGNED) as start,
+ CAST(IF(dateline+data > $to, $to, dateline+data) as UNSIGNED) as end,
+ typeid, machineuuid, 'statistic'
+ from statistic where dateline+data >= $from and dateline <= $to and typeid = '$typeid'
+ $union
+ ) t
+ ) t
+ ) t
+ ) t
+
+
+ # Filter out the session that are at least overlapping with the time bounds.
+ where (
+ (daysDiff = 0 and (start <= UNIX_TIMESTAMP(FROM_UNIXTIME(start, $upperFormat)) and end >= UNIX_TIMESTAMP(FROM_UNIXTIME(end, $lowerFormat))))
+ or
+ (daysDiff = 1 and (start <= UNIX_TIMESTAMP(FROM_UNIXTIME(start, $upperFormat)) or end >= UNIX_TIMESTAMP(FROM_UNIXTIME(end, $lowerFormat))))
+ or
+ daysDiff >= 2
+ )
+ ";
+ return "(".$queryString.")";
+ }
+}
+
diff --git a/modules-available/statistics_reporting/inc/remotereport.inc.php b/modules-available/statistics_reporting/inc/remotereport.inc.php
new file mode 100644
index 00000000..4c5f604b
--- /dev/null
+++ b/modules-available/statistics_reporting/inc/remotereport.inc.php
@@ -0,0 +1,82 @@
+<?php
+
+class RemoteReport
+{
+
+ const ENABLED_ID = 'statistics-reporting-enabled';
+ const NEXT_SUBMIT_ID = 'statistics-reporting-next';
+
+ /**
+ * Enable or disable remote reporting of usage statistics.
+ *
+ * @param bool|string $isEnabled true or 'on' if reporting should be enabled
+ */
+ public static function setReportingEnabled($isEnabled)
+ {
+ $value = ($isEnabled === true || $isEnabled === 'on') ? 'on' : '';
+ Property::set(self::ENABLED_ID, $value, 60 * 24 * 14);
+ }
+
+ /**
+ * Returns whether remote reporting is enabled or not.
+ *
+ * @return bool true if reporting is on, false if off
+ */
+ public static function isReportingEnabled()
+ {
+ return Property::get(self::ENABLED_ID, false) === 'on';
+ }
+
+ /**
+ * Get the timestamp of the end of the next 7 day interval to
+ * report statistics for. Usually if this is < time() you want
+ * to generate the report.
+ *
+ * @return int timestamp of the end of the reporting time frame
+ */
+ public static function getReportingTimestamp()
+ {
+ $ts = Property::get(self::NEXT_SUBMIT_ID, 0);
+ if ($ts === 0) {
+ // No timestamp stored yet - might be a fresh install
+ // schedule for next time
+ self::writeNextReportingTimestamp();
+ $ts = Property::get(self::NEXT_SUBMIT_ID, 0);
+ } elseif ($ts < strtotime('last monday')) {
+ // Too long ago, move forward to last monday
+ $ts = strtotime('last monday');
+ }
+ return $ts;
+ }
+
+ /**
+ * Update the timestamp of the next scheduled statistics report.
+ * This sets the end of the next 7 day interval to the start of
+ * next monday (00:00).
+ */
+ public static function writeNextReportingTimestamp()
+ {
+ Property::set(self::NEXT_SUBMIT_ID, strtotime('next monday'), 60 * 24 * 14);
+ }
+
+ /**
+ * Generate the multi-dimensional array containing the anonymized
+ * (weekly) statistics to report.
+ *
+ * @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, false));
+ $data = GetData::total(GETDATA_ANONYMOUS);
+ $data['perLocation'] = GetData::perLocation(GETDATA_ANONYMOUS);
+ $data['perVM'] = GetData::perVM(GETDATA_ANONYMOUS);
+ $data['tsFrom'] = $from;
+ $data['tsTo'] = $to;
+ return $data;
+ }
+
+} \ No newline at end of file
diff --git a/modules-available/statistics_reporting/lang/de/messages.json b/modules-available/statistics_reporting/lang/de/messages.json
new file mode 100644
index 00000000..0121e49a
--- /dev/null
+++ b/modules-available/statistics_reporting/lang/de/messages.json
@@ -0,0 +1,4 @@
+{
+ "invalid-table-type": "Ung\u00fcltiger Tabellentyp: {{0}}",
+ "remote-report-disabled": "Anonymer Statistikreport ist deaktiviert :-("
+} \ No newline at end of file
diff --git a/modules-available/statistics_reporting/lang/de/module.json b/modules-available/statistics_reporting/lang/de/module.json
new file mode 100644
index 00000000..64003d47
--- /dev/null
+++ b/modules-available/statistics_reporting/lang/de/module.json
@@ -0,0 +1,10 @@
+{
+ "module_name": "Statistikauswertung",
+ "notAssigned": "Nicht zugewiesen",
+ "page_title": "Statistikauswertung",
+ "table_client": "Nach Clients",
+ "table_location": "Nach Orten",
+ "table_total": "Gesamt",
+ "table_user": "Nach Benutzern",
+ "table_vm": "Nach Veranstaltungen"
+} \ No newline at end of file
diff --git a/modules-available/statistics_reporting/lang/de/template-tags.json b/modules-available/statistics_reporting/lang/de/template-tags.json
new file mode 100644
index 00000000..3801f3a6
--- /dev/null
+++ b/modules-available/statistics_reporting/lang/de/template-tags.json
@@ -0,0 +1,22 @@
+{
+ "lang_apply": "Anwenden",
+ "lang_displayColumns": "Auswahl angezeigter Spalten",
+ "lang_displaySelection": "Anzeigemodus, Auswahl Zeitfenster",
+ "lang_downloadReport": "Report herunterladen",
+ "lang_export": "Exportieren",
+ "lang_hostname": "Hostname",
+ "lang_lastLogout": "Letzter Logout",
+ "lang_lastStart": "Letzter Boot",
+ "lang_location": "Ort",
+ "lang_longSessions": "Sitzungen \u2265 60s",
+ "lang_medianSessionLength": "Sitzungsdauer Median",
+ "lang_reportingDescription": "Helfen Sie uns bwLehrpool durch das w\u00f6chentliche Verschicken eines anonymisierten Statistikberichts zu verbessern. Wenn Sie den Inhalt eines solchen Reports genauer inspizieren wollen, k\u00f6nnen Sie \u00fcber den untenstehenden Button einen aktuellen Report ihres Servers herunterladen.",
+ "lang_reportingLabel": "Anonymisierte Nutzungsstatistiken \u00fcbermitteln",
+ "lang_sessions": "Sitzungen",
+ "lang_shortSessions": "Sitzungen < 60s",
+ "lang_total": "Gesamt",
+ "lang_totalOffTime": "Gesamtzeit offline",
+ "lang_totalTime": "Gesamtzeit",
+ "lang_user": "Nutzer",
+ "lang_vm": "Veranstaltung"
+} \ No newline at end of file
diff --git a/modules-available/statistics_reporting/lang/en/messages.json b/modules-available/statistics_reporting/lang/en/messages.json
new file mode 100644
index 00000000..cd423426
--- /dev/null
+++ b/modules-available/statistics_reporting/lang/en/messages.json
@@ -0,0 +1,4 @@
+{
+ "invalid-table-type": "Invalid table type: {{0}}",
+ "remote-report-disabled": "Anonymous statistics report is disabled :-("
+} \ No newline at end of file
diff --git a/modules-available/statistics_reporting/lang/en/module.json b/modules-available/statistics_reporting/lang/en/module.json
new file mode 100644
index 00000000..f5ed37d3
--- /dev/null
+++ b/modules-available/statistics_reporting/lang/en/module.json
@@ -0,0 +1,10 @@
+{
+ "module_name": "Statistics Reporting",
+ "notAssigned": "Not assigned",
+ "page_title": "Statistics Reporting",
+ "table_client": "By client",
+ "table_location": "By location",
+ "table_total": "Total",
+ "table_user": "By user",
+ "table_vm": "By lecture"
+} \ No newline at end of file
diff --git a/modules-available/statistics_reporting/lang/en/template-tags.json b/modules-available/statistics_reporting/lang/en/template-tags.json
new file mode 100644
index 00000000..a4865931
--- /dev/null
+++ b/modules-available/statistics_reporting/lang/en/template-tags.json
@@ -0,0 +1,22 @@
+{
+ "lang_apply": "Apply",
+ "lang_displayColumns": "Select columns to display",
+ "lang_displaySelection": "Select display mode and specify time span",
+ "lang_downloadReport": "Download report",
+ "lang_export": "Export",
+ "lang_hostname": "Hostname",
+ "lang_lastLogout": "Last logout",
+ "lang_lastStart": "Last boot",
+ "lang_location": "Location",
+ "lang_longSessions": "Sessions \u2265 60s",
+ "lang_medianSessionLength": "Median Session Length",
+ "lang_reportingDescription": "Help us improve bwLehrpool by automatically sending an anonymized statistics report once a week. If you want to check what data the report contains, you can download such a report for reference below.",
+ "lang_reportingLabel": "Send anonymized usage statistics",
+ "lang_sessions": "Sessions",
+ "lang_shortSessions": "Sessions < 60s",
+ "lang_total": "Total",
+ "lang_totalOffTime": "Total Time Offline",
+ "lang_totalTime": "Total Time",
+ "lang_user": "User",
+ "lang_vm": "Lecture"
+} \ No newline at end of file
diff --git a/modules-available/statistics_reporting/lang/pt/template-tags.json b/modules-available/statistics_reporting/lang/pt/template-tags.json
new file mode 100644
index 00000000..e7981844
--- /dev/null
+++ b/modules-available/statistics_reporting/lang/pt/template-tags.json
@@ -0,0 +1,3 @@
+{
+ "lang_hello": "Olá"
+} \ No newline at end of file
diff --git a/modules-available/statistics_reporting/page.inc.php b/modules-available/statistics_reporting/page.inc.php
new file mode 100644
index 00000000..6bd908a5
--- /dev/null
+++ b/modules-available/statistics_reporting/page.inc.php
@@ -0,0 +1,279 @@
+<?php
+
+
+class Page_Statistics_Reporting extends Page
+{
+
+ private $action;
+ private $type;
+
+ // "Constants"
+ private $days;
+
+ /**
+ * @var array Names of columns that are being used by the various tables
+ */
+ private $COLUMNS = array('location', 'totalTime', 'medianSessionLength', 'sessions', 'longSessions', 'shortSessions',
+ 'totalOffTime', 'lastLogout', 'lastStart');
+
+ /**
+ * @var array Names of the tables we can display
+ */
+ private $TABLES = array('total', 'location', 'client', 'user', 'vm');
+
+ /**
+ * Called before any page rendering happens - early hook to check parameters etc.
+ */
+ protected function doPreprocess()
+ {
+ User::load();
+
+ if (!User::isLoggedIn()) {
+ Message::addError('main.no-permission');
+ Util::redirect('?do=Main'); // does not return
+ }
+
+ $this->action = Request::any('action', 'show', 'string');
+ $this->type = Request::get('type', 'total', 'string');
+ $this->days = Request::get('cutoff', 7, 'int');
+ $this->lower = Request::get('lower', 8, 'int');
+ $this->upper = Request::get('upper', 20, 'int');
+
+ if (!in_array($this->type, $this->TABLES)) {
+ Message::addError('invalid-table-type', $this->type);
+ $this->type = 'total';
+ }
+
+ // timespan you want to see. default = last 7 days
+ GetData::$from = strtotime("- " . ($this->days - 1) . " days 00:00:00");
+ GetData::$to = time();
+ GetData::$lowerTimeBound = $this->lower;
+ GetData::$upperTimeBound = $this->upper;
+
+ // Export - handle in doPreprocess so we don't render the menu etc.
+ if ($this->action === 'export') {
+ $this->doExport();
+ // Does not return
+ }
+ // Get report - fetch data exactly the way it would automatically be reported
+ // so the user can know what is going on
+ if ($this->action === 'getreport') {
+ $report = RemoteReport::generateReport(strtotime('-7 days'), time('now'));
+ Header('Content-Disposition: attachment; filename=remote-report.json');
+ Header('Content-Type: application/json; charset=utf-8');
+ die(json_encode($report));
+ }
+ }
+
+ /**
+ * Menu etc. has already been generated, now it's time to generate page content.
+ */
+ protected function doRender()
+ {
+ if ($this->action === 'show') {
+
+ /*
+ * Leave these here for the translate module
+ * Dictionary::translate('table_total');
+ * Dictionary::translate('table_location');
+ * Dictionary::translate('table_client');
+ * Dictionary::translate('table_user');
+ * Dictionary::translate('table_vm');
+ */
+
+ $data = array(
+ 'columns' => array(),
+ 'tables' => array(),
+ 'days' => array()
+ );
+
+ $forceOn = (Request::get('type') === false);
+ foreach ($this->COLUMNS as $column) {
+ $data['columns'][] = array(
+ 'id' => 'col_' . $column,
+ 'name' => Dictionary::translateFile('template-tags', 'lang_' . $column, true),
+ 'checked' => ($forceOn || Request::get('col_' . $column, 'off', 'string') !== 'off') ? 'checked' : '',
+ );
+ }
+
+ foreach ($this->TABLES as $table) {
+ $data['tables'][] = array(
+ 'name' => Dictionary::translate('table_' . $table, true),
+ 'value' => $table,
+ 'selected' => ($this->type === $table) ? 'selected' : '',
+ );
+ }
+
+ foreach (array(1,2,5,7,14,30,90) as $day) {
+ $data['days'][] = array(
+ 'days' => $day,
+ 'selected' => ($day === $this->days) ? 'selected' : '',
+ );
+ }
+
+ $data['lower'] = $this->lower;
+ $data['upper'] = $this->upper;
+
+ if (RemoteReport::isReportingEnabled()) {
+ $data['settingsButtonClass'] = 'default';
+ $data['reportChecked'] = 'checked';
+ } else {
+ $data['settingsButtonClass'] = 'danger';
+ }
+
+ Render::addTemplate('columnChooser', $data);
+
+ $data['data'] = $this->fetchData(GETDATA_PRINTABLE);
+ Render::addTemplate('table-' . $this->type, $data);
+ }
+ }
+
+ protected function doAjax()
+ {
+ $this->action = Request::any('action', false, 'string');
+ if ($this->action === 'setReporting') {
+ if (!User::isLoggedIn()) {
+ die("No.");
+ }
+ $state = Request::post('reporting', false, 'string');
+ if ($state === false) {
+ die('Missing setting value.');
+ }
+ RemoteReport::setReportingEnabled($state);
+ $data = array();
+ if (RemoteReport::isReportingEnabled()) {
+ $data['class'] = 'default';
+ $data['checked'] = true;
+ } else {
+ $data['class'] = 'danger';
+ }
+ Header('Content-Type: application/json; charset=utf-8');
+ die(json_encode($data));
+ } else {
+ echo 'Invalid action.';
+ }
+ }
+
+ private function doExport()
+ {
+ $format = Request::get('format', 'json', 'string');
+ $printable = (bool)Request::get('printable', 0, 'int');
+ $flags = 0;
+ if ($printable) {
+ $flags |= GETDATA_PRINTABLE;
+ }
+ $res = $this->fetchData($flags);
+ // Filter unwanted columns
+ if (isset($res[0])) {
+ foreach ($this->COLUMNS as $column) {
+ if (Request::get('col_' . $column, 'delete', 'string') === 'delete') {
+ foreach ($res as &$row) {
+ unset($row[$column], $row[$column . '_s']);
+ if ($column === 'location') {
+ unset($row['locationId']);
+ }
+ }
+ } elseif ($printable && isset($row[0][$column . '_s'])) {
+ foreach ($res as &$row) {
+ unset($row[$column]);
+ }
+ } elseif ($column === 'location' && (isset($res[0]['location']) || isset($res[0]['locationId']))) {
+ foreach ($res as &$row) {
+ if ($printable) {
+ unset($row['locationId']);
+ } else {
+ unset($row['location']);
+ }
+ }
+ }
+ }
+ unset($row);
+ }
+ Header('Content-Disposition: attachment; filename=' . 'statistics-' . date('Y.m.d-H.i.s') . '.' . $format);
+ switch ($format) {
+ case 'json':
+ Header('Content-Type: application/json; charset=utf-8');
+ $output = json_encode(array('data' => $res));
+ break;
+ case 'csv':
+ if (!is_array($res)) {
+ die('Error fetching data.');
+ }
+ Header('Content-Type: text/csv; charset=utf-8');
+ $fh = fopen('php://output', 'w');
+ // Output UTF-8 BOM - Excel needs this to automatically decode as UTF-8
+ // (and since Excel is the only sane reason to export as csv, just always do it)
+ fputs($fh, chr(239) . chr(187) . chr(191));
+ // Output
+ if (isset($res[0]) && is_array($res[0])) {
+ // List of rows
+ fputcsv($fh, array_keys($res[0]), ';');
+ foreach ($res as $row) {
+ fputcsv($fh, $row, ';');
+ }
+ } else {
+ // Single assoc array
+ fputcsv($fh, array_keys($res), ';');
+ fputcsv($fh, $res, ';');
+ }
+ fclose($fh);
+ exit();
+ break;
+ case 'xml':
+ $xml_data = new SimpleXMLElement('<?xml version="1.0" encoding="utf-8" ?><data></data>');
+ $this->array_to_xml($res, $xml_data, 'row');
+ $output = $xml_data->asXML();
+ Header('Content-Type: text/xml; charset=utf-8');
+ break;
+ default:
+ Header('Content-Type: text/plain');
+ $output = 'Invalid format: ' . $format;
+ }
+ die($output);
+ }
+
+ /**
+ * @param $data array Data to encode
+ * @param $xml_data \SimpleXMLElement XML Object to append to
+ */
+ private function array_to_xml($data, $xml_data, $parentName = 'row')
+ {
+ foreach ($data as $key => $value) {
+ if (is_numeric($key)) {
+ $key = $parentName;
+ }
+ if (is_array($value)) {
+ $subnode = $xml_data->addChild($key);
+ $this->array_to_xml($value, $subnode, $key);
+ } else {
+ $xml_data->addChild($key, htmlspecialchars($value));
+ }
+ }
+ }
+
+ private function fetchData($flags)
+ {
+ switch ($this->type) {
+ case 'total':
+ return GetData::total($flags);
+ case 'location':
+ $data = GetData::perLocation($flags);
+ $highlight = Request::get('location', false, 'int');
+ if ($highlight !== false) {
+ foreach ($data as &$row) {
+ if ($row['locationId'] == $highlight) {
+ $row['highlight'] = true;
+ }
+ }
+ }
+ return $data;
+ case 'client':
+ return GetData::perClient($flags);
+ case 'user':
+ return GetData::perUser($flags);
+ case 'vm':
+ return GetData::perVM($flags);
+ }
+ }
+
+}
diff --git a/modules-available/statistics_reporting/style.css b/modules-available/statistics_reporting/style.css
new file mode 100644
index 00000000..81dc74b0
--- /dev/null
+++ b/modules-available/statistics_reporting/style.css
@@ -0,0 +1,42 @@
+.top-row {
+ margin-bottom: 10px;
+}
+
+.top-row > * {
+ margin-right: 10px;
+ margin-bottom: 10px;
+}
+
+.top-row #button-settings {
+ margin-right: 0;
+}
+
+.buttonbar button {
+ margin-bottom: 4px;
+}
+
+.buttonbar {
+ margin-bottom: 20px;
+}
+
+#slider {
+ display: inline-block;
+ width: 160px;
+ margin-left: 20px;
+ margin-bottom: 3px;
+ margin-right: 20px;
+}
+
+#lower-handle, #upper-handle {
+ width: 3em;
+ height: 1.6em;
+ top: 50%;
+ margin-top: -.8em;
+ margin-left: -1.5em;
+ text-align: center;
+ line-height: 1.6em;
+}
+
+th[data-sort] {
+ cursor: pointer;
+}
diff --git a/modules-available/statistics_reporting/templates/columnChooser.html b/modules-available/statistics_reporting/templates/columnChooser.html
new file mode 100644
index 00000000..a5ac828b
--- /dev/null
+++ b/modules-available/statistics_reporting/templates/columnChooser.html
@@ -0,0 +1,181 @@
+<form method="get" id="controlsForm">
+ <input type="hidden" name="do" value="statistics_reporting">
+ <div class="row">
+ <div class="col-md-12">
+ <button id="button-settings" type="button" class="pull-right btn btn-{{settingsButtonClass}}" data-toggle="modal" data-target="#modal-settings"><span class="glyphicon glyphicon-cog"></span></button>
+ <strong class="text-capitalize">{{lang_displaySelection}}</strong>
+ </div>
+ </div>
+ <div class="row top-row">
+ <div class="col-md-4">
+ <select name="type" id="select-table" class="form-control">
+ {{#tables}}
+ <option value="{{value}}" {{selected}}>{{name}}</option>
+ {{/tables}}
+ </select>
+ </div>
+ <div class="col-md-4">
+ <select name="cutoff" id="select-cutoff" class="form-control">
+ {{#days}}
+ <option value="{{days}}" {{selected}}>{{days}} {{lang_days}}</option>
+ {{/days}}
+ </select>
+ </div>
+ <div class="col-md-3">
+ <div id="slider">
+ <div id="lower-handle" class="ui-slider-handle"></div>
+ <div id="upper-handle" class="ui-slider-handle"></div>
+ <input type="hidden" id="lower-field" name="lower" value="{{lower}}">
+ <input type="hidden" id="upper-field" name="upper" value="{{upper}}">
+ </div>
+ </div>
+ </div>
+ <div class="row top-row">
+ <div class="col-md-12 form-inline">
+ <div><strong class="text-capitalize">{{lang_displayColumns}}</strong></div>
+ {{#columns}}
+ <div class="checkbox">
+ <input id="id_{{id}}" name="{{id}}" value="on" type="checkbox" class="column-toggle form-control" {{checked}}>
+ <label for="id_{{id}}">{{name}}</label>
+ </div>
+ {{/columns}}
+ </div>
+ </div>
+ <div class="row top-row">
+ <div class="col-md-12 form-inline">
+ <div class="pull-right input-group">
+ <select class="form-control" name="format">
+ <option value="json">JSON</option>
+ <option value="csv">CSV (Excel)</option>
+ <option value="xml">XML</option>
+ </select>
+ <div class="input-group-btn">
+ <button type="submit" class="btn btn-default" name="action" value="export">{{lang_export}}</button>
+ </div>
+ </div>
+ <button type="submit" class="btn btn-primary">{{lang_apply}}</button>
+ </div>
+ </div>
+</form>
+
+<hr>
+
+<div id="modal-settings" class="modal fade" role="dialog">
+ <div class="modal-dialog">
+
+ <!-- Modal content-->
+ <div class="modal-content">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal">&times;</button>
+ <h4 class="modal-title"><b>Settings</b></h4>
+ </div>
+ <div class="modal-body">
+ <div class="checkbox">
+ <input id="checkbox-reporting" type="checkbox" value="on" {{reportChecked}}>
+ <label for="checkbox-reporting" style="padding-left: 40px">{{lang_reportingLabel}}</label>
+ </div>
+ <div>
+ <p>{{lang_reportingDescription}}</p>
+ <a class="btn btn-success" href="?do=statistics_reporting&amp;action=getreport">{{lang_downloadReport}}</a>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-primary" data-dismiss="modal" onclick="saveSettings()">{{lang_save}}</button>
+ </div>
+ </div>
+
+ </div>
+</div>
+
+<script type="application/javascript">
+
+ document.addEventListener("DOMContentLoaded", function () {
+ var lowerHandle = $("#lower-handle");
+ var upperHandle = $("#upper-handle");
+ var lower = $('#lower-field').val();
+ var upper = $('#upper-field').val();
+ $( "#slider" ).slider({
+ range: true,
+ min: 0,
+ max: 24,
+ values: [ lower, upper ],
+ create: function() {
+ lowerHandle.text( lower+":00" );
+ upperHandle.text( upper+":00" );
+ },
+ slide: function(event, ui) {
+ lowerHandle.text(ui.values[0]+":00");
+ upperHandle.text(ui.values[1]+":00");
+ $('#lower-field').val(ui.values[0]);
+ $('#upper-field').val(ui.values[1]);
+ },
+ });
+
+ var table = $("table").stupidtable();
+ 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 ? "up" : "down";
+ th.eq(data.column).append(' <span class="arrow glyphicon glyphicon-chevron-'+arrow+'"></span>');
+ });
+
+ $(".locationLink").click(function(e) {
+ e.preventDefault();
+ var form = $('#controlsForm');
+ var inp = $('#location-id');
+ if (inp.length === 0) {
+ inp = $('<input />').attr('type', 'hidden')
+ .attr('name', "location")
+ .attr('id', 'location-id')
+ .appendTo(form);
+ }
+ inp.attr('value', $(this).data('lid'));
+ form.find('#select-table').val("location");
+ form.submit();
+ });
+
+ $('.column-toggle').change(function () {
+ updateColumn($(this));
+ });
+ $('.column-toggle').each(function () {
+ var box = $(this);
+ if ($('.' + box.attr('name')).length === 0) {
+ if (!box.is(':checked')) {
+ box.attr('value', 'off');
+ box.prop('checked', true);
+ }
+ box.parent().hide();
+ } else {
+ updateColumn(box);
+ }
+ });
+
+ $('th[data-sort]').first().click();
+ });
+
+ function updateColumn(checkbox) {
+ var cols = $('.' + checkbox.attr('name'));
+ if (checkbox.is(':checked')) {
+ cols.show();
+ } else {
+ cols.hide();
+ }
+ }
+
+ function saveSettings() {
+ $.ajax({
+ url: '?do=statistics_reporting',
+ type: 'POST',
+ data: { action: "setReporting", reporting: $("#checkbox-reporting").is(":checked") ? "on" : "off", token: TOKEN },
+ success: function(value) {
+ if (typeof(value) === 'object') {
+ $("#checkbox-reporting").prop('checked', !!value['checked']);
+ $("#button-settings").removeClass('btn-default btn-danger').addClass('btn-' + value['class']);
+ } else {
+ alert('Invalid reply when setting value: ' + value + ' (' + typeof(value) + ')');
+ }
+ }
+ });
+ }
+</script> \ No newline at end of file
diff --git a/modules-available/statistics_reporting/templates/table-client.html b/modules-available/statistics_reporting/templates/table-client.html
new file mode 100644
index 00000000..be504cef
--- /dev/null
+++ b/modules-available/statistics_reporting/templates/table-client.html
@@ -0,0 +1,30 @@
+<table id="table-perclient" class="table table-condensed table-striped">
+ <thead>
+ <tr>
+ <th data-sort="string" class="text-left col-md-4">{{lang_hostname}}</th>
+ <th data-sort="string" class="text-left col_location">{{lang_location}}</th>
+ <th data-sort="int" data-sort-default="desc" class="text-left col_totalTime">{{lang_totalTime}}</th>
+ <th data-sort="int" data-sort-default="desc" class="text-left col_medianSessionLength">{{lang_medianSessionLength}}</th>
+ <th data-sort="int" data-sort-default="desc" class="text-left col_longSessions">{{lang_longSessions}}</th>
+ <th data-sort="int" data-sort-default="desc" class="text-left col_shortSessions">{{lang_shortSessions}}</th>
+ <th data-sort="int" data-sort-default="desc" class="text-left col_totalOffTime">{{lang_totalOffTime}}</th>
+ <th data-sort="int" data-sort-default="desc" class="text-left col_lastLogout">{{lang_lastLogout}}</th>
+ <th data-sort="int" data-sort-default="desc" class="text-left col_lastStart">{{lang_lastStart}}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {{#data}}
+ <tr>
+ <td class="text-left">{{hostname}}</td>
+ <td class="text-left col_location"><a class="locationLink" href="#" data-lid="{{locationId}}">{{location}}</a></td>
+ <td data-sort-value="{{totalTime}}" class="text-left col_totalTime">{{totalTime_s}}</td>
+ <td data-sort-value="{{medianSessionLength}}" class="text-left col_medianSessionLength">{{medianSessionLength_s}}</td>
+ <td class="text-left col_longSessions">{{longSessions}}</td>
+ <td class="text-left col_shortSessions">{{shortSessions}}</td>
+ <td data-sort-value="{{totalOffTime}}" class="text-left col_totalOffTime">{{totalOffTime_s}}</td>
+ <td data-sort-value="{{lastLogout}}" class="text-left col_lastLogout">{{lastLogout_s}}</td>
+ <td data-sort-value="{{lastStart}}" class="text-left col_lastStart">{{lastStart_s}}</td>
+ </tr>
+ {{/data}}
+ </tbody>
+</table>
diff --git a/modules-available/statistics_reporting/templates/table-location.html b/modules-available/statistics_reporting/templates/table-location.html
new file mode 100644
index 00000000..ccac623d
--- /dev/null
+++ b/modules-available/statistics_reporting/templates/table-location.html
@@ -0,0 +1,24 @@
+<table id="table-perlocation" class="table table-condensed table-striped">
+ <thead>
+ <tr>
+ <th data-sort="string" class="text-left col-md-2">{{lang_location}}</th>
+ <th data-sort="int" data-sort-default="desc" class="text-left col_totalTime">{{lang_totalTime}}</th>
+ <th data-sort="int" data-sort-default="desc" class="text-left col_medianSessionLength">{{lang_medianSessionLength}}</th>
+ <th data-sort="int" data-sort-default="desc" class="text-left col_longSessions">{{lang_longSessions}}</th>
+ <th data-sort="int" data-sort-default="desc" class="text-left col_shortSessions">{{lang_shortSessions}}</th>
+ <th data-sort="int" data-sort-default="desc" class="text-left col_totalOffTime">{{lang_totalOffTime}}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {{#data}}
+ <tr{{#highlight}} class="info"{{/highlight}}>
+ <td class="locationName text-left">{{location}}</td>
+ <td data-sort-value="{{totalTime}}" class="text-left col_totalTime">{{totalTime_s}}</td>
+ <td data-sort-value="{{medianSessionLength}}" class="text-left col_medianSessionLength">{{medianSessionLength_s}}</td>
+ <td class="text-left col_longSessions">{{longSessions}}</td>
+ <td class="text-left col_shortSessions">{{shortSessions}}</td>
+ <td data-sort-value="{{totalOffTime}}" class="text-left col_totalOffTime">{{totalOffTime_s}}</td>
+ </tr>
+ {{/data}}
+ </tbody>
+</table>
diff --git a/modules-available/statistics_reporting/templates/table-total.html b/modules-available/statistics_reporting/templates/table-total.html
new file mode 100644
index 00000000..4048a178
--- /dev/null
+++ b/modules-available/statistics_reporting/templates/table-total.html
@@ -0,0 +1,22 @@
+<table id="table-total" class="table table-condensed table-striped">
+ <thead>
+ <tr>
+ <th class="text-left col-md-2"></th>
+ <th class="text-left col_totalTime">{{lang_totalTime}}</th>
+ <th class="text-left col_medianSessionLength">{{lang_medianSessionLength}}</th>
+ <th class="text-left col_longSessions">{{lang_longSessions}}</th>
+ <th class="text-left col_shortSessions">{{lang_shortSessions}}</th>
+ <th class="text-left col_totalOffTime">{{lang_totalOffTime}}</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <th class="text-left">{{lang_total}}</th>
+ <td class="text-left col_totalTime">{{data.totalTime_s}}</td>
+ <td class="text-left col_medianSessionLength">{{data.medianSessionLength_s}}</td>
+ <td class="text-left col_longSessions">{{data.longSessions}}</td>
+ <td class="text-left col_shortSessions">{{data.shortSessions}}</td>
+ <td class="text-left col_totalOffTime">{{data.totalOffTime_s}}</td>
+ </tr>
+ </tbody>
+</table>
diff --git a/modules-available/statistics_reporting/templates/table-user.html b/modules-available/statistics_reporting/templates/table-user.html
new file mode 100644
index 00000000..5c2ba56f
--- /dev/null
+++ b/modules-available/statistics_reporting/templates/table-user.html
@@ -0,0 +1,16 @@
+<table id="table-peruser" class="table table-condensed table-striped">
+ <thead>
+ <tr>
+ <th data-sort="string" class="text-left col-md-4">{{lang_user}}</th>
+ <th data-sort="int" data-sort-default="desc" class="text-left col_sessions">{{lang_sessions}}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {{#data}}
+ <tr>
+ <td class="text-left">{{user}}</td>
+ <td class="text-left col_sessions">{{sessions}}</td>
+ </tr>
+ {{/data}}
+ </tbody>
+</table>
diff --git a/modules-available/statistics_reporting/templates/table-vm.html b/modules-available/statistics_reporting/templates/table-vm.html
new file mode 100644
index 00000000..9a775709
--- /dev/null
+++ b/modules-available/statistics_reporting/templates/table-vm.html
@@ -0,0 +1,19 @@
+<table id="table-pervm" class="table table-condensed table-striped">
+ <thead>
+ <tr>
+ <th data-sort="string" class="text-left col-md-4">{{lang_vm}}</th>
+ <th data-sort="int" data-sort-default="desc" class="text-left col_sessions">{{lang_sessions}}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {{#data}}
+ <tr>
+ <td class="text-left">{{vm}}</td>
+ <td class="text-left col_sessions">{{sessions}}</td>
+ </tr>
+ {{/data}}
+ </tbody>
+</table>
+
+
+
diff --git a/modules-available/sysconfig/addmodule_adauth.inc.php b/modules-available/sysconfig/addmodule_adauth.inc.php
index 266327a8..82df5bad 100644
--- a/modules-available/sysconfig/addmodule_adauth.inc.php
+++ b/modules-available/sysconfig/addmodule_adauth.inc.php
@@ -428,7 +428,8 @@ class AdAuth_HomeDir extends AddModule_Base
public static function getAttributes()
{
- return array('shareRemapMode', 'shareRemapCreate', 'shareDocuments', 'shareDownloads', 'shareDesktop', 'shareMedia', 'shareOther', 'shareHomeDrive');
+ return array('shareRemapMode', 'shareRemapCreate', 'shareDocuments', 'shareDownloads', 'shareDesktop',
+ 'shareMedia', 'shareOther', 'shareHomeDrive', 'shareDomain', 'credentialPassthrough');
}
}
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_ldapauth.inc.php b/modules-available/sysconfig/addmodule_ldapauth.inc.php
index 2bd4b584..c61c710c 100644
--- a/modules-available/sysconfig/addmodule_ldapauth.inc.php
+++ b/modules-available/sysconfig/addmodule_ldapauth.inc.php
@@ -212,7 +212,8 @@ class LdapAuth_HomeDir extends AddModule_Base
public static function getAttributes()
{
- return array('shareRemapMode', 'shareRemapCreate', 'shareDocuments', 'shareDownloads', 'shareDesktop', 'shareMedia', 'shareOther', 'shareHomeDrive');
+ return array('shareRemapMode', 'shareRemapCreate', 'shareDocuments', 'shareDownloads', 'shareDesktop',
+ 'shareMedia', 'shareOther', 'shareHomeDrive', 'shareDomain', 'credentialPassthrough');
}
}
diff --git a/modules-available/sysconfig/addmodule_sshconfig.inc.php b/modules-available/sysconfig/addmodule_sshconfig.inc.php
index 19272c32..ec01f878 100644
--- a/modules-available/sysconfig/addmodule_sshconfig.inc.php
+++ b/modules-available/sysconfig/addmodule_sshconfig.inc.php
@@ -18,7 +18,7 @@ class SshConfig_Start extends AddModule_Base
} else {
$data = array();
}
- Render::addDialog(Dictionary::translate('lang_clientSshConfig'), false, 'sshconfig-start', $data + array(
+ Render::addDialog(Dictionary::translateFile('config-module', 'sshconfig_title'), false, 'sshconfig-start', $data + array(
'step' => 'SshConfig_Finish',
));
}
diff --git a/modules-available/sysconfig/hooks/cron.inc.php b/modules-available/sysconfig/hooks/cron.inc.php
new file mode 100644
index 00000000..d1f91437
--- /dev/null
+++ b/modules-available/sysconfig/hooks/cron.inc.php
@@ -0,0 +1,3 @@
+<?php
+
+Trigger::ldadp(); \ No newline at end of file
diff --git a/modules-available/sysconfig/inc/configmodulebaseldap.inc.php b/modules-available/sysconfig/inc/configmodulebaseldap.inc.php
index 760593e1..8e42478e 100644
--- a/modules-available/sysconfig/inc/configmodulebaseldap.inc.php
+++ b/modules-available/sysconfig/inc/configmodulebaseldap.inc.php
@@ -7,7 +7,8 @@ abstract class ConfigModuleBaseLdap extends ConfigModule
private static $REQUIRED_FIELDS = array('server', 'searchbase');
private static $OPTIONAL_FIELDS = array('binddn', 'bindpw', 'home', 'ssl', 'fingerprint', 'certificate', 'homeattr',
- 'shareRemapMode', 'shareRemapCreate', 'shareDocuments', 'shareDownloads', 'shareDesktop', 'shareMedia', 'shareOther', 'shareHomeDrive');
+ 'shareRemapMode', 'shareRemapCreate', 'shareDocuments', 'shareDownloads', 'shareDesktop', 'shareMedia',
+ 'shareOther', 'shareHomeDrive', 'shareDomain', 'credentialPassthrough');
protected function generateInternal($tgz, $parent)
{
diff --git a/modules-available/sysconfig/inc/configtgz.inc.php b/modules-available/sysconfig/inc/configtgz.inc.php
index b51d2787..c03d1c5e 100644
--- a/modules-available/sysconfig/inc/configtgz.inc.php
+++ b/modules-available/sysconfig/inc/configtgz.inc.php
@@ -82,7 +82,141 @@ class ConfigTgz
));
return true;
}
-
+
+ /**
+ *
+ * @param bool $deleteOnError
+ * @param int $timeoutMs
+ * @return string - OK (success)
+ * - OUTDATED (updating failed, but old version still exists)
+ * - MISSING (failed and no old version available)
+ */
+ public function generate($deleteOnError = false, $timeoutMs = 0)
+ {
+ if (!($this->configId > 0) || !is_array($this->modules) || $this->file === false)
+ Util::traceError ('configId <= 0 or modules not array in ConfigTgz::rebuild()');
+ $files = array();
+ // Get all config modules for system config
+ foreach ($this->modules as $module) {
+ if (!empty($module['filepath']) && file_exists($module['filepath']))
+ $files[] = $module['filepath'];
+ }
+ // Get stuff other modules want to inject
+ $handler = function($hook) {
+ include $hook->file;
+ return isset($file) ? $file : false;
+ };
+ foreach (Hook::load('config-tgz') as $hook) {
+ $file = $handler($hook);
+ if ($file !== false) {
+ $files[] = $file;
+ }
+ }
+ // Hand over to tm
+ $task = Taskmanager::submit('RecompressArchive', array(
+ 'inputFiles' => $files,
+ 'outputFile' => $this->file
+ ));
+ // Wait for completion
+ if ($timeoutMs > 0 && !Taskmanager::isFailed($task) && !Taskmanager::isFinished($task))
+ $task = Taskmanager::waitComplete($task, $timeoutMs);
+ if ($task === true || (isset($task['statusCode']) && $task['statusCode'] === TASK_FINISHED)) {
+ // Success!
+ $this->markUpdated();
+ return true;
+ }
+ if (!is_array($task) || !isset($task['id']) || Taskmanager::isFailed($task)) {
+ // Failed...
+ Taskmanager::addErrorMessage($task);
+ if (!$deleteOnError)
+ $this->markFailed();
+ else
+ $this->delete();
+ return false;
+ }
+ // Still running, add callback
+ TaskmanagerCallback::addCallback($task, 'cbConfTgzCreated', array(
+ 'configid' => $this->configId,
+ 'deleteOnError' => $deleteOnError
+ ));
+ return $task['id'];
+ }
+
+ public function delete()
+ {
+ if ($this->configId === 0)
+ Util::traceError('ConfigTgz::delete called with invalid config id!');
+ $ret = Database::exec("DELETE FROM configtgz WHERE configid = :configid LIMIT 1", array(
+ 'configid' => $this->configId
+ ), true) !== false;
+ if ($ret !== false) {
+ if ($this->file !== false)
+ Taskmanager::submit('DeleteFile', array('file' => $this->file), true);
+ $this->configId = 0;
+ $this->modules = false;
+ $this->file = false;
+ }
+ return $ret;
+ }
+
+ public function markOutdated()
+ {
+ if ($this->configId === 0)
+ Util::traceError('ConfigTgz::markOutdated called with invalid config id!');
+ return $this->mark('OUTDATED');
+ }
+
+ private function markUpdated()
+ {
+ if ($this->configId === 0)
+ Util::traceError('ConfigTgz::markUpdated called with invalid config id!');
+ Event::activeConfigChanged();
+ if ($this->areAllModulesUpToDate())
+ return $this->mark('OK');
+ return $this->mark('OUTDATED');
+ }
+
+ private function markFailed()
+ {
+ if ($this->configId === 0)
+ Util::traceError('ConfigTgz::markFailed called with invalid config id!');
+ if ($this->file === false || !file_exists($this->file))
+ return $this->mark('MISSING');
+ return $this->mark('OUTDATED');
+ }
+
+ private function mark($status)
+ {
+ Database::exec("UPDATE configtgz SET status = :status WHERE configid = :configid LIMIT 1", array(
+ 'configid' => $this->configId,
+ 'status' => $status
+ ));
+ return $status;
+ }
+
+ /*
+ * Static part
+ */
+
+ /**
+ * Marks all modules as outdated and triggers generate()
+ * on each one. This mostly makes sense to call if a global module
+ * that is injected via a hook has changed.
+ */
+ public static function rebuildAllConfigs()
+ {
+ Database::exec("UPDATE configtgz SET status = :status", array(
+ 'status' => 'OUTDATED'
+ ));
+ $res = Database::simpleQuery("SELECT configid FROM configtgz");
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ $module = self::get($row['configid']);
+ if ($module !== false) {
+ $module->generate();
+ }
+ }
+ }
+
public static function insert($title, $moduleIds)
{
if (!is_array($moduleIds))
@@ -116,7 +250,7 @@ class ConfigTgz
}
return $instance;
}
-
+
public static function get($configId)
{
$ret = Database::queryFirst("SELECT configid, title, filepath FROM configtgz WHERE configid = :configid", array(
@@ -137,7 +271,7 @@ class ConfigTgz
}
return $instance;
}
-
+
public static function getAllForModule($moduleId)
{
$res = Database::simpleQuery("SELECT configid, title, filepath FROM configtgz_x_module "
@@ -215,103 +349,4 @@ class ConfigTgz
$config->markUpdated();
}
- /**
- *
- * @param type $deleteOnError
- * @param type $timeoutMs
- * @return string - OK (success)
- * - OUTDATED (updating failed, but old version still exists)
- * - MISSING (failed and no old version available)
- */
- public function generate($deleteOnError = false, $timeoutMs = 0)
- {
- if (!($this->configId > 0) || !is_array($this->modules) || $this->file === false)
- Util::traceError ('configId <= 0 or modules not array in ConfigTgz::rebuild()');
- $files = array();
- foreach ($this->modules as $module) {
- if (!empty($module['filepath']) && file_exists($module['filepath']))
- $files[] = $module['filepath'];
- }
- // Hand over to tm
- $task = Taskmanager::submit('RecompressArchive', array(
- 'inputFiles' => $files,
- 'outputFile' => $this->file
- ));
- // Wait for completion
- if ($timeoutMs > 0 && !Taskmanager::isFailed($task) && !Taskmanager::isFinished($task))
- $task = Taskmanager::waitComplete($task, $timeoutMs);
- if ($task === true || (isset($task['statusCode']) && $task['statusCode'] === TASK_FINISHED)) {
- // Success!
- $this->markUpdated();
- return true;
- }
- if (!is_array($task) || !isset($task['id']) || Taskmanager::isFailed($task)) {
- // Failed...
- Taskmanager::addErrorMessage($task);
- if (!$deleteOnError)
- $this->markFailed();
- else
- $this->delete();
- return false;
- }
- // Still running, add callback
- TaskmanagerCallback::addCallback($task, 'cbConfTgzCreated', array(
- 'configid' => $this->configId,
- 'deleteOnError' => $deleteOnError
- ));
- return $task['id'];
- }
-
- public function delete()
- {
- if ($this->configId === 0)
- Util::traceError('ConfigTgz::delete called with invalid config id!');
- $ret = Database::exec("DELETE FROM configtgz WHERE configid = :configid LIMIT 1", array(
- 'configid' => $this->configId
- ), true) !== false;
- if ($ret !== false) {
- if ($this->file !== false)
- Taskmanager::submit('DeleteFile', array('file' => $this->file), true);
- $this->configId = 0;
- $this->modules = false;
- $this->file = false;
- }
- return $ret;
- }
-
- public function markOutdated()
- {
- if ($this->configId === 0)
- Util::traceError('ConfigTgz::markOutdated called with invalid config id!');
- return $this->mark('OUTDATED');
- }
-
- private function markUpdated()
- {
- if ($this->configId === 0)
- Util::traceError('ConfigTgz::markUpdated called with invalid config id!');
- Event::activeConfigChanged();
- if ($this->areAllModulesUpToDate())
- return $this->mark('OK');
- return $this->mark('OUTDATED');
- }
-
- private function markFailed()
- {
- if ($this->configId === 0)
- Util::traceError('ConfigTgz::markFailed called with invalid config id!');
- if ($this->file === false || !file_exists($this->file))
- return $this->mark('MISSING');
- return $this->mark('OUTDATED');
- }
-
- private function mark($status)
- {
- Database::exec("UPDATE configtgz SET status = :status WHERE configid = :configid LIMIT 1", array(
- 'configid' => $this->configId,
- 'status' => $status
- ));
- return $status;
- }
-
}
diff --git a/modules-available/sysconfig/inc/ppd.inc.php b/modules-available/sysconfig/inc/ppd.inc.php
new file mode 100644
index 00000000..aa2e0e5a
--- /dev/null
+++ b/modules-available/sysconfig/inc/ppd.inc.php
@@ -0,0 +1,1162 @@
+<?php
+
+/**
+ * Class Ppd for parsing PPD files. This class was developed around
+ * the PPD spec v4.3. All comments in this class referring to sections of
+ * the spec will refer to this version, if not stated otherwise.
+ */
+class Ppd
+{
+
+ const FILE = 0;
+ const STRING = 1;
+
+ const INCLUDE_UNKNOWN_MAIN_KEYWORDS = 1;
+
+ /**
+ * regexp matching valid PPD keywords ASCII 33-126, excluding colon and slash.
+ * See section 3.2/3.3
+ */
+ const EXP_KEYWORD = '[\x21-\x2e\x30-\x39\x3b-\x7e]+';
+
+ const PPD_INT = '\-?\d+';
+
+ const PPD_REAL = '\-?\d+(\.\d+)?';
+
+ const PPD_BOOL = 'True|False';
+
+ const PPD_RECTANGLE = '\-?\d+(\.\d+)?\s+\-?\d+(\.\d+)?\s+\-?\d+(\.\d+)?\s+\-?\d+(\.\d+)?';
+
+ const PPD_DIMENSION = '\-?\d+(\.\d+)?\s+\-?\d+(\.\d+)?';
+
+ private $REQUIRED_KEYWORDS = array(
+ 'PPD-Adobe' => '4\.[0123]',
+ 'FileVersion' => '.*',
+ 'FormatVersion' => '.*',
+ 'LanguageEncoding' => '.*',
+ 'LanguageVersion' => '.*',
+ 'Manufacturer' => '.*',
+ 'ModelName' => '.*',
+ 'NickName' => '.*',
+ 'PCFileName' => '.*',
+ 'Product' => '\(.*\)',
+ 'PSVersion' => '\(.*\)\s+\d+',
+ 'ShortNickName' => '.*'
+ );
+
+ private $LANGUAGE_MAPPINGS = array(
+ 'English' => 'ISOLatin1',
+ 'Chinese' => 'None',
+ 'Danish' => 'ISOLatin1',
+ 'Dutch' => 'ISOLatin1',
+ 'Finnish' => 'ISOLatin1',
+ 'French' => 'ISOLatin1',
+ 'German' => 'ISOLatin1',
+ 'Italian' => 'ISOLatin1',
+ 'Japanese' => 'JIS83-RKSJ',
+ 'Norwegian' => 'ISOLatin1',
+ 'Portuguese' => 'ISOLatin1',
+ 'Russian' => 'None',
+ 'Spanish' => 'ISOLatin1',
+ 'Swedish' => 'ISOLatin1',
+ 'Turkish' => 'None'
+ );
+
+ private $ENCODINGS = array(
+ 'ISOLatin1' => 'ISO-8859-1',
+ 'ISOLatin2' => 'ISO-8859-2',
+ 'ISOLatin5' => 'ISO-8859-5',
+ 'JIS83-RKSJ' => 'SJIS',
+ 'MacStandard' => 'MACINTOSH',
+ 'WindowsANSI' => 'Windows-1252'
+ );
+
+ /**
+ * @var string name of source charset (PPD)
+ */
+ private $sourceEncoding;
+ /**
+ * @var string 'mb' or 'iconv'
+ */
+ private $encoder;
+
+
+ /**
+ * List of known main keywords.
+ * Key is the keyword, value is either a regex for the value, if we don't care about the option format,
+ * or an array with [0] = regex for option keyword, and [1] = regex for value
+ *
+ * @var array
+ */
+ private $KNOWN_KEYWORDS = array(
+ /*
+ * Basic Device Capabilities, section 5.5
+ */
+ 'ColorDevice' => self::PPD_BOOL,
+ 'DefaultColorSpace' => 'CMY|CMYK|RGB|Gray',
+ 'Extensions' => '(DPS|CMYK|Composite|FileSystem)(\s+(DPS|CMYK|Composite|FileSystem))*',
+ 'FaxSupport' => 'Base',
+ 'FileSystem' => self::PPD_BOOL,
+ 'LanguageLevel' => self::PPD_INT,
+ 'Throughput' => '\d+(\.\d+)?',
+ 'TTRasterizer' => 'None|Accept68K|Type42|TrueImage',
+ '1284Modes' => 'Compat|Nibble|Byte|ECP|EPP',
+ '1284DeviceID' => '.*',
+ /*
+ * System Management, section 5.6
+ */
+ 'PatchFile' => '.*',
+ 'JobPatchFile' => array(self::PPD_INT, '.*'),
+ 'FreeVM' => self::PPD_INT,
+ 'VMOption' => self::PPD_INT,
+ 'InstalledMemory' => '.*',
+ 'DefaultInstalledMemory' => '.*',
+ 'Reset' => '.*',
+ 'Password' => '.*',
+ 'ExitJamRecovery' => array(self::PPD_BOOL, '.*'),
+ 'DefaultExitJamRecovery' => 'True|False|Unknown',
+ 'ExitServer' => '.*',
+ 'SuggestedJobTimeout' => self::PPD_INT,
+ 'SuggestedManualFeedTimeout' => self::PPD_INT, // XXX: Typo in spec? It says "SuggestedManualfFeedTimeout"
+ 'SuggestedWaitTimeout' => self::PPD_INT,
+ 'PrintPSErrors' => self::PPD_BOOL,
+ 'DeviceAdjustMatrix' => '\[[\d\s]+\]',
+ /*
+ * Emulations and Protocols, section 5.7
+ */
+ 'Protocols' => '(BCP|PJL|TBCP)(\s+(BCP|PJL|TBCP))*',
+ 'Emulators' => '\S+(\s+\S+)*', // TODO This requires matching *(Start|Stop)Emulator_(\S+): "code" main keywords
+ /*
+ * JCL, section 5.8
+ */
+ 'JCLBegin' => '.*',
+ 'JCLToPSInterpreter' => '.*',
+ 'JCLEnd' => '.*',
+ // TODO: The above three need to be either completely absent, or all three must be defined
+ /*
+ * Resolution and Appearence Control, section 5.9
+ */
+ /*
+ * Gray Levels and Halftoning, section 5.10
+ */
+ 'AccurateScreensSupport' => self::PPD_BOOL,
+ 'ContoneOnly' => self::PPD_BOOL,
+ 'DefaultHalftoneType' => self::PPD_INT,
+ 'ScreenAngle' => self::PPD_REAL,
+ 'ScreenFreq' => self::PPD_REAL,
+ 'ResScreenFreq' => self::PPD_REAL,
+ 'ResScreenAngle' => self::PPD_REAL,
+ 'DefaultScreenProc' => 'Dot|Line|Ellipse|Cross|Mezzo|DiamondDot',
+ 'ScreenProc' => array('Dot|Line|Ellipse|Cross|Mezzo|DiamondDot', '.*'),
+ 'DefaultTransfer' => 'Null|Factory', // XXX: Spec seems to allow only these two values as default, but why
+ 'Transfer' => array('Null|Factory|Normalized|Red|Green|Blue', '.*'),
+ /*
+ * Color Adjustment, section 5.11
+ */
+ 'BlackSubstitution' => array(self::PPD_BOOL, '.*'),
+ 'DefaultBlackSubstitution' => 'True|False|Unknown',
+ 'ColorModel' => array('CMY|CMYK|RGB|Gray', '.*'),
+ 'DefaultColorModel' => 'CMY|CMYK|RGB|Gray|Unknown',
+ 'RenderingIntent' => '.*',
+ 'PageDeviceName' => '.*',
+ 'HalftoneName' => '.*',
+ /*
+ * Media Selection, section 5.14
+ */
+ 'ManualFeed' => array(self::PPD_BOOL, '.*'),
+ 'DefaultManualFeed' => 'True|False|Unknown',
+ /*
+ * Information About Media Sizes, section 5.15
+ */
+ 'ImageableArea' => self::PPD_RECTANGLE,
+ 'PaperDimension' => self::PPD_DIMENSION,
+ 'RequiresPageRegion' => self::PPD_BOOL,
+ 'LandscapeOrientation' => 'Plus90|Minus90|Any',
+ /*
+ * Custom Page Size, section 5.16
+ */
+ 'CustomPageSize' => array('True', '.*'),
+ 'ParamCustomPageSize' => array('Width|Height|WidthOffset|HeightOffset|Orientation', '\d+\s+(int|real|points)\s+' . self::PPD_REAL . '\s+' . self::PPD_REAL),
+ 'MaxMediaWidth' => self::PPD_REAL,
+ 'MaxMediaHeight' => self::PPD_REAL,
+ 'CenterRegistered' => self::PPD_BOOL,
+ 'LeadingEdge' => array('Short|Long|PreferLong|Forced|Unknown', '\s*'),
+ 'DefaultLeadingEdge' => 'Short|Long|PreferLong|Forced|Unknown',
+ 'HWMargins' => self::PPD_RECTANGLE,
+ 'UseHWMargins' => array(self::PPD_BOOL, '\s*'),
+ 'DefaultUseHWMargins' => self::PPD_BOOL,
+ /*
+ * Media Handling Features, section 5.17
+ */
+ 'OutputOrder' => array('Normal|Reverse', '.*'),
+ 'DefaultOutputOrder' => 'Normal|Reverse|Unknown',
+ 'PageStackOrder' => 'Normal|Reverse',
+ 'TraySwitch' => array(self::PPD_BOOL, '.*'),
+ 'DefaultTraySwitch' => 'True|False|Unknown',
+ 'Duplex' => array('DuplexTumble|DuplexNoTumble|SimplexTumble|None|False|SimplexNoTumble', '.*'),
+ 'DefaultDuplex' => 'DuplexTumble|DuplexNoTumble|SimplexTumble|None|False|SimplexNoTumble',
+ /*
+ * Finishing Features, section 5.18ff
+ * TODO
+ */
+
+ /*
+ * Font Related Keywords, section 5.20
+ */
+ 'FDirSize' => self::PPD_INT,
+ 'FCacheSize' => self::PPD_INT,
+ // TODO: 'Font' = >
+ /*
+ * Printer Messages, section 5.21
+ */
+ 'PrinterError' => '.*',
+ 'Status' => '.*',
+ 'Source' => '.*',
+ 'Message' => '.*',
+ /*
+ * 5.22
+ */
+ 'InkName' => '.+',
+ );
+
+ /**
+ * Appendix A.1: UI Keywords.
+ * SORTED, so we can do a binary search.
+ *
+ * @var array list of UI keywords.
+ */
+ private $UI_KEYWORDS = array('AdvanceMedia', 'BindColor', 'BindEdge', 'BindType', 'BindWhen', 'BitsPerPixel',
+ 'BlackSubstitution', 'Booklet', 'Collate', 'ColorModel', 'CutMedia', 'Duplex', 'ExitJamRecovery', 'FoldType',
+ 'FoldWhen', 'InputSlot', 'InstalledMemory', 'Jog', 'ManualFeed', 'MediaColor', 'MediaType', 'MediaWeight',
+ 'MirrorPrint', 'NegativePrint', 'OutputBin', 'OutputMode', 'OutputOrder', 'PageSize', 'PageRegion', 'Separations',
+ 'Signature', 'Slipsheet', 'Smoothing', 'Sorter', 'StapleLocation', 'StapleOrientation', 'StapleWhen', 'StapleX',
+ 'StapleY', 'TraySwitch'
+ );
+
+ /**
+ * Appendix A.2: Repeated Keywords.
+ * SORTED, so we can do a binary search.
+ *
+ * @var array list of repeated keywords
+ */
+ private $REPEATED_KEYWORDS = array('HalftoneName', 'Include', 'InkName', 'Message', 'NonUIConstraints', 'NonUIOrderDependency',
+ 'OrderDependency', 'PageDeviceName', 'PrinterError', 'Product', 'PSVersion', 'QueryOrderDependency',
+ 'RenderingIntent', 'Source', 'Status', 'UIConstraints'
+ );
+
+ private $data;
+ private $dataLen;
+
+ private $error;
+ private $warnings;
+
+ private $knownKeywordMalformed;
+
+ /**
+ * @var PpdSettingInternal[] known options of this ppd
+ */
+ private $settings;
+
+ private $requiredKeywords;
+
+ function __construct($ppd, $type = self::FILE, $flags = 0)
+ {
+ if (empty($ppd)) {
+ $this->error = 'Empty $ppd';
+ return;
+ }
+ if ($type == self::FILE) {
+ $this->data = file_get_contents($ppd);
+ if ($this->data === false) {
+ $this->error = 'Could not open ' . substr($ppd, 1);
+ return;
+ }
+ } elseif ($type == self::STRING) {
+ $this->data = $ppd;
+ } else {
+ $this->error = 'Invalid $type passed';
+ return;
+ }
+ $this->parse();
+ }
+
+ private function parse()
+ {
+ $r = substr_count($this->data, "\r");
+ $n = substr_count($this->data, "\n");
+ if ($r > 10 && abs($r - $n) < $r / 10) {
+ if (substr($this->data, -2) !== "\r\n") {
+ $this->data .= "\r\n";
+ }
+ } elseif ($r > $n) {
+ if (substr($this->data, -1) !== "\r") {
+ $this->data .= "\r";
+ }
+ } else {
+ if (substr($this->data, -1) !== "\n") {
+ $this->data .= "\n";
+ }
+ }
+
+ $this->dataLen = strlen($this->data);
+ $this->encoder = false;
+ $this->sourceEncoding = false;
+ $this->error = false;
+ $this->warnings = array();
+ $this->knownKeywordMalformed = false;
+ $this->settings = array();
+ $this->requiredKeywords = array();
+
+ // Parse
+ /* @var $rawOption \PpdOption */
+ /* @var $currentBlock \PpdBlockInternal */
+ $currentBlock = false;
+ $inRawBlock = false; // True if in a multi-line InvocationValue or QuotedValue (3.6: Parsing Summary for Values)
+ $wantsEnd = false;
+ // For now we ignore values mostly while parsing. The spec says that InvocationValues must only contain printable
+ // ASCII characters, so we should issue a warning if we encounter invalid chars in them.
+ $lStart = -1;
+ $lEnd = -1;
+ $no = 0;
+ while ($lStart < $this->dataLen && $lEnd !== false) {
+ unset($mainKeyword, $optionKeyword, $optionTranslation, $option, $value, $valueTranslation);
+ if ($no !== 0 && $this->data{$lEnd} === "\r" && $this->data{$lEnd + 1} === "\n") {
+ $lEnd++;
+ }
+ if ($no === 1) {
+ // The first line must be *PPD-Adobe, check if that was the case
+ if (!isset($this->requiredKeywords['PPD-Adobe'])) {
+ $this->error = 'First line does not contain *PPD-Adobe main keyword';
+ return;
+ }
+ }
+ $lStart = $lEnd + 1;
+ $lEnd = $this->nextLineEnd($lStart);
+ $no++;
+ // Validate
+ $len = $lEnd - $lStart;
+ $line = substr($this->data, $lStart, $len);
+ if ($len === 0) {
+ continue;
+ }
+ if ($len > 255) {
+ $this->warn($no, 'Exceeds length of 255');
+ }
+ if (!$inRawBlock && preg_match_all('/[^\x09\x0A\x0D\x20-\xFF]/', $line, $out)) {
+ $chars = $this->escapeBinaryArray($out[0]);
+ $this->warn($no, 'Contains invalid character(s) ' . $chars);
+ }
+ // Handle
+ // 1) We're inside an InvocationValue or QuotedValue, need a single " at line end to close it
+ if ($inRawBlock) {
+ if (substr($line, -1) === '"') {
+ $inRawBlock = false;
+ $wantsEnd = true;
+ if (isset($rawOption)) {
+ $rawOption->lineLen = $lEnd - $rawOption->lineOffset;
+ }
+ }
+ continue;
+ }
+ // 2) InvocationValue or QuotedValue just closed, an '*End' has to follow
+ if ($wantsEnd) {
+ $wantsEnd = false;
+ if ($line !== '*End' && $line !== '*SymbolEnd') { // XXX: We don't properly check which one we expected...
+ $this->warn($no, 'End of multi-line InvocationValue or QuotedValue not followed by "*(Symbol)End"');
+ unset($rawOption);
+ } else {
+ if (isset($rawOption)) {
+ $rawOption->lineLen = $lEnd - $rawOption->lineOffset;
+ }
+ unset($rawOption);
+ continue;
+ }
+ }
+ // 3) Handle "key [option]: value"
+ if ($line{0} === '*') {
+ if ($line{1} === '%') {
+ // Skip comment
+ continue;
+ }
+ $parts = preg_split('/\s*:\s*/', $line, 2); // TODO: UIConstrains
+ if (count($parts) !== 2) {
+ $this->warn($no, 'No colon found; not in "key [option]: value" format, ignoring line');
+ continue;
+ }
+ // Now $parts[0] is "key[ option]" and $parts[1] is "value"
+ // 3a) Determine key and option
+ if (1 > preg_match(',^\*(' . self::EXP_KEYWORD . ')($|\s+([^/]+)(/.*)?$),', $parts[0], $out)) {
+ $this->warn($no, 'Not a valid Main Keyword, "' . $parts[0] . '", line ignored');
+ continue;
+ }
+ $mainKeyword = $out[1];
+ $optionKeyword = isset($out[3]) ? $out[3] : false;
+ $optionTranslation = isset($out[4]) ? $this->unhexTranslation($no, substr($out[4], 1)) : $optionKeyword; // If no translation given, fallback to option
+ // 3b) Handle value
+ $value = $parts[1];
+ if ($value{0} === '"') {
+ // Start of InvocationValue or QuotedValue
+ if (preg_match(',^"([^"]*)"(/.*)?$,', $value, $vMatch)) {
+ // Single line
+ $value = $vMatch[1];
+ $valueTranslation = isset($vMatch[2]) ? $this->unhexTranslation($no, substr($vMatch[2], 1)) : $value;
+ } else {
+ // Multi-line
+ $inRawBlock = true;
+ $value = '<TODO: Multiline>'; // TODO: Handle multi-line values properly
+ $valueTranslation = '';
+ }
+ } elseif (preg_match(',^\^' . self::EXP_KEYWORD . '$,', $value)) {
+ // SymbolValue TODO: Can be followed by translation?
+ $valueTranslation = $value;
+ } elseif (preg_match(',^([^"][^/]*)(/.*)?$,', $value, $vMatch)) {
+ // StringValue
+ $value = $vMatch[1];
+ $valueTranslation = isset($vMatch[2]) ? $this->unhexTranslation($no, substr($vMatch[2], 1)) : $value;
+ }
+ // Key-value-pair parsed, now the fun part
+ // Special cases for openening closing certain groups
+ if ($mainKeyword === 'OpenGroup') {
+ if ($currentBlock !== false) {
+ $this->error = 'Line ' . $no . ': OpenGroup while other block (type=' . $currentBlock->type
+ . ', id=' . $currentBlock->id . ') was not closed yet';
+ return;
+ }
+ // TODO: Check unique
+ $nb = new PpdBlockInternal($value, $valueTranslation, 'Group', $currentBlock, $lStart);
+ if ($currentBlock !== false) {
+ $currentBlock->childBlocks[] = $nb;
+ }
+ $currentBlock = $nb;
+ continue;
+ } elseif ($mainKeyword === 'OpenSubGroup') {
+ if ($currentBlock === false || $currentBlock->type !== 'Group') {
+ $this->error = 'Line ' . $no . ': OpenSubGroup with no preceeding OpenGroup';
+ return;
+ }
+ // TODO: Check unique
+ $nb = new PpdBlockInternal($value, $valueTranslation, 'SubGroup', $currentBlock, $lStart);
+ if ($currentBlock !== false) {
+ $currentBlock->childBlocks[] = $nb;
+ }
+ $currentBlock = $nb;
+ continue;
+ } elseif ($mainKeyword === 'OpenUI' || $mainKeyword === 'JCLOpenUI') {
+ $type = $mainKeyword;
+ if (substr($type, 0, 3) === 'JCL') {
+ $type = 'JCL' . substr($type, 7);
+ } else {
+ $type = substr($type, 4);
+ }
+ if ($currentBlock !== false && $currentBlock->isUi()) {
+ $this->error = 'Line ' . $no . ': ' . $mainKeyword . ' while previous ' . $type . ' "'
+ . $currentBlock->id . '" was not closed yet';
+ return;
+ }
+ if ($optionKeyword === false) {
+ $this->error = 'Line ' . $no . ': ' . $mainKeyword . ' with no option keyword';
+ return;
+ }
+ if ($optionKeyword{0} !== '*') {
+ $this->error = 'Line ' . $no . ': ' . $mainKeyword . " with option keyword that doesn't start with asterisk (*).";
+ return;
+ }
+ // TODO: Check unique
+ $nb = new PpdBlockInternal($optionKeyword, $optionTranslation, $type, $currentBlock, $lStart);
+ $nb->value = $value;
+ if ($currentBlock !== false) {
+ $currentBlock->childBlocks[] = $nb;
+ }
+ $currentBlock = $nb;
+ $this->getOption(substr($optionKeyword, 1), $currentBlock); // ->type = $value; unused?
+ continue;
+ } elseif ($mainKeyword === 'CloseGroup' || $mainKeyword === 'CloseSubGroup' || $mainKeyword === 'CloseUI'
+ || $mainKeyword === 'JCLCloseUI'
+ ) {
+ $type = $mainKeyword;
+ if (substr($type, 0, 3) === 'JCL') {
+ $type = 'JCL' . substr($type, 8);
+ } else {
+ $type = substr($type, 5);
+ }
+ if ($currentBlock === false) {
+ $this->error = 'Line ' . $no . ': ' . $mainKeyword . ' with no Open' . $type;
+ return;
+ }
+ if ($currentBlock->type !== $type) {
+ $this->error = 'Line ' . $no . ': ' . $mainKeyword . ' after Open' . $currentBlock->type;
+ return;
+ }
+ if ($currentBlock->id !== $value) {
+ $this->error = 'Line ' . $no . ': ' . $mainKeyword . ' for "' . $value . '" while currently open '
+ . $type . ' is "' . $currentBlock->id . '"';
+ return;
+ }
+ $currentBlock->end = $lEnd;
+ $currentBlock = $currentBlock->parent;
+ continue;
+ } elseif ($mainKeyword === 'OrderDependency') {
+ if ($currentBlock === false || $currentBlock->isUi()) {
+ $this->warn($no, 'OrderDependency outside OpenUI/CloseUI block');
+ }
+ continue;
+ } elseif ($mainKeyword === 'Include') {
+ $this->warn($no, 'PPD tries to include a file (' . $value
+ . '), which is not supported. Will continue, but errors might occur');
+ continue;
+ } elseif ($mainKeyword === 'UIConstraints' || $mainKeyword === 'NonUIConstraints'
+ || $mainKeyword === 'SymbolLength' || $mainKeyword === 'SymbolValue'
+ || $mainKeyword === 'SymbolEnd' || $mainKeyword === 'NonUIOrderDependency'
+ ) {
+ continue;
+ }
+ // General information keywords, which are required
+ if (isset($this->REQUIRED_KEYWORDS[$mainKeyword])) {
+ if (isset($this->requiredKeywords[$mainKeyword])) {
+ if ($this->binary_in_array($mainKeyword, $this->REPEATED_KEYWORDS)) {
+ $this->requiredKeywords[$mainKeyword][] = $value;
+ } else {
+ $this->warn($no, 'Required keyword ' . $mainKeyword . ' declared twice, ignoring');
+ continue;
+ }
+ }
+ $this->requiredKeywords[$mainKeyword] = array($value);
+ if (($err = $this->validateLine($this->REQUIRED_KEYWORDS[$mainKeyword], $optionKeyword, $value)) !== true) {
+ $this->warn($no, 'Required main keyword ' . $mainKeyword . ': ' . $err);
+ $this->knownKeywordMalformed = true;
+ }
+ continue;
+ }
+ // Other well known keywords
+ if (isset($this->KNOWN_KEYWORDS[$mainKeyword])) {
+ if (($err = $this->validateLine($this->KNOWN_KEYWORDS[$mainKeyword], $optionKeyword, $value)) !== true) {
+ $this->warn($no, 'Known main keyword ' . $mainKeyword . ': ' . $err);
+ $this->knownKeywordMalformed = true;
+ }
+ }
+ if (substr($mainKeyword, 0, 7) === 'Default') {
+ // Default keyword
+ $option = $this->getOption(substr($mainKeyword, 7), $currentBlock);
+ $option->default = new PpdOption($lStart, $len, $value, $valueTranslation);
+ continue;
+ } elseif (substr($mainKeyword, 0, 17) === 'FoomaticRIPOption') {
+ if ($optionKeyword === false) {
+ $this->warn($no, "$mainKeyword with no option keyword");
+ } elseif ($currentBlock !== false && isset($this->settings[$optionKeyword])) {
+ $option = $this->getOption($optionKeyword, $currentBlock);
+ $option->foomatic[substr($mainKeyword, 11)] = new PpdOption($lStart, $len, $value, $valueTranslation);
+ } else {
+ $this->warn($no, 'TODO: ' . $line);
+ }
+ } elseif (substr($mainKeyword, 0, 6) === 'Custom') {
+ if ($optionKeyword === false) {
+ $this->warn($no, "$mainKeyword with no option keyword");
+ } elseif ($optionKeyword !== 'True') {
+ $this->warn($no, "$mainKeyword with option keyword other than 'True'; ignored");
+ } else {
+ $option = $this->getOption(substr($mainKeyword, 6), $currentBlock);
+ $option->custom = new PpdOption($lStart, $len, $value, $valueTranslation);
+ }
+ } elseif (substr($mainKeyword, 0, 11) === 'ParamCustom') {
+ if ($optionKeyword === false) {
+ $this->warn($no, "$mainKeyword with no option keyword");
+ } elseif (substr($mainKeyword, 11) !== $optionKeyword) {
+ $this->warn($no, "Don't know how to handle $mainKeyword with option keyword $optionKeyword "
+ . "(expected '*ParamCustomSomething Something: <format>'");
+ } else {
+ $option = $this->getOption($optionKeyword, $currentBlock);
+ $option->customParam = new PpdOption($lStart, $len, $value, $valueTranslation);
+ }
+ } elseif ($mainKeyword{0} === '?') {
+ // Ignoring option query for now
+ } elseif ($optionKeyword === false && !isset($this->KNOWN_KEYWORDS[$mainKeyword])) {
+ // Must be a definition for an option
+ $this->warn($no, "Don't know how to handle line with main keyword '$mainKeyword', no option keyword found.");
+ } else {
+ // Some option for some option ;)
+ if ($optionKeyword === false) {
+ // We know that this is a known main keyword otherwise we would have hit the previous elseif block
+ $optionKeyword = $value;
+ $optionTranslation = $valueTranslation;
+ }
+ $option = $this->getOption($mainKeyword, $currentBlock);
+ $optionInstance = new PpdOption($lStart, $len, $optionKeyword, $optionTranslation);
+ if ($this->binary_in_array($mainKeyword, $this->REPEATED_KEYWORDS)) {
+ // This can occur multiple times, just pile them up
+ $option->values[] = $optionInstance;
+ } else {
+ $key = "k$optionKeyword";
+ if (isset($option->values[$key])) {
+ $this->warn($no, "Ignoring re-definition of option '$optionKeyword' for Main Keyword '$mainKeyword'");
+ } else {
+ $option->values[$key] = $optionInstance;
+ }
+ }
+ if ($inRawBlock) {
+ $optionInstance->multiLine = true;
+ $rawOption = $optionInstance;
+ }
+ unset($optionInstance);
+ }
+ } elseif (strlen(trim($line)) !== 0) {
+ $this->warn($no, 'Invalid format; not empty and not starting with asterisk (*)');
+ }
+ }
+ //
+ if ($currentBlock !== false) {
+ $this->error = 'Block ' . $currentBlock->id . ' (' . $currentBlock->type . ') was never closed.';
+ return;
+ }
+ foreach ($this->REQUIRED_KEYWORDS as $kw => $regex) {
+ if (!isset($this->requiredKeywords[$kw])) {
+ $this->warn(0, "Required keyword '$kw' missing from file.'");
+ $this->error = 'One or more required keywords missing';
+ }
+ }
+ if ($this->error !== false) {
+ return;
+ }
+ // All required keywords exist
+ if (preg_match('/utf\-?8/i', $this->requiredKeywords['LanguageEncoding'][0])) {
+ $this->sourceEncoding = false; // Would be a NOOP
+ } elseif (isset($this->ENCODINGS[$this->requiredKeywords['LanguageEncoding'][0]])) {
+ $this->sourceEncoding = $this->ENCODINGS[$this->requiredKeywords['LanguageEncoding'][0]];
+ } else if (isset($this->LANGUAGE_MAPPINGS[$this->requiredKeywords['LanguageVersion'][0]])) {
+ $this->sourceEncoding = $this->ENCODINGS[$this->LANGUAGE_MAPPINGS[$this->requiredKeywords['LanguageVersion'][0]]];
+ } elseif (!empty($this->requiredKeywords['LanguageEncoding'][0])) {
+ $this->sourceEncoding = $this->requiredKeywords['LanguageEncoding'][0];
+ }
+ if ($this->sourceEncoding !== false) {
+ if (is_callable('iconv')) {
+ $encoding = strtoupper($this->sourceEncoding);
+ if (@iconv($encoding, 'UTF-8//TRANSLIT', 'test') === 'test') {
+ $this->encoder = function ($string, $reverse = false) use ($encoding) {
+ if ($reverse) {
+ $retval = iconv('UTF-8', $encoding . '//TRANSLIT', $string);
+ } else {
+ $retval = iconv($encoding, 'UTF-8//TRANSLIT', $string);
+ }
+ if ($retval === false)
+ return $string;
+ return $retval;
+ };
+ }
+ }
+ if ($this->encoder === false && is_callable('mb_list_encodings')) {
+ $encodings = mb_list_encodings();
+ foreach ($encodings as $encoding) {
+ if (strtolower($encoding) === $this->sourceEncoding) {
+ $this->sourceEncoding = $encoding;
+ $this->encoder = function ($string, $reverse = false) use ($encoding) {
+ if ($reverse) {
+ $retval = mb_convert_encoding($string, $encoding, 'UTF-8');
+ } else {
+ $retval = mb_convert_encoding($string, 'UTF-8', $encoding);
+ }
+ if ($retval === false)
+ return $string;
+ return $retval;
+ };
+ break;
+ }
+ }
+ }
+ }
+ if ($this->encoder === false) {
+ $this->encoder = function ($foo, $reverse = false) { return $foo; };
+ }
+ }
+
+ private function nextLineEnd($start)
+ {
+ if ($start >= $this->dataLen)
+ return false;
+ while ($start < $this->dataLen) {
+ $char = $this->data{$start};
+ if ($char === "\r" || $char === "\n")
+ return $start;
+ ++$start;
+ }
+ return $this->dataLen;
+ }
+
+ private function warn($lineNo, $message)
+ {
+ $line = 'Line ' . $lineNo . ': ' . $message;
+ $this->warnings[] = $line;
+ }
+
+ private function escapeBinaryArray($array)
+ {
+ $chars = array_reduce(array_unique($array), function ($carry, $item) {
+ return $carry . '\x' . dechex(ord($item));
+ }, '');
+ }
+
+ private function unhexTranslation($lineNo, $translation)
+ {
+ if (strpos($translation, '<') === false)
+ return $translation;
+ return preg_replace_callback('/<[^>]*>/', function ($match) use ($lineNo) {
+ if (preg_match_all('/[^a-fA-F0-9\<\>\s]/', $match[0], $out)) {
+ $this->warn($lineNo, 'Invalid character(s) in hex substring: ' . $this->escapeBinaryArray($out[0]));
+ }
+ $string = preg_replace('/[^a-fA-F0-9]/', '', $match[0]);
+ if (strlen($string) % 2 !== 0) {
+ $this->warn('Odd number of hex digits in hex substring');
+ $string = substr($string, 0, -1);
+ }
+ return pack('H*', $string);
+ }, $translation);
+ }
+
+ private function hexTranslation($translation)
+ {
+ return preg_replace_callback('/[\x00-\x1f\x7b-\xff\:\<\>]+/', function ($match) {
+ return '<' . unpack('H*', $match[0])[1] . '>';
+ }, $translation);
+ }
+
+ /**
+ * Get option object
+ *
+ * @param string $name option name
+ * @param \PpdBlockInternal $block which block this option is defined in
+ * @return \PpdSettingInternal the option object
+ */
+ private function getOption($name, $block = false)
+ {
+ if (!isset($this->settings[$name])) {
+ $this->settings[$name] = new PpdSettingInternal();
+ $this->settings[$name]->block = $block;
+ } elseif ($block !== false) {
+ if ($this->settings[$name]->block === false || $block->isChildOf($this->settings[$name]->block)) {
+ $this->settings[$name]->block = $block;
+ }
+ }
+ return $this->settings[$name];
+ }
+
+ private function binary_in_array($elem, $array)
+ {
+ $top = sizeof($array) - 1;
+ $bot = 0;
+ while ($top >= $bot) {
+ $p = floor(($top + $bot) / 2);
+ if ($array[$p] < $elem)
+ $bot = $p + 1;
+ elseif ($array[$p] > $elem)
+ $top = $p - 1;
+ else return true;
+ }
+ return false;
+ }
+
+ private function validateLine($validator, $option, $value)
+ {
+ if (is_array($validator)) {
+ $oExp = $validator[0];
+ $vExp = $validator[1];
+ } else {
+ $oExp = false;
+ $vExp = $validator;
+ }
+ $regex = '/^\s*' . $vExp . '\s*$/s';
+ if (!preg_match($regex, $value)) {
+ return "Value '$value' does not match $regex";
+ }
+ if ($oExp !== false) {
+ if ($option === false) {
+ return 'Option keyword required, but not present';
+ }
+ $regex = '/^\s*' . $oExp . '\s*$/s';
+ if (!preg_match($regex, $option)) {
+ return "Option keyword '$option' does not match $regex";
+ }
+ }
+ return true;
+ }
+
+ private function getEolChar()
+ {
+ $rn = substr_count("\r\n", $this->data);
+ $r = substr_count("\r", $this->data) - $rn;
+ $n = substr_count("\n", $this->data) - $rn;
+ if ($rn > $r && $rn > $n) {
+ $eol = "\r\n";
+ } elseif ($r > $n) {
+ $eol = "\r";
+ } else {
+ $eol = "\n";
+ }
+ return $eol;
+ }
+
+ /*
+ *
+ */
+
+ public function getError()
+ {
+ return $this->error;
+ }
+
+ public function getWarnings()
+ {
+ return $this->warnings;
+ }
+
+ public function getUISettings()
+ {
+ $result = array();
+ foreach ($this->settings as $mk => $option) {
+ $isUi = ($option->block !== false && $option->block->isUi()) || isset($this->UI_KEYWORDS[$mk]);
+ if ($isUi) {
+ $result[] = $mk;
+ }
+ }
+ return $result;
+ }
+
+ public function getSetting($name)
+ {
+ if (!isset($this->settings[$name]))
+ return false;
+ return new PpdSetting($this->settings[$name], isset($this->UI_KEYWORDS[$name]), $this->encoder);
+ }
+
+ public function removeSetting($name)
+ {
+ if (!isset($this->settings[$name]))
+ return false;
+ $setting = $this->settings[$name];
+ $ranges = array();
+ $this->mergeRanges($ranges, $setting->default);
+ $this->mergeRanges($ranges, $setting->custom);
+ $this->mergeRanges($ranges, $setting->customParam);
+ foreach ($setting->foomatic as $obj) {
+ $this->mergeRanges($ranges, $obj);
+ }
+ foreach ($setting->values as $obj) {
+ $this->mergeRanges($ranges, $obj);
+ }
+ if ($setting->block !== false && $setting->block->isUi()) {
+ $this->mergeRanges($ranges, $setting->block->start, $setting->block->end);
+ }
+ $tmp = array_map(function ($e) { return $e[0]; }, $ranges);
+ array_multisort($tmp, SORT_NUMERIC, $ranges);
+ $new = '';
+ $last = 0;
+ foreach ($ranges as $range) {
+ $new .= substr($this->data, $last, $range[0] - $last);
+ $last = $range[1];
+ if ($this->data{$last} === "\r") {
+ $last++;
+ }
+ if ($this->data{$last} === "\n") {
+ $last++;
+ }
+ }
+ $new .= substr($this->data, $last);
+ $this->data = $new;
+ $this->parse();
+ return $this->error === false;
+ }
+
+ public function addEmptyOption($settingName, $option, $translation = false, $prepend = true)
+ {
+ if (!isset($this->settings[$settingName]))
+ return false;
+ $setting = $this->settings[$settingName];
+ $pos = false;
+ if (!empty($setting->values)) {
+ if ($prepend) {
+ $pos = array_reduce($setting->values, function ($carry, $option) { return min($carry, $option->lineOffset); }, PHP_INT_MAX);
+ } else {
+ $pos = array_reduce($setting->values, function ($carry, $option) { return max($carry, $option->lineOffset); }, 0);
+ }
+ } elseif ($setting->default !== false) {
+ $pos = $setting->default->lineOffset;
+ } elseif ($setting->block !== false && $setting->block->isUi()) {
+ $pos = $this->nextLineEnd($setting->block->start);
+ while ($pos !== false && $pos < $this->dataLen && ($this->data{$pos} === "\r" || $this->data{$pos} === "\n")) {
+ $pos++;
+ }
+ }
+ if ($pos === false) {
+ return false;
+ }
+ $line = '*' . $settingName . ' ' . $option;
+ if ($translation !== false) {
+ $line .= '/' . $this->hexTranslation(($this->encoder)($translation, true));
+ }
+ $eol = $this->getEolChar();
+ $line .= ': ""' . $eol;
+ $this->data = substr($this->data, 0, $pos) . $line . substr($this->data, $pos);
+ $this->parse();
+ return $this->error === false;
+ }
+
+ public function setDefaultOption($settingName, $optionName)
+ {
+ if (!isset($this->settings[$settingName]))
+ return false;
+ $setting = $this->settings[$settingName];
+ $line = '*Default' . $settingName . ': ' . $optionName;
+ if ($setting->default !== false) {
+ $start = $setting->default->lineOffset;
+ $end = $start + $setting->default->lineLen;
+ } elseif (empty($setting->values)) {
+ return false;
+ } else {
+ $option = reset($setting->values);
+ $end = $start = $option->lineOffset;
+ $line .= $this->getEolChar();
+ }
+ $this->data = substr($this->data, 0, $start) . $line . substr($this->data, $end);
+ $this->parse();
+ return $this->error === false;
+ }
+
+ public function write($file)
+ {
+ return file_put_contents($file, $this->data);
+ }
+
+ private function mergeRanges(&$ranges, $start, $end = false)
+ {
+ if (is_object($start) && get_class($start) === 'PpdOption') {
+ $end = $start->lineOffset + $start->lineLen;
+ $start = $start->lineOffset;
+ }
+ if ($start === false || $end === false)
+ return;
+ if ($start >= $end)
+ return; // Don't even bother
+ foreach (array_keys($ranges) as $key) {
+ if ($start <= $ranges[$key][0] && $end >= $ranges[$key][1]) {
+ // Fully dominated
+ unset($ranges[$key]);
+ continue; // Might partially overlap with additional ranges, keep going
+ }
+ if ($ranges[$key][0] <= $start && $ranges[$key][1] >= $start) {
+ // $start lies within existing range
+ if ($ranges[$key][0] <= $end && $ranges[$key][1] >= $end)
+ return; // Fully in existing range, do nothing
+ // $end seems to extend range we're checking against but $start lies within this range, update and keep going
+ $start = $ranges[$key][0];
+ unset($ranges[$key]);
+ continue;
+ }
+ // Last possibility: $start is before range, $end within range
+ if ($ranges[$key][0] <= $end && $ranges[$key][1] >= $end) {
+ // $start must lie before range start, otherwise we'd have hit the case above
+ $end = $ranges[$key][1];
+ unset($ranges[$key]);
+ continue;
+ }
+ }
+ $ranges[] = array($start, $end);
+ }
+
+ /**
+ * @return bool whether there was at least one known option with format restriction violated.
+ */
+ public function hasInvalidOption()
+ {
+ return $this->knownKeywordMalformed;
+ }
+
+}
+
+/*
+ * Helper classes
+ */
+
+/**
+ * Class PpdOption represents a ppd option
+ */
+class PpdSetting
+{
+
+ /**
+ * @var string default value for this option, or false if not set
+ */
+ public $default = false;
+ /**
+ * @var string|bool what type of block this is in.
+ * Format: Group<groupname>/SubGroup<subgroupname>
+ */
+ public $group = false;
+ /**
+ * @var bool true if this is a ui option
+ */
+ public $isUi;
+ /**
+ * @var string[] list of options mapping optionKeyword => translation
+ */
+ public $options = array();
+ /**
+ * @var bool|string FoomaticRIPOption (format of option) if set, false otherwise
+ */
+ public $foomaticOption = false;
+
+ /**
+ * @var bool|string PickOne, Boolean or PickMany
+ */
+ public $uiOptionType = false;
+
+ public $uiOptionTranslation = false;
+
+ /**
+ * PpdSetting constructor.
+ *
+ * @param \PpdSettingInternal $setting
+ */
+ public function __construct($setting, $isUi, $enc)
+ {
+ if ($setting->default !== false) {
+ $this->default = $setting->default->option;
+ }
+ if ($setting->block !== false && $setting->block->isUi()) {
+ $this->uiOptionType = $setting->block->value;
+ $this->uiOptionTranslation = $enc($setting->block->translation);
+ $this->isUi = true;
+ } else if ($isUi) {
+ $this->uiOptionType = 'PickOne'; // Kinda our fallback
+ $this->isUi = true;
+ } else {
+ $this->isUi = false;
+ }
+ $block = $setting->block;
+ while ($block !== false) {
+ if ($block->isUi()) {
+ if ($this->group === false) {
+ $this->group = $block->type . $block->id;
+ } else {
+ $this->group = $block->type . $block->id . '/' . $this->group;
+ }
+ }
+ $block = $block->parent;
+ }
+ foreach ($setting->values as $value) {
+ $this->options[$value->option] = $enc($value->optionTranslation);
+ }
+ if (isset($setting->foomatic['Option'])) {
+ $this->foomaticOption = $setting->foomatic['Option']->option;
+ }
+ }
+
+}
+
+class PpdSettingInternal
+{
+ /**
+ * @var \PpdOption
+ */
+ public $default = false;
+ /**
+ * @var \PpdOption[]
+ */
+ public $values = array();
+ /**
+ * @var \PpdOption[]
+ */
+ public $foomatic = array();
+ /**
+ * @var \PpdOption
+ */
+ public $custom = false;
+ /**
+ * @var \PpdOption
+ */
+ public $customParam = false;
+ /**
+ * @var \PpdBlockInternal the innermost block this option resides in
+ */
+ public $block = false;
+}
+
+class PpdOption
+{
+ public $option;
+ public $optionTranslation;
+ public $lineOffset;
+ public $lineLen;
+ public $multiLine = false;
+
+ public function __construct($lineOffset, $lineLen, $option, $optionTranslation)
+ {
+ $this->option = $option;
+ $this->optionTranslation = $optionTranslation;
+ $this->lineOffset = $lineOffset;
+ $this->lineLen = $lineLen;
+ }
+}
+
+/**
+ * Class PpdBlock represents a Group, SubGroup, or UI block
+ */
+class PpdBlockInternal
+{
+ public $id;
+ public $translation;
+ public $type;
+ /**
+ * @var \PpdBlockInternal[]
+ */
+ public $childBlocks = array();
+ /**
+ * @var \PpdBlockInternal
+ */
+ public $parent;
+
+ /**
+ * @var int start byte in ppd
+ */
+ public $start;
+
+ /**
+ * @var int|bool end byte in ppd, false if block is not closed
+ */
+ public $end = false;
+
+ /**
+ * @var string value of opening line for block, e.g. 'PickOne' for OpenUI
+ */
+ public $value = false;
+
+ public function __construct($id, $translation, $type, $parent, $start)
+ {
+ $this->id = $id;
+ $this->translation = $translation;
+ $this->type = $type;
+ $this->parent = $parent;
+ $this->start = $start;
+ }
+
+ /**
+ * @return bool true if this is a UI block
+ */
+ public function isUi()
+ {
+ return $this->type == 'UI' || $this->type === 'JCLUI';
+ }
+
+ /**
+ * @param \PpdBlockInternal $block some other PpdBlock instance
+ * @return bool true if this is a child of $block
+ */
+ public function isChildOf($block)
+ {
+ $parent = $this->parent;
+ while ($parent !== false) {
+ if ($parent === $block) {
+ return true;
+ }
+ $parent = $parent->parent;
+ }
+ return false;
+ }
+
+}
diff --git a/modules-available/sysconfig/lang/de/template-tags.json b/modules-available/sysconfig/lang/de/template-tags.json
index b2d5dfd0..4618aa08 100644
--- a/modules-available/sysconfig/lang/de/template-tags.json
+++ b/modules-available/sysconfig/lang/de/template-tags.json
@@ -28,6 +28,8 @@
"lang_confirmDeleteQuestion": "Wollen Sie dieses Element wirklich l\u00f6schen?",
"lang_connectionWait": "\u00dcberpr\u00fcfe Verbindung, bitte warten",
"lang_continueAnyway": "Trotzdem weiter",
+ "lang_credPassingNotes": "Wenn Sie diese Option aktivieren, werden Nutzername und Passwort des angemeldeten Benutzers an das Startskript der gew\u00e4hlten Veranstaltung \u00fcbergeben. Somit kann der Ersteller der Veranstaltung mit seinem Script beim Start der VM automatisiert weitere Dienste ansteuern, die eine Authentifizierung erfordern. Sie sollten diese Funktion nur aktivieren, wenn Sie der Personengruppe, die Veranstaltungen bearbeiten kann, entsprechend vertrauen.",
+ "lang_credentialPassing": "Zugangsdaten des Nutzers an das Startscript der Veranstaltung \u00fcbergeben.",
"lang_customCertificate": "Zur Validierung zus\u00e4tzlich erforderliche (Intermediate-)Zertifikate",
"lang_customModuleInfo1": "\u00dcber ein benutzerdefiniertes Modul ist es m\u00f6glich, beliebige Dateien zum Linux-Grundsystem, das auf den Clients gebootet wird, hinzuzuf\u00fcgen. Dazu kann ein Archiv mit einer Dateisystemstruktur hochgeladen werden, die in dieser Form 1:1 in das gebootete Linux extrahiert wird.",
"lang_customModuleInfo2": "Beispiel: Enth\u00e4lt das hochgeladene Archiv eine Datei etc\/beispiel.conf, so wird auf einem gebooteten Client diese Datei als \/etc\/beispiel.conf zu finden sein.",
@@ -36,11 +38,12 @@
"lang_dnLookup": "Ermitteln der Bind-DN",
"lang_download": "Herunterladen",
"lang_downloadLong": "Dieses Modul \"so wie es ist\" herunterladen.",
+ "lang_driveLetterNote": "WICHTIG: Bitte w\u00e4hlen Sie einen Laufwerksbuchstaben, der in den eingesetzten VMs verf\u00fcgbar ist, da ansonsten auf einen anderen Buchstaben ausgewichen werden muss.",
"lang_editLong": "Modul oder Konfiguration bearbeiten.",
"lang_editingLocationInfo": "Sie setzen die Konfiguration eines bestimmten Raums\/Orts, nicht die globale Konfiguration",
"lang_folderRedirection": "Folder Redirection",
"lang_generateModule": "Modul erzeugen",
- "lang_handlingNotes": "Hier k\u00f6nnen Sie festlegen, wie Netzwerk-Shares (inkl. des Home-Verzeichnisses) an Virtuelle Maschinen durchgereicht werden. In \u00e4lteren Versionen von bwLehrpool wurden die VMware Shared Folders genutzt, was mit bestimmten file servern Probleme verursachen konnte. Der neue native Modus funktioniert deutlich besser, ist aber bei Windows-G\u00e4sten darauf angewiesen, dass (1) der file server smb\/cifs spricht (z.B. Windows Server, Samba unter Linux) und (2) die openslx.exe im Autostart eingebunden ist (bei den bwLehrpool Vorlagen bereits der Fall). Der native Modus mit Fallback auf VMware ist experimentell und kann dazu f\u00fchren, dass die VM in regelm\u00e4\u00dfigen Abst\u00e4nden H\u00e4nger hat. WICHTIG: Bitte w\u00e4hlen Sie einen Laufwerksbuchstaben, der in den eingesetzten VMs verf\u00fcgbar ist, da ansonsten auf einen anderen Buchstaben ausgewichen werden muss.",
+ "lang_handlingNotes": "Hier k\u00f6nnen Sie festlegen, wie Netzwerk-Shares (inkl. des Home-Verzeichnisses) an Virtuelle Maschinen durchgereicht werden. In \u00e4lteren Versionen von bwLehrpool wurden die VMware Shared Folders genutzt, was mit bestimmten file servern Probleme verursachen konnte. Der neue native Modus funktioniert deutlich besser, ist aber bei Windows-G\u00e4sten darauf angewiesen, dass (1) der file server smb\/cifs spricht (z.B. Windows Server, Samba unter Linux) und (2) die openslx.exe im Autostart eingebunden ist (bei den bwLehrpool Vorlagen bereits der Fall).",
"lang_helpHomeAttrHead": "Name des Home-Verzeichnis-Attributs",
"lang_helpHomeAttrText": "Hier k\u00f6nnen Sie alternativ zum fest vorgegebenem Template des Home-Verzeichnis Servers den Attributsnamen im Active Directory angeben, der diesen Pfad bereitstellt. Normalerweise ist dies \"homeDirectory\". Wird das Feld leer gelassen, versucht der Assistent, das Attribut selbstst\u00e4ndig zu ermitteln. Falls das Einbinden der Home-Verzeichnisse anschlie\u00dfend nicht funktioniert, \u00fcberpr\u00fcfen Sie bitte den Client-Log (Status->Client Log) und den LDAP-Proxy-Log (Status->Server Status).",
"lang_helpModuleConfiguration": "Konfigurationsmodule sind die Bausteine, aus denen eine Systemkonfiguration erstellt wird. Hier lassen sich sowohl generische Module durch einen Wizard anlegen, als auch komplett eigene Module erstellen (fortgeschritten, Linuxkenntnisse erforderlich).",
@@ -87,10 +90,13 @@
"lang_selfSignedNote": "Das Zertifikat des Servers scheint selbst signiert zu sein. Wenn Sie fortfahren wird versucht, die Zertifikatskette vom Server abzufragen. Dies ist in den meisten F\u00e4llen erfolgreich, sollte aber nur getan werden wenn Sie wissen, dass das Zertifikat des Servers von einer unbekannten CA signiert wurde. Falls die Authentifizierung anschlie\u00dfend nicht funktioniert, \u00fcberpr\u00fcfen Sie die LDAP-Proxy Logs auf der Serverstatus-Seite.",
"lang_shareDesktop": "Desktop (Achtung: Vom Dozent angelegte Verkn\u00fcpfungen nicht sichtbar!)",
"lang_shareDocuments": "Eigene Dokumente",
+ "lang_shareDomainLabel": "Dom\u00e4nenname",
+ "lang_shareDomainNote": "Der Dom\u00e4nenname wird beim Einbinden des Home-Verzeichnisses dem Benutzernamen vorangestellt (DOMAIN\\user). Normalerweise wird der Dom\u00e4nenname automatisch ermittelt, er l\u00e4sst sich hiermit aber explizit \u00fcberschreiben.",
"lang_shareDownloads": "Downloads",
"lang_shareHomeDrive": "Home-Verzeichnis Buchstabe (Windows)",
"lang_shareMapCreate": "Ordner auf dem Netzlaufwerk bei Bedarf anlegen",
"lang_shareMedia": "Eigene Musik, Videos, Bilder",
+ "lang_shareModeNote": "\"Nativer Modus mit Fallback auf VMware\" ist experimentell und kann dazu f\u00fchren, dass die VM in regelm\u00e4\u00dfigen Abst\u00e4nden H\u00e4nger hat.",
"lang_shareOther": "Andere (Saved Games, Kontakte, Favoriten, ...)",
"lang_shareRemapMode": "Einbindemodus",
"lang_show": "Ansehen",
diff --git a/modules-available/sysconfig/lang/en/template-tags.json b/modules-available/sysconfig/lang/en/template-tags.json
index 15516bf2..9b399f04 100644
--- a/modules-available/sysconfig/lang/en/template-tags.json
+++ b/modules-available/sysconfig/lang/en/template-tags.json
@@ -28,6 +28,8 @@
"lang_confirmDeleteQuestion": "Are you sure you want to delete this entry?",
"lang_connectionWait": "Checking connection, please wait",
"lang_continueAnyway": "Continue anyway",
+ "lang_credPassingNotes": "If you enable this option, the username and password of the logged in user will be passed to the start script inside the VM, which is supplied by the tutor who created the lecture. This can be used to automatically authenticate the user to additional services by script. You should only enable this option if you trust the group of users eligible to edit lectures.",
+ "lang_credentialPassing": "Pass user credentials to start script.",
"lang_customCertificate": "Additional (intermediate) certificates required for certificate validation",
"lang_customModuleInfo1": "About a custom module, it is possible to add arbitrary files to a Linux system that is booted clients. For this purpose, an archive can be uploaded using a file system structure that is extracted in this form 1:1 in the booted Linux.",
"lang_customModuleInfo2": "Example: If the uploaded archive is the file etc\/example.conf, this file will be located as \/etc\/example.conf to a booted client.",
@@ -36,11 +38,12 @@
"lang_dnLookup": "Looking up bind dn",
"lang_download": "Download",
"lang_downloadLong": "Download module \"as is\".",
+ "lang_driveLetterNote": "IMPORTANT: Pick a drive letter for the home directory that will be free in the Virtual Machines. Otherwise, a random letter will be assigned.",
"lang_editLong": "Edit module or configuration.",
"lang_editingLocationInfo": "You're setting the configuration for a specific location, not the global one",
"lang_folderRedirection": "Folder Redirection",
"lang_generateModule": "Generating module",
- "lang_handlingNotes": "Here you can configure how network shares (like the user's home directory) are mapped inside the VM. Old Versions of bwLehrpool used the VMware Shared Folder technique, which could cause problems with certain file servers. The new \"native mode\" works much better, but on Windows guests, it requires that you (1) use an smb\/cifs file server (Windows Server, Linux with Samba) and (2) have openslx.exe setup to autorun in the VM (this is already configured for bwLehrpool templates). Native mode with fallback is experimental and known to cause temporary freezes with some VMs. Use with care. IMPORTANT: Pick a drive letter for the home directory that will be free in the Virtual Machines. Otherwise, a random letter will be assigned.",
+ "lang_handlingNotes": "Here you can configure how network shares (like the user's home directory) are mapped inside the VM. Old Versions of bwLehrpool used the VMware Shared Folder technique, which could cause problems with certain file servers. The new \"native mode\" works much better, but on Windows guests, it requires that you (1) use an smb\/cifs file server (Windows Server, Linux with Samba) and (2) have openslx.exe setup to autorun in the VM (this is already configured for bwLehrpool templates).",
"lang_helpHomeAttrHead": "Name of the home directory attribute",
"lang_helpHomeAttrText": "Here you can specify the name of the attribute on the Active Directory that contains the path of the home directory server. Usually this is \"homeDirectory\". If you leave this blank, the wiszard will try to determine the attribute name automatically. If home directories don't work, check the client log (Status->Client log) and the LDAP proxy log (Status->Server status).",
"lang_helpModuleConfiguration": "Configuration modules are the building blocks from which a system configuration is created. Here you can create both generic modules by a wizard, as well as create completely custom modules (advanced Linux knowledge required).",
@@ -87,10 +90,13 @@
"lang_selfSignedNote": "The certificate of this server cannot be verified using the builtin trust store. If you know that the server's certificate was signed by an unknown CA, you can try to proceed. The chain will then be extracted from the server, which should be successful in most cases. If the authentication module does not work afterwards, check the LDAP-proxy logs on the server status page.",
"lang_shareDesktop": "Desktop (Might hide shortcuts created by the tutor)",
"lang_shareDocuments": "My Documents",
+ "lang_shareDomainLabel": "Domain name",
+ "lang_shareDomainNote": "The user name will be prefixed by the domain when trying to mount home directories (DOMAIN\\user). Usually this will be determined automatically, but you can always override it here.",
"lang_shareDownloads": "Downloads",
"lang_shareHomeDrive": "Home drive letter (Windows)",
"lang_shareMapCreate": "Create folders on network share if they don't exist",
"lang_shareMedia": "My Music, Videos, Pictures",
+ "lang_shareModeNote": "\"Native mode with fallback\" is experimental and known to cause temporary freezes with some VMs. Use with care.",
"lang_shareOther": "Other (Saved Games, Contacts, Favorites, ...)",
"lang_shareRemapMode": "Mapping mode",
"lang_show": "Show",
diff --git a/modules-available/sysconfig/templates/ad_ldap-homedir.html b/modules-available/sysconfig/templates/ad_ldap-homedir.html
index de64e49e..2ced563d 100644
--- a/modules-available/sysconfig/templates/ad_ldap-homedir.html
+++ b/modules-available/sysconfig/templates/ad_ldap-homedir.html
@@ -1,8 +1,3 @@
-<div class="slx-bold">
- {{lang_homedirHandling}}
-</div>
-<div><i>{{lang_handlingNotes}}</i></div>
-
<!-- 'shareRemapMode', 'shareRemapCreate', 'shareDocuments', 'shareDownloads', 'shareDesktop', 'shareMedia', 'shareOther' -->
<form method="post" class="form-horizontal" action="?do=SysConfig&amp;action=addmodule&amp;step={{next}}">
@@ -21,6 +16,17 @@
{{/ssl}}
<input name="fingerprint" value="{{fingerprint}}" type="hidden">
+ <div class="slx-bold">{{lang_credentialPassing}}</div>
+ <div class="checkbox">
+ <input type="checkbox" class="form-control" id="inputcredentialPassthrough" name="credentialPassthrough">
+ <label for="inputcredentialPassthrough">{{lang_credentialPassing}}</label>
+ </div>
+ <div><i>{{lang_credPassingNotes}}</i></div>
+
+ <hr>
+ <div class="slx-bold">{{lang_homedirHandling}}</div>
+ <div><i>{{lang_handlingNotes}}</i></div>
+
<div class="form-group row">
<label for="inputshareRemapMode" class="control-label col-xs-4">{{lang_shareRemapMode}}</label>
<div class="col-xs-8">
@@ -30,6 +36,14 @@
<option value="3" {{shareRemapMode_3}}>{{lang_mapModeVmware}}</option>
<option value="2" {{shareRemapMode_2}}>{{lang_mapModeNativeFallback}}</option>
</select>
+ <p><i>{{lang_shareModeNote}}</i></p>
+ </div>
+ </div>
+ <div class="form-group row">
+ <label for="inputshareDomain" class="control-label col-xs-4">{{lang_shareDomainLabel}}</label>
+ <div class="col-xs-8">
+ <input type="text" class="form-control" id="inputshareDomain" name="shareDomain">
+ <p><i>{{lang_shareDomainNote}}</i></p>
</div>
</div>
<div class="form-group row">
@@ -40,31 +54,37 @@
<option {{selected}}>{{drive}}</option>
{{/drives}}
</select>
+ <p><i>{{lang_driveLetterNote}}</i></p>
</div>
</div>
+
<hr>
<div class="slx-bold">{{lang_folderRedirection}}</div>
<div><i>{{lang_redirectionWarning}}</i></div>
- <div>
- <label class="control-label"><input type="checkbox" name="shareDocuments" {{shareDocuments_c}}> {{lang_shareDocuments}}</label>
+
+ <div class="checkbox">
+ <input id="shareDocuments" type="checkbox" class="form-control" name="shareDocuments" {{shareDocuments_c}}>
+ <label for="shareDocuments">{{lang_shareDocuments}}</label>
</div>
- <div>
- <label class="control-label"><input type="checkbox" name="shareDownloads" {{shareDownloads_c}}> {{lang_shareDownloads}}</label>
+ <div class="checkbox">
+ <input id="shareDownloads" type="checkbox" class="form-control" name="shareDownloads" {{shareDownloads_c}}>
+ <label for="shareDownloads">{{lang_shareDownloads}}</label>
</div>
- <div>
- <label class="control-label"><input type="checkbox" name="shareDesktop" {{shareDesktop_c}}> {{lang_shareDesktop}}</label>
+ <div class="checkbox">
+ <input id="shareDesktop" type="checkbox" class="form-control" name="shareDesktop" {{shareDesktop_c}}>
+ <label for="shareDesktop">{{lang_shareDesktop}}</label>
</div>
- <div>
- <label class="control-label"><input type="checkbox" name="shareMedia" {{shareMedia_c}}> {{lang_shareMedia}}</label>
+ <div class="checkbox">
+ <input id="shareMedia" type="checkbox" class="form-control" name="shareMedia" {{shareMedia_c}}>
+ <label for="shareMedia">{{lang_shareMedia}}</label>
</div>
- <div>
- <label class="control-label"><input type="checkbox" name="shareOther" {{shareOther_c}}> {{lang_shareOther}}</label>
+ <div class="checkbox">
+ <input id="shareOther" type="checkbox" class="form-control" name="shareOther" {{shareOther_c}}>
+ <label for="shareOther">{{lang_shareOther}}</label>
</div>
- <div class="row">
- <div class="col-xs-2"></div>
- <div class="col-xs-10">
- <label class="control-label"><input type="checkbox" name="shareRemapCreate" {{shareRemapCreate_c}}> {{lang_shareMapCreate}}</label>
- </div>
+ <div class="checkbox">
+ <input id="shareRemapCreate" type="checkbox" class="form-control" name="shareRemapCreate" {{shareRemapCreate_c}}>
+ <label for="shareRemapCreate">{{lang_shareMapCreate}}</label>
</div>
<div class="pull-right">
<button type="submit" class="btn btn-primary">{{lang_next}} &raquo;</button>
diff --git a/modules-available/sysconfig/templates/branding-check.html b/modules-available/sysconfig/templates/branding-check.html
index 8f6ef055..1a021309 100644
--- a/modules-available/sysconfig/templates/branding-check.html
+++ b/modules-available/sysconfig/templates/branding-check.html
@@ -21,6 +21,11 @@
<label for="title-id">{{lang_title}}</label>
<input type="text" name="title" value="{{title}}" id ="title-id" class="form-control" placeholder="Name des Moduls">
</div>
- <button type="submit" class="btn btn-primary">{{lang_save}}</button>
+ <div class="btn-group">
+ <a class="btn btn-default" href="?do=SysConfig&action=addmodule&step=Branding_Start">{{lang_cancel}}</a>
+ </div>
+ <div class="btn-group pull-right">
+ <button type="submit" class="btn btn-primary">{{lang_save}}</button>
+ </div>
</form>
</div>
diff --git a/modules-available/systemstatus/lang/de/template-tags.json b/modules-available/systemstatus/lang/de/template-tags.json
index 04ee998b..3b929a6b 100644
--- a/modules-available/systemstatus/lang/de/template-tags.json
+++ b/modules-available/systemstatus/lang/de/template-tags.json
@@ -11,6 +11,7 @@
"lang_goToStoreConf": "Zur VM-Store-Konfiguration wechseln",
"lang_iAmSure": "Ja, ich bin sicher",
"lang_ldadpLog": "AD\/LDAP-Proxy Log",
+ "lang_lighttpdLog": "lighttpd-Error Log",
"lang_logicCPUs": "Logische CPUs",
"lang_maintenance": "Maintenance",
"lang_notDetermined": "Konnte nicht ermittelt werden",
diff --git a/modules-available/systemstatus/lang/en/template-tags.json b/modules-available/systemstatus/lang/en/template-tags.json
index e756b256..b8730411 100644
--- a/modules-available/systemstatus/lang/en/template-tags.json
+++ b/modules-available/systemstatus/lang/en/template-tags.json
@@ -11,6 +11,7 @@
"lang_goToStoreConf": "Go to VM store configuration",
"lang_iAmSure": "Yes, I am sure",
"lang_ldadpLog": "AD\/LDAP proxy log",
+ "lang_lighttpdLog": "lighttpd-error log",
"lang_logicCPUs": "Logic CPUs",
"lang_maintenance": "Maintenance",
"lang_notDetermined": "Could not be determined",
diff --git a/modules-available/systemstatus/page.inc.php b/modules-available/systemstatus/page.inc.php
index 59a69267..b9fef0fd 100644
--- a/modules-available/systemstatus/page.inc.php
+++ b/modules-available/systemstatus/page.inc.php
@@ -269,6 +269,38 @@ class Page_SystemStatus extends Page
echo '<pre>', htmlspecialchars(substr($data, $start), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'), '</pre>';
}
+ protected function ajaxLighttpdLog()
+ {
+ $fh = @fopen('/var/log/lighttpd/error.log', 'r');
+ if ($fh === false) {
+ echo 'Error opening log file';
+ return;
+ }
+ fseek($fh, -6000, SEEK_END);
+ $data = fread($fh, 6000);
+ @fclose($fh);
+ if ($data === false) {
+ echo 'Error reading from log file';
+ return;
+ }
+ // If we could read less, try the .1 file too
+ $amount = 6000 - strlen($data);
+ if ($amount > 100) {
+ $fh = @fopen('/var/log/lighttpd/error.log.1', 'r');
+ if ($fh !== false) {
+ fseek($fh, -$amount, SEEK_END);
+ $data = fread($fh, $amount) . $data;
+ @fclose($fh);
+ }
+ }
+ if (strlen($data) < 5990) {
+ $start = 0;
+ } else {
+ $start = strpos($data, "\n") + 1;
+ }
+ echo '<pre>', htmlspecialchars(substr($data, $start), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'), '</pre>';
+ }
+
protected function ajaxLdadpLog()
{
$haveSysconfig = Module::isAvailable('sysconfig');
diff --git a/modules-available/systemstatus/templates/_page.html b/modules-available/systemstatus/templates/_page.html
index 51aa5b55..0573a20c 100644
--- a/modules-available/systemstatus/templates/_page.html
+++ b/modules-available/systemstatus/templates/_page.html
@@ -97,6 +97,14 @@
</div>
<div class="panel panel-default">
<div class="panel-heading">
+ {{lang_lighttpdLog}}
+ </div>
+ <div class="panel-body" id="lighttpd-log">
+ <span class="glyphicon glyphicon-refresh slx-rotation"></span>
+ </div>
+</div>
+<div class="panel panel-default">
+ <div class="panel-heading">
netstat -tulpn
</div>
<div class="panel-body" id="netstat">
@@ -124,6 +132,7 @@
$('#netstat').load('?do=SystemStatus&action=Netstat');
$('#pslist').load('?do=SystemStatus&action=PsList');
$('#ldadp-log').load('?do=SystemStatus&action=LdadpLog');
+ $('#lighttpd-log').load('?do=SystemStatus&action=LighttpdLog');
}, 300);
}, false);
</script>
diff --git a/modules-available/webinterface/lang/de/messages.json b/modules-available/webinterface/lang/de/messages.json
new file mode 100644
index 00000000..24ca7d5f
--- /dev/null
+++ b/modules-available/webinterface/lang/de/messages.json
@@ -0,0 +1,6 @@
+{
+ "https-on-cert-missing": "HTTPS ist aktiviert, das Zertifikat ist jedoch nicht vorhanden. Bitte nehmen Sie die HTTPS-Konfiguration erneut vor.",
+ "https-used-without-cert": "HTTPS wird gerade verwendet, obwohl kein Zertifikat installiert ist. Falls Sie die Webserver-Konfiguration manuell angepasst haben, um HTTPS zu aktivieren beachten Sie bitte, dass die Konfiguration bei einem zuk\u00fcnftigen Server-Update ohne Nachfrage \u00fcberschrieben werden k\u00f6nnte.",
+ "https-want-off-is-used": "HTTPS wird gerade verwendet, obwohl es laut Einstellungen deaktiviert ist. Merkw\u00fcrdig.",
+ "https-want-redirect-is-plain": "Weiterleitung von HTTP auf HTTPS ist aktiviert, trotzdem scheint die Verbindung Ihres Browsers mit dem Server unverschl\u00fcsselt zu sein. Nehmen Sie die Konfiguration erneut vor und wenden Sie sich an den Support, wenn das Problem weiterhin besteht."
+} \ No newline at end of file
diff --git a/modules-available/webinterface/lang/de/template-tags.json b/modules-available/webinterface/lang/de/template-tags.json
index 3ac6186c..ea1074d2 100644
--- a/modules-available/webinterface/lang/de/template-tags.json
+++ b/modules-available/webinterface/lang/de/template-tags.json
@@ -1,17 +1,23 @@
{
- "lang_HttpsIsDisabled": "HTTPS ist derzeit deaktiviert",
"lang_applyingSettings": "Anwenden der Einstellungen",
"lang_caChain": "Optional k\u00f6nnen Sie hier die zum Zertifikat geh\u00f6rende Zertifikatkette (CA-Chain) einf\u00fcgen. Dies wird ben\u00f6tigt, wenn das Zertifikat nicht direkt von einer der in Browsern mitgeliferten CAs signiert wurde. Die Datei enth\u00e4lt ein oder meherere Zertifikatsbl\u00f6cke, im gleichen Format wie das oben gezeigte Zertifikat.",
"lang_certificate": "Bitte f\u00fcgen Sie hier das Zertifikat ein. Das Zertifikat wird im Base64-codierten x509-Format erwartet (manchmal pem genannt). Es sieht in etwa wie folgt aus:",
"lang_customCert": "Eigenes Zertifikat verwenden",
+ "lang_generatedSelected": "Der Server verwendet zur Zeit ein automatisch generiertes, selbst signiertes Zertifikat.",
"lang_hidePasswords": "Passw\u00f6rter maskieren",
"lang_httpsDescription": "Hier k\u00f6nnen Sie festlegen, ob das Web-Interface auch per HTTPS erreichbar sein soll, und welches Zertifikat daf\u00fcr verwendet werden soll.",
+ "lang_httpsRedirect": "Anfragen per HTTP immer auf HTTPS umleiten (sofern aktiviert)",
"lang_httpsSettings": "HTTPS-Konfiguration",
"lang_installAndRestart": "Zertifikat installieren und Webserver neustarten",
"lang_noHttps": "HTTPS wieder deaktivieren, aktuelles Zertifikat l\u00f6schen",
+ "lang_offSelected": "HTTPS ist derzeit deaktiviert.",
"lang_passwordFields": "Passwortfelder",
"lang_passwordsDescription": "Legen Sie fest, ob Passwortfelder in der Web-Schnittstelle maskiert werden, oder ob Ihr Inhalt sichtbar sein soll. Wenn Sie die Schnittstelle in einer sicheren Umgebung nutzen (keine neugierigen Augen), kann dies den Komfort erh\u00f6hen. Das Passwortfeld der Anmeldemaske ist von dieser Einstellung ausgenommen.",
"lang_privateKey": "Bitte f\u00fcgen Sie hier den privaten Schl\u00fcssel ein, der zum obigen Zertifikat geh\u00f6rt. Er muss ebenfalls im \"pem\"-Format vorliegen, und sieht wie folgt aus:",
"lang_randomCert": "Neues selbstsigniertes Zertifikat generieren",
- "lang_showPasswords": "Passw\u00f6rter anzeigen"
+ "lang_showPasswords": "Passw\u00f6rter anzeigen",
+ "lang_suppliedSelected": "Der Server verwendet zur Zeit ein \u00fcber die Option \"Eigenes Zertifikat\" hochgeladenes Zertifikat.",
+ "lang_unknownSelected": "Unbekanntes oder ung\u00fcltiges Zertifikat vorhanden. Wahrscheinlich wurde der Server von einer alten Version aktualisiert. Um diese Meldung zu entfernen, die HTTPS-Konfiguration erneut vornehmen.",
+ "lang_youreNotUsingHttps": "Sie besuchen diese Seite nicht per HTTPS (oder die HTTPS-Terminierung wird von einem vorgeschalteten Proxy \u00fcbernommen).",
+ "lang_youreUsingHttps": "Sie besuchen diese Seite (aus Sicht des Webservers) per HTTPS."
} \ No newline at end of file
diff --git a/modules-available/webinterface/lang/en/messages.json b/modules-available/webinterface/lang/en/messages.json
new file mode 100644
index 00000000..803dc73f
--- /dev/null
+++ b/modules-available/webinterface/lang/en/messages.json
@@ -0,0 +1,6 @@
+{
+ "https-on-cert-missing": "HTTPS is enabled, but the certificate is missing. Please redo the configuration steps.",
+ "https-used-without-cert": "HTTPS is currently used, but there is no certificate installed. If you tweaked the web server's configuration manually to enable HTTPS bear in mind that a future server update might overwrite your modified configuration without asking.",
+ "https-want-off-is-used": "HTTPS is currently in use although it is disabled in the settings. Very weird indeed.",
+ "https-want-redirect-is-plain": "HTTP to HTTPS redirects are enabled, but the connection from your browser appears to be unencrypted. Please redo the HTTPS configuration and contact support if the problem persists."
+} \ No newline at end of file
diff --git a/modules-available/webinterface/lang/en/template-tags.json b/modules-available/webinterface/lang/en/template-tags.json
index 4d91e4b6..efe649cb 100644
--- a/modules-available/webinterface/lang/en/template-tags.json
+++ b/modules-available/webinterface/lang/en/template-tags.json
@@ -1,17 +1,23 @@
{
- "lang_HttpsIsDisabled": "HTTPS is currently disabled",
"lang_applyingSettings": "Applying settings",
"lang_caChain": "Here you can paste an optional certificate chain. It should only be required if you have a certificate that was not directly signed by a certificate authority known by the browsers. It should contain one or more certificate blocks, looking just like the certificate above.",
"lang_certificate": "Please paste your certificate below. It has to be in base64 encoded x509 format (sometimes called pem). It should look something like this:",
"lang_customCert": "Supply own certificate",
+ "lang_generatedSelected": "The server is currently using an automatically generated, self-signed certificate.",
"lang_hidePasswords": "Mask passwords",
- "lang_httpsDescription": "Here you can set whether the web interface should be accessible via https. You can chose if you want to use a random self signed certificate, or supply your own.",
+ "lang_httpsDescription": "Here you can set whether the web interface should be accessible via https. You can choose if you want to use a random self signed certificate, or supply your own.",
+ "lang_httpsRedirect": "Redirect incoming HTTP requests to HTTPS (if enabled).",
"lang_httpsSettings": "HTTPS settings",
"lang_installAndRestart": "Installing certificate and restarting web server",
"lang_noHttps": "Disable HTTPS, delete current certificate",
+ "lang_offSelected": "HTTPS is currently disabled.",
"lang_passwordFields": "Password fields",
"lang_passwordsDescription": "Set whether password fields should be masked or not. The password field of the login page to the web interface is always masked.",
"lang_privateKey": "Please paste the private key belonging to the certificate here. It has to be in \"pem\" format too, which should look like this:",
"lang_randomCert": "Generate new self-signed certificate",
- "lang_showPasswords": "Show passwords"
-}
+ "lang_showPasswords": "Show passwords",
+ "lang_suppliedSelected": "The server is currently using a certificate supplied using the \"Supply own certificate\" option.",
+ "lang_unknownSelected": "Unknown or invalid certificate in use. The server war probably updated from an old version while HTTPS was already enabled. Redo the HTTPS configuration steps to get rid of this message.",
+ "lang_youreNotUsingHttps": "You're not using HTTPS to visit this website (or the HTTPS termination is done by a reverse proxy).",
+ "lang_youreUsingHttps": "You're visiting this server through an HTTPS connection (from the server's point of view)."
+} \ No newline at end of file
diff --git a/modules-available/webinterface/page.inc.php b/modules-available/webinterface/page.inc.php
index 3c4304cd..93d659f0 100644
--- a/modules-available/webinterface/page.inc.php
+++ b/modules-available/webinterface/page.inc.php
@@ -3,6 +3,9 @@
class Page_WebInterface extends Page
{
+ const PROP_REDIRECT = 'webinterface.https-redirect';
+ const PROP_TYPE = 'webinterface.https-type';
+
protected function doPreprocess()
{
User::load();
@@ -23,9 +26,11 @@ class Page_WebInterface extends Page
private function actionConfigureHttps()
{
$task = false;
+ $off = '';
switch (Request::post('mode')) {
case 'off':
$task = $this->setHttpsOff();
+ $off = '&hsts=off';
break;
case 'random':
$task = $this->setHttpsRandomCert();
@@ -33,13 +38,17 @@ class Page_WebInterface extends Page
case 'custom':
$task = $this->setHttpsCustomCert();
break;
+ default:
+ $task = $this->setRedirectMode();
+ break;
}
if (isset($task['id'])) {
Session::set('https-id', $task['id']);
- Util::redirect('?do=WebInterface&show=httpsupdate');
+ Util::redirect('?do=WebInterface&show=httpsupdate' . $off);
}
+ Util::redirect('?do=WebInterface');
}
-
+
private function actionShowHidePassword()
{
Property::setPasswordFieldType(Request::post('mode') === 'show' ? 'text' : 'password');
@@ -48,10 +57,57 @@ class Page_WebInterface extends Page
protected function doRender()
{
+ //
+ // HTTPS
+ //
if (Request::get('show') === 'httpsupdate') {
Render::addTemplate('httpd-restart', array('taskid' => Session::get('https-id')));
}
- Render::addTemplate('https', array('httpsEnabled' => file_exists('/etc/lighttpd/server.pem')));
+ $type = Property::get(self::PROP_TYPE);
+ $force = Property::get(self::PROP_REDIRECT) === 'True';
+ $https = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
+ $exists = file_exists('/etc/lighttpd/server.pem');
+ $data = array(
+ 'httpsUsed' => $https,
+ 'redirect_checked' => ($force ? 'checked' : '')
+ );
+ // Type should be 'off', 'generated', 'supplied'
+ if ($type === 'off') {
+ if ($exists) {
+ // HTTPS is set to off, but a certificate exists
+ if ($https) {
+ // User is using https, just warn to prevent lockout
+ Message::addWarning('https-want-off-is-used');
+ } else {
+ // User is not using https, try to delete stray certificate
+ $this->setHttpsOff();
+ }
+ } elseif ($https) {
+ // Set to off, no cert found, but still using HTTPS apparently
+ // Admin might have modified web server config in another way
+ Message::addWarning('https-used-without-cert');
+ }
+ } elseif ($type === 'generated' || $type === 'supplied') {
+ $data['httpsEnabled'] = true;
+ if ($force && !$https) {
+ Message::addWarning('https-want-redirect-is-plain');
+ }
+ if (!$exists) {
+ Message::addWarning('https-on-cert-missing');
+ }
+ } else {
+ // Unknown config - maybe upgraded old install that doesn't keep track
+ if ($exists || $https) {
+ $type = 'unknown'; // Legacy fallback
+ } else {
+ $type = 'off';
+ }
+ }
+ $data[$type . 'Selected'] = true;
+ Render::addTemplate('https', $data);
+ //
+ // Password fields
+ //
$data = array();
if (Property::getPasswordFieldType() === 'text')
$data['selected_show'] = 'checked';
@@ -62,23 +118,49 @@ class Page_WebInterface extends Page
private function setHttpsOff()
{
+ Property::set(self::PROP_TYPE, 'off');
+ Header('Strict-Transport-Security: max-age=0', true);
return Taskmanager::submit('LighttpdHttps', array());
}
private function setHttpsRandomCert()
{
+ $force = Request::post('httpsredirect', false, 'string') === 'on';
+ Property::set(self::PROP_TYPE, 'generated');
+ Property::set(self::PROP_REDIRECT, $force ? 'True' : 'False');
return Taskmanager::submit('LighttpdHttps', array(
- 'proxyip' => Property::getServerIp()
+ 'proxyip' => Property::getServerIp(),
+ 'redirect' => $force,
));
}
private function setHttpsCustomCert()
{
+ $force = Request::post('httpsredirect', false, 'string') === 'on';
+ Property::set(self::PROP_TYPE, 'supplied');
+ Property::set(self::PROP_REDIRECT, $force ? 'True' : 'False');
return Taskmanager::submit('LighttpdHttps', array(
'importcert' => Request::post('certificate', 'bla'),
'importkey' => Request::post('privatekey', 'bla'),
- 'importchain' => Request::post('cachain', '')
+ 'importchain' => Request::post('cachain', ''),
+ 'redirect' => $force,
+ ));
+ }
+
+ private function setRedirectMode()
+ {
+ $force = Request::post('httpsredirect', false, 'string') === 'on';
+ Property::set(self::PROP_REDIRECT, $force ? 'True' : 'False');
+ if (Property::get(self::PROP_TYPE) === 'off') {
+ // Don't bother running the task if https isn't enabled - just
+ // update the state in DB
+ return false;
+ }
+ return Taskmanager::submit('LighttpdHttps', array(
+ 'redirectOnly' => true,
+ 'redirect' => $force,
));
}
}
+
diff --git a/modules-available/webinterface/templates/httpd-restart.html b/modules-available/webinterface/templates/httpd-restart.html
index cc84aafb..ac4e726b 100644
--- a/modules-available/webinterface/templates/httpd-restart.html
+++ b/modules-available/webinterface/templates/httpd-restart.html
@@ -1,6 +1,42 @@
<div class="panel panel-default">
<div class="panel-heading">{{lang_applyingSettings}}</div>
<div class="panel-body">
- <div data-tm-id="{{taskid}}" data-tm-log="error">{{lang_installAndRestart}}</div>
+ <div data-tm-id="{{taskid}}" data-tm-log="error" data-tm-callback="slxRestartCb">{{lang_installAndRestart}}</div>
</div>
</div>
+<script type="application/javascript"><!--
+
+var slxRedirTimeout = 0;
+var slxRedirTimer = false;
+
+function slxRestartCb(task) {
+ if (!task || !task.statusCode)
+ return;
+ if (task.statusCode === 'TASK_WAITING' || task.statusCode === 'TASK_PROCESSING') {
+ // Polling still works, reset counter
+ console.log('Resetting because ' + task.statusCode);
+ slxRedirTimeout = 0;
+ } else {
+ console.log('Disabling because ' + task.statusCode);
+ clearInterval(slxRedirTimer);
+ window.location.replace(window.location.href.replace('&show=httpsupdate', ''));
+ }
+}
+
+slxRedirTimer = setInterval(function() {
+ // Didn't get status update from TM for 6 seconds - try to switch protocols
+ if (++slxRedirTimeout > 6) {
+ console.log('TIMEOUT REACHED');
+ clearInterval(slxRedirTimer);
+ var url = window.location.href.split(':', 2)[1];
+ if (window.location.protocol === 'https:') {
+ url = 'http:' + url;
+ } else {
+ url = 'https:' + url;
+ }
+ console.log('REDIRECT TO ' + url);
+ window.location.replace(url);
+ }
+}, 1000);
+
+//--></script> \ No newline at end of file
diff --git a/modules-available/webinterface/templates/https.html b/modules-available/webinterface/templates/https.html
index dfd2a3fe..77585ddf 100644
--- a/modules-available/webinterface/templates/https.html
+++ b/modules-available/webinterface/templates/https.html
@@ -5,9 +5,26 @@
<div class="panel-heading">{{lang_httpsSettings}}</div>
<div class="panel-body">
<p>{{lang_httpsDescription}}</p>
- {{^httpsEnabled}}
- <p>{{lang_HttpsIsDisabled}}</p>
- {{/httpsEnabled}}
+ {{^httpsUsed}}
+ {{lang_youreNotUsingHttps}}
+ {{/httpsUsed}}
+ {{#httpsUsed}}
+ {{lang_youreUsingHttps}}
+ {{/httpsUsed}}
+ <div class="text-info slx-bold">
+ {{#offSelected}}
+ <p>{{lang_offSelected}}</p>
+ {{/offSelected}}
+ {{#unknownSelected}}
+ <p>{{lang_unknownSelected}}</p>
+ {{/unknownSelected}}
+ {{#generatedSelected}}
+ <p>{{lang_generatedSelected}}</p>
+ {{/generatedSelected}}
+ {{#suppliedSelected}}
+ <p>{{lang_suppliedSelected}}</p>
+ {{/suppliedSelected}}
+ </div>
{{#httpsEnabled}}
<div class="input-group" onclick="$('#moff').prop('checked', true);
$('#wcustom').hide()">
@@ -31,6 +48,7 @@
{{lang_customCert}}
</span>
</div>
+
<div class="well well-sm" style="display:none" id="wcustom">
{{lang_certificate}}
<pre class="small">
@@ -52,6 +70,16 @@ MIIFfTCCA...
<textarea name="cachain" class="form-control small" cols="101" rows="10"></textarea>
<hr>
</div>
+
+ <br>
+ <div class="input-group">
+ <span class="input-group-addon"><input id="httpsredirect" type="checkbox" name="httpsredirect" value="on" {{redirect_checked}}></span>
+ <span class="form-control" onclick="$('#httpsredirect').prop('checked', !$('#httpsredirect').prop('checked'))">
+ {{lang_httpsRedirect}}
+ </span>
+ </div>
+ <br>
+
<div class="pull-right">
<button type="submit" class="btn btn-primary">{{lang_save}}</button>
</div>
diff --git a/modules-available/webinterface/templates/passwords.html b/modules-available/webinterface/templates/passwords.html
index 1f23dfc4..8481d884 100644
--- a/modules-available/webinterface/templates/passwords.html
+++ b/modules-available/webinterface/templates/passwords.html
@@ -17,6 +17,7 @@
{{lang_hidePasswords}}
</span>
</div>
+ <br>
<div class="pull-right">
<button type="submit" class="btn btn-primary">{{lang_save}}</button>
</div>