summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--apis/clientlog.inc.php8
-rw-r--r--inc/database.inc.php94
-rw-r--r--inc/util.inc.php47
-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.php55
-rw-r--r--modules-available/statistics/page.inc.php70
-rw-r--r--modules-available/statistics/templates/machine-main.html30
-rw-r--r--modules-available/statistics/templates/projector-list.html21
10 files changed, 416 insertions, 20 deletions
diff --git a/apis/clientlog.inc.php b/apis/clientlog.inc.php
index 81a7dbf7..b68e4632 100644
--- a/apis/clientlog.inc.php
+++ b/apis/clientlog.inc.php
@@ -10,7 +10,11 @@ if (empty($_POST['type'])) die('Missing options.');
$type = mb_strtolower($_POST['type']);
if ($type{0} === '~' || $type{0} === '.') {
- require 'modules/statistics/api.inc.php';
+ if (Module::isAvailable('statistics')) {
+ require 'modules/statistics/api.inc.php';
+ }
} else {
- require 'modules/syslog/api.inc.php';
+ if (Module::isAvailable('syslog')) {
+ require 'modules/syslog/api.inc.php';
+ }
}
diff --git a/inc/database.inc.php b/inc/database.inc.php
index 4a5821f4..ff98f5ee 100644
--- a/inc/database.inc.php
+++ b/inc/database.inc.php
@@ -132,4 +132,98 @@ class Database
return self::$dbh->prepare($query);
}
+ /**
+ * Insert row into table, returning the generated key.
+ * This requires the table to have an AUTO_INCREMENT column and
+ * usually requires the given $uniqueValues to span across a UNIQUE index.
+ * The code first tries to SELECT the key for the given values without
+ * inserting first. This means this function is best used for cases
+ * where you expect that the entry already exists in the table, so
+ * only one SELECT will run. For all the entries that do not exist,
+ * an INSERT or INSERT IGNORE is run, depending on whether $additionalValues
+ * is empty or not. Another reason we don't run the INSERT (IGNORE) first
+ * is that it will increase the AUTO_INCREMENT value on InnoDB, even when
+ * no INSERT took place. So if you expect a lot of collisions you might
+ * use this function to prevent your A_I value from counting up too
+ * quickly.
+ * Other than that, this is just a dumb version of running INSERT and then
+ * getting the LAST_INSERT_ID(), or doing a query for the existing ID in
+ * case of a key collision.
+ *
+ * @param string $table table to insert into
+ * @param string $aiKey name of the AUTO_INCREMENT column
+ * @param array $uniqueValues assoc array containing columnName => value mapping
+ * @param array $additionalValues assoc array containing columnName => value mapping
+ * @return int[] list of AUTO_INCREMENT values matching the list of $values
+ */
+ public static function insertIgnore($table, $aiKey, $uniqueValues, $additionalValues = false)
+ {
+ // Sanity checks
+ if (array_key_exists($aiKey, $uniqueValues)) {
+ Util::traceError("$aiKey must not be in \$uniqueValues");
+ }
+ if (is_array($additionalValues) && array_key_exists($aiKey, $additionalValues)) {
+ Util::traceError("$aiKey must not be in \$additionalValues");
+ }
+ // Simple SELECT first
+ $selectSql = 'SELECT ' . $aiKey . ' FROM ' . $table . ' WHERE 1';
+ foreach ($uniqueValues as $key => $value) {
+ $selectSql .= ' AND ' . $key . ' = :' . $key;
+ }
+ $selectSql .= ' LIMIT 1';
+ $res = self::queryFirst($selectSql, $uniqueValues);
+ if ($res !== false) {
+ // Exists
+ if (!empty($additionalValues)) {
+ // Simulate ON DUPLICATE KEY UPDATE ...
+ $updateSql = 'UPDATE ' . $table . ' SET ';
+ $first = true;
+ foreach ($additionalValues as $key => $value) {
+ if ($first) {
+ $first = false;
+ } else {
+ $updateSql .= ', ';
+ }
+ $updateSql .= $key . ' = :' . $key;
+ }
+ $updateSql .= ' WHERE ' . $aiKey . ' = :' . $aiKey;
+ $additionalValues[$aiKey] = $res[$aiKey];
+ Database::exec($updateSql, $additionalValues);
+ }
+ return $res[$aiKey];
+ }
+ // Does not exist:
+ if (empty($additionalValues)) {
+ $combined =& $uniqueValues;
+ } else {
+ $combined = $uniqueValues + $additionalValues;
+ }
+ // Aight, try INSERT or INSERT IGNORE
+ $insertSql = 'INTO ' . $table . ' (' . implode(', ', array_keys($combined))
+ . ') VALUES (:' . implode(', :', array_keys($combined)) . ')';
+ if (empty($additionalValues)) {
+ // Simple INSERT IGNORE
+ $insertSql = 'INSERT IGNORE ' . $insertSql;
+ } else {
+ // INSERT ... ON DUPLICATE (in case we have a race)
+ $insertSql = 'INSERT ' . $insertSql . ' ON DUPLICATE KEY UPDATE ';
+ $first = true;
+ foreach ($additionalValues as $key => $value) {
+ if ($first) {
+ $first = false;
+ } else {
+ $insertSql .= ', ';
+ }
+ $insertSql .= $key . ' = VALUES(' . $key . ')';
+ }
+ }
+ self::exec($insertSql, $combined);
+ // Insert done, retrieve key again
+ $res = self::queryFirst($selectSql, $uniqueValues);
+ if ($res === false) {
+ Util::traceError('Could not find value in table ' . $table . ' that was just inserted');
+ }
+ return $res[$aiKey];
+ }
+
}
diff --git a/inc/util.inc.php b/inc/util.inc.php
index f5e10ebc..5d1a4563 100644
--- a/inc/util.inc.php
+++ b/inc/util.inc.php
@@ -21,6 +21,11 @@ class Util
exit(1);
}
Header('HTTP/1.1 500 Internal Server Error');
+ if (isset($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'html') === false ) {
+ Header('Content-Type: text/plain; charset=utf-8');
+ echo 'API ERROR: ', $message, "\n", self::formatBacktracePlain(debug_backtrace());
+ exit(0);
+ }
Header('Content-Type: text/html; charset=utf-8');
echo '<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><style>', "\n",
".arg { color: red; background: white; }\n",
@@ -79,19 +84,38 @@ SADFACE;
exit(0);
}
+ private static function formatArgument($arg, $expandArray = true)
+ {
+ if (is_string($arg)) {
+ $arg = "'$arg'";
+ } elseif (is_object($arg)) {
+ $arg = 'instanceof ' . get_class($arg);
+ } elseif (is_array($arg)) {
+ if ($expandArray && count($arg) < 20) {
+ $expanded = '';
+ foreach ($arg as $key => $value) {
+ if (!empty($expanded)) {
+ $expanded .= ', ';
+ }
+ $expanded .= $key . ': ' . self::formatArgument($value, false);
+ if (strlen($expanded) > 200)
+ break;
+ }
+ if (strlen($expanded) <= 200)
+ return '[' . $expanded . ']';
+ }
+ $arg = 'Array(' . count($arg) . ')';
+ }
+ return $arg;
+ }
+
public static function formatBacktraceHtml($trace, $escape = true)
{
$output = '';
foreach ($trace as $idx => $line) {
$args = array();
foreach ($line['args'] as $arg) {
- if (is_string($arg)) {
- $arg = "'$arg'";
- } elseif (is_object($arg)) {
- $arg = 'instanceof ' . get_class($arg);
- } elseif (is_array($arg)) {
- $arg = 'Array(' . count($arg) . ')';
- }
+ $arg = self::formatArgument($arg);
$args[] = '<span class="arg">' . htmlspecialchars($arg) . '</span>';
}
$frame = str_pad('#' . $idx, 3, ' ', STR_PAD_LEFT);
@@ -111,14 +135,7 @@ SADFACE;
foreach ($trace as $idx => $line) {
$args = array();
foreach ($line['args'] as $arg) {
- if (is_string($arg)) {
- $arg = "'$arg'";
- } elseif (is_object($arg)) {
- $arg = 'instanceof ' . get_class($arg);
- } elseif (is_array($arg)) {
- $arg = 'Array(' . count($arg) . ')';
- }
- $args[] = $arg;
+ $args[] = self::formatArgument($arg);
}
$frame = str_pad('#' . $idx, 3, ' ', STR_PAD_LEFT);
$args = implode(', ', $args);
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..5d8ce1bc 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,20 @@ if ($addTrigger) {
}
}
+if ($machineHwCreate === UPDATE_DONE) {
+ $ret = true;
+ $ret = Database::exec('ALTER TABLE `machine_x_hw`
+ ADD CONSTRAINT `machine_x_hw_ibfk_1` FOREIGN KEY (`hwid`) REFERENCES `statistic_hw` (`hwid`) ON DELETE CASCADE,
+ ADD CONSTRAINT `machine_x_hw_ibfk_2` FOREIGN KEY (`machineuuid`) REFERENCES `machine` (`machineuuid`) ON DELETE CASCADE') && $ret;
+ $ret = Database::exec('ALTER TABLE `machine_x_hw_prop`
+ ADD CONSTRAINT `machine_x_hw_prop_ibfk_1` FOREIGN KEY (`machinehwid`) REFERENCES `machine_x_hw` (`machinehwid`) ON DELETE CASCADE') && $ret;
+ $ret = Database::exec('ALTER TABLE `statistic_hw_prop`
+ ADD CONSTRAINT `statistic_hw_prop_ibfk_1` FOREIGN KEY (`hwid`) REFERENCES `statistic_hw` (`hwid`) ON DELETE CASCADE') && $ret;
+ if ($ret === false) {
+ finalResponse(UPDATE_FAILED, 'Adding foreign key machineuuid to hardware* failed: ' . Database::lastError());
+ }
+}
+
// Create response
if (in_array(UPDATE_DONE, $res)) {
diff --git a/modules-available/statistics/page.inc.php b/modules-available/statistics/page.inc.php
index 87c8e7c4..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
diff --git a/modules-available/statistics/templates/machine-main.html b/modules-available/statistics/templates/machine-main.html
index 0b333a27..56454e78 100644
--- a/modules-available/statistics/templates/machine-main.html
+++ b/modules-available/statistics/templates/machine-main.html
@@ -130,6 +130,36 @@
<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}}
+ <span class="btn btn-default">{{lang_projector}}</span>
+ <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