<?php
class Page_Roomplanner extends Page
{
/**
* @var ?int locationid of location we're editing, or null if unknown/not set
*/
private $locationid = null;
/**
* @var array location data from location table
*/
private $location = null;
/**
* @var string action to perform
*/
private $action = false;
/**
* @var bool is this a leaf node, with a real room plan, or something in between with a composed plan
*/
private $isLeaf;
private function loadRequestedLocation()
{
$this->locationid = Request::get('locationid', null, 'integer');
if ($this->locationid !== null) {
$locs = Location::getLocationsAssoc();
if (isset($locs[$this->locationid])) {
$this->location = $locs[$this->locationid];
$this->isLeaf = empty($this->location['children']);
}
}
}
protected function doPreprocess()
{
User::load();
if (!User::isLoggedIn()) {
Message::addError('main.no-permission');
Util::redirect('?do=Main');
}
$this->action = Request::any('action', 'show', 'string');
$this->loadRequestedLocation();
if ($this->locationid === null) {
Message::addError('need-locationid');
Util::redirect('?do=locations');
}
if ($this->location === null) {
Message::addError('locations.invalid-location-id', $this->locationid);
Util::redirect('?do=locations');
}
if ($this->action === 'save') {
$this->handleSaveRequest(false);
Util::redirect("?do=roomplanner&locationid={$this->locationid}&action=show");
}
Render::setTitle($this->location['locationname']);
}
protected function doRender()
{
if ($this->action === 'show') {
/* do nothing */
Dashboard::disable();
if ($this->isLeaf) {
$this->showLeafEditor();
} else {
$this->showComposedEditor();
}
} else {
Message::addError('main.invalid-action', $this->action);
}
}
private function showLeafEditor()
{
$config = Database::queryFirst('SELECT roomplan, managerip, tutoruuid
FROM location_roomplan
WHERE locationid = :locationid', ['locationid' => $this->locationid]);
if ($config === false) {
$config = ['managerip' => '', 'tutoruuid' => ''];
}
$runmode = RunMode::getForMode(Page::getModule(), $this->locationid, true);
if (empty($runmode)) {
$config['dedicatedmgr'] = false;
} else {
$runmode = array_pop($runmode);
$config['managerip'] = $runmode['clientip'];
$config['manageruuid'] = $runmode['machineuuid'];
$data = json_decode($runmode['modedata'], true);
$config['dedicatedmgr'] = (isset($data['dedicatedmgr']) && $data['dedicatedmgr']);
}
$furniture = $this->getFurniture($config);
$subnetMachines = $this->getPotentialMachines();
$machinesOnPlan = $this->getMachinesOnPlan($config['tutoruuid']);
$roomConfig = array_merge($furniture, $machinesOnPlan);
$canEdit = User::hasPermission('edit', $this->locationid);
$params = [
'location' => $this->location,
'managerip' => $config['managerip'],
'dediMgrChecked' => $config['dedicatedmgr'] ? 'checked' : '',
'subnetMachines' => json_encode($subnetMachines),
'locationid' => $this->locationid,
'roomConfiguration' => json_encode($roomConfig),
'edit_disabled' => $canEdit ? '' : 'disabled',
'statistics_disabled' =>
(Module::get('statistics') !== false && User::hasPermission('.statistics.machine.view-details'))
? '' : 'disabled',
];
Render::addTemplate('header', $params);
if ($canEdit) {
Render::addTemplate('item-selector', $params);
}
Render::addTemplate('main-roomplan', $params);
Render::addTemplate('footer', $params);
}
private function showComposedEditor()
{
// Load settings
$row = Database::queryFirst("SELECT locationid, roomplan
FROM location_roomplan
WHERE locationid = :lid",
['lid' => $this->locationid]);
$room = new ComposedRoom($row);
$params = [
'location' => $this->location,
'locations' => [],
$room->orientation() . '_checked' => 'checked',
];
if (!$room->shouldSkip()) {
$params['enabled_checked'] = 'checked';
}
$inverseList = array_flip($room->subLocationIds());
$sortList = [];
// Load locations
$locs = Location::getLocationsAssoc();
foreach ($this->location['directchildren'] as $loc) {
if (isset($locs[$loc])) {
$data = $locs[$loc];
if (isset($inverseList[$loc])) {
$sortList[] = $inverseList[$loc];
} else {
$sortList[] = 1000 + $loc;
}
if ($loc === $room->controlRoom()) {
$data['checked'] = 'checked';
}
$params['locations'][] = $data;
}
}
array_multisort($sortList, SORT_ASC | SORT_NUMERIC, $params['locations']);
Render::addTemplate('edit-composed-room', $params);
}
protected function doAjax()
{
$this->action = Request::any('action', false, 'string');
if ($this->action === 'getmachines') {
// Load suggestions when typing in the search box of the "add machine" pop-up
User::load();
$locations = User::getAllowedLocations('edit');
if (empty($locations)) {
die('{"machines":[]}');
}
$roomLocationId = Request::any('locationid', 0, 'int');
$query = Request::get('query', false, 'string');
$aquery = preg_replace('/[^\x01-\x7f]+/', '%', $query);
if (strlen(str_replace('%', '', $aquery)) < 2) {
$aquery = $query;
}
$condition = 'locationid IN (:locations)';
if (in_array(0, $locations)) {
$condition .= ' OR locationid IS NULL';
}
$result = Database::simpleQuery("SELECT machineuuid, macaddr, clientip, hostname, fixedlocationid, subnetlocationid
FROM machine
WHERE ($condition) AND machineuuid LIKE :aquery
OR macaddr LIKE :aquery
OR clientip LIKE :aquery
OR hostname LIKE :query
LIMIT 500", ['query' => "%$query%", 'aquery' => "%$aquery%", 'locations' => $locations]);
$returnObject = ['machines' => []];
foreach ($result as $row) {
if (!Location::isFixedLocationValid($roomLocationId, $row['subnetlocationid']))
continue;
if (empty($row['hostname'])) {
$row['hostname'] = $row['clientip'];
}
$returnObject['machines'][] = $row;
if (count($returnObject['machines']) > 100)
break;
}
echo json_encode($returnObject);
} elseif ($this->action === 'save') {
// Save roomplan - give feedback if it failed so the window can stay open
$this->loadRequestedLocation();
if ($this->locationid === null) {
die('Missing locationid in save data');
}
if ($this->location === null) {
die('Location with id ' . $this->locationid . ' does not exist.');
}
$this->handleSaveRequest(true);
die('SUCCESS');
} else {
echo 'Invalid AJAX action';
}
}
private function handleSaveRequest($isAjax)
{
User::assertPermission('edit', $this->locationid);
$leaf = (bool)Request::post('isleaf', 1, 'int');
if ($leaf !== $this->isLeaf) {
if ($isAjax) {
die('Leaf mode mismatch. Did you restructure locations while editing this room?');
}
Message::addError('leaf-mode-mismatch');
Util::redirect("?do=roomplanner&locationid={$this->locationid}&action=show");
}
if ($this->isLeaf) {
$this->saveLeafRoom($isAjax);
} else {
$this->saveComposedRoom($isAjax);
}
}
private function saveLeafRoom($isAjax)
{
$machinesOnPlan = $this->getMachinesOnPlan('invalid');
$config = Request::post('serializedRoom', null, 'string');
$config = json_decode($config, true);
if (!is_array($config) || !isset($config['furniture']) || !isset($config['computers'])) {
if ($isAjax) {
die('JSON data incomplete');
}
Message::addError('json-data-invalid');
Util::redirect("?do=roomplanner&locationid={$this->locationid}&action=show");
}
$tutorUuid = Request::post('tutoruuid', '', 'string');
if (empty($tutorUuid)) {
$tutorUuid = null;
} else {
$ret = Database::queryFirst('SELECT machineuuid FROM machine WHERE machineuuid = :uuid', ['uuid' => $tutorUuid]);
if ($ret === false) {
if ($isAjax) {
die('Invalid tutor UUID');
}
Message::addError('invalid-tutor-uuid');
Util::redirect("?do=roomplanner&locationid={$this->locationid}&action=show");
}
}
$this->saveRoomConfig($config['furniture'], $tutorUuid);
$this->saveComputerConfig($config['computers'], $machinesOnPlan);
}
private function saveComposedRoom($isAjax)
{
$room = new ComposedRoom(true);
$res = Database::exec('INSERT INTO location_roomplan (locationid, roomplan)
VALUES (:lid, :plan) ON DUPLICATE KEY UPDATE roomplan = VALUES(roomplan)',
['lid' => $this->locationid, 'plan' => $room->serialize()]);
if ($res === false) {
if ($isAjax) {
die('Error writing config to DB');
}
Message::addError('db-error');
Util::redirect("?do=roomplanner&locationid={$this->locationid}&action=show");
}
}
private function sanitizeNumber(&$number, $lower, $upper)
{
if (!is_numeric($number) || $number < $lower) {
$number = $lower;
} elseif ($number > $upper) {
$number = $upper;
}
}
/**
* @param array $computers Deserialized json from browser with all the computers
* @param array $oldComputers Deserialized old roomplan from database, used to find removed computers
*/
protected function saveComputerConfig(array $computers, array $oldComputers)
{
$oldUuids = [];
/* collect all uuids from the old roomplan */
foreach ($oldComputers['computers'] as $c) {
$oldUuids[] = $c['muuid'];
}
$newUuids = [];
foreach ($computers as $computer) {
$newUuids[] = $computer['muuid'];
// Fix/sanitize properties
// TODO: The list of items, computers, etc. in general is copied and pasted in multiple places. We need a central definition with generators for the various formats we need it in
if (!isset($computer['itemlook']) || !in_array($computer['itemlook'], ['pc-north', 'pc-south', 'pc-west', 'pc-east', 'copier', 'telephone'])) {
$computer['itemlook'] = 'pc-north';
}
if (!isset($computer['gridRow'])) {
$computer['gridRow'] = 0;
} else {
Util::clamp($computer['gridRow'], 0, 32 * 4);
}
if (!isset($computer['gridCol'])) {
$computer['gridCol'] = 0;
} else {
Util::clamp($computer['gridCol'], 0, 32 * 4);
}
$position = json_encode(['gridRow' => $computer['gridRow'],
'gridCol' => $computer['gridCol'],
'itemlook' => $computer['itemlook']]);
Database::exec('UPDATE machine SET position = :position, fixedlocationid = :locationid WHERE machineuuid = :muuid',
['locationid' => $this->locationid, 'muuid' => $computer['muuid'], 'position' => $position]);
}
// Get all computers that were removed from the roomplan and reset their data in DB
$toDelete = array_diff($oldUuids, $newUuids);
foreach ($toDelete as $d) {
Database::exec("UPDATE machine SET position = '', fixedlocationid = NULL WHERE machineuuid = :uuid", ['uuid' => $d]);
}
}
protected function saveRoomConfig(?array $furniture, ?string $tutorUuid)
{
$obj = json_encode(['furniture' => $furniture]);
$managerIp = Request::post('managerip', '', 'string');
Database::exec('INSERT INTO location_roomplan (locationid, roomplan, managerip, tutoruuid)'
. ' VALUES (:locationid, :roomplan, :managerip, :tutoruuid)'
. ' ON DUPLICATE KEY UPDATE '
. ' roomplan=VALUES(roomplan), managerip=VALUES(managerip), tutoruuid=VALUES(tutoruuid)', [
'locationid' => $this->locationid,
'roomplan' => $obj,
'managerip' => $managerIp,
'tutoruuid' => $tutorUuid,
]);
// See if the client is known, set run-mode
RunMode::deleteMode(Page::getModule(), (string)$this->locationid);
if (!empty($managerIp)) {
$pc = Statistics::getMachinesByIp($managerIp, Machine::NO_DATA, 'lastseen DESC');
if (!empty($pc)) {
$dedicated = (Request::post('dedimgr') === 'on');
$pc = array_shift($pc);
RunMode::setRunMode($pc->machineuuid, Page::getModule()->getIdentifier(), $this->locationid, json_encode([
'dedicatedmgr' => $dedicated
]), !$dedicated);
}
}
}
protected function getFurniture(array $config): array
{
if (empty($config['roomplan']))
return [];
$config = json_decode($config['roomplan'], true);
if (!is_array($config))
return [];
return $config;
}
/**
* @return array{computers: array}
*/
protected function getMachinesOnPlan(?string $tutorUuid): array
{
$result = Database::simpleQuery('SELECT machineuuid, macaddr, clientip, hostname, position
FROM machine
WHERE fixedlocationid = :locationid',
['locationid' => $this->locationid]);
$machines = [];
foreach ($result as $row) {
$machine = [];
$pos = json_decode($row['position'], true);
if ($pos === false || !isset($pos['gridRow']) || !isset($pos['gridCol'])) {
// Missing/incomplete position information - reset
Database::exec("UPDATE machine SET fixedlocationid = NULL, position = '' WHERE machineuuid = :uuid",
array('uuid' => $row['machineuuid']));
continue;
}
$machine['muuid'] = $row['machineuuid'];
$machine['ip'] = $row['clientip'];
$machine['mac_address'] = $row['macaddr'];
$machine['hostname'] = $row['hostname'];
$machine['gridRow'] = (int)$pos['gridRow'];
$machine['gridCol'] = (int)$pos['gridCol'];
$machine['itemlook'] = $pos['itemlook'];
$machine['data-width'] = 100;
$machine['data-height'] = 100;
if ($row['machineuuid'] === $tutorUuid) {
$machine['istutor'] = 'true';
}
$machines[] = $machine;
}
return ['computers' => $machines];
}
protected function getPotentialMachines(): array
{
$result = Database::simpleQuery('SELECT m.machineuuid, m.macaddr, m.clientip, m.hostname, l.locationname AS otherroom, m.fixedlocationid
FROM machine m
LEFT JOIN location l ON (m.fixedlocationid = l.locationid AND m.subnetlocationid <> m.fixedlocationid)
WHERE subnetlocationid = :locationid', ['locationid' => $this->locationid]);
$machines = [];
foreach ($result as $row) {
if (empty($row['hostname'])) {
$row['hostname'] = $row['clientip'];
}
$machines[] = $row;
}
return $machines;
}
}