<?php
class SubPage
{
public static function doPreprocess($action)
{
if ($action === 'updatelocation') {
self::updateLocation();
return true;
} else if ($action === 'updateOpeningtimes') {
self::updateOpeningTimes();
return true;
}
return false;
}
public static function doRender($action)
{
return false;
}
public static function doAjax($action)
{
if ($action === 'showlocation') {
self::ajaxShowLocation();
return true;
} elseif ($action === 'getOpeningtimes') {
$id = Request::any('locid', 0, 'int');
self::ajaxOpeningTimes($id);
return true;
}
return false;
}
private static function updateOpeningTimes()
{
$otInherited = Request::post('openingtimes-inherited', false, 'bool');
$openingTimes = Request::post('openingtimes', Request::REQUIRED, 'string');
$locationid = Request::post('locationid', Request::REQUIRED, 'int');
$wol = Request::post('wol', false, 'bool');
$woloffset = Request::post('wol-offset', 0, 'int');
$sd = Request::post('sd', false, 'bool');
$sdoffset = Request::post('sd-offset', 0, 'int');
User::assertPermission('location.edit.openingtimes', $locationid);
// Construct opening-times for database
if ($otInherited || $openingTimes === '') {
$openingTimes = null;
} else {
$openingTimes = json_decode($openingTimes, true);
if (!is_array($openingTimes)) {
$openingTimes = null;
} else {
$mangled = array();
foreach (array_keys($openingTimes) as $key) {
$entry = $openingTimes[$key];
if (!isset($entry['days']) || !is_array($entry['days']) || empty($entry['days'])) {
Message::addError('ignored-line-no-days');
continue;
}
$start = self::getTime($entry['openingtime']);
$end = self::getTime($entry['closingtime']);
if ($start === false) {
Message::addError('ignored-invalid-start', $entry['openingtime']);
continue;
}
if ($end === false) {
Message::addError('ignored-invalid-end', $entry['closingtime']);
continue;
}
if ($end <= $start) {
Message::addError('ignored-invalid-range', $entry['openingtime'], $entry['closingtime']);
continue;
}
unset($entry['tag']);
$mangled[] = $entry;
}
if (empty($mangled)) {
$openingTimes = null;
} else {
$openingTimes = json_encode($mangled);
}
}
}
// Check if opening-times changed
// $res = Database::queryFirst('SELECT openingtime FROM location WHERE locationid = :locationid', compact('locationid'));
// $otChanged = $res === false || $res['openingtime'] !== $openingTimes;
Database::exec('UPDATE location SET openingtime = :openingtime WHERE locationid = :locationid',
array('locationid' => $locationid, 'openingtime' => $openingTimes));
if (Module::isAvailable('rebootcontrol')) {
// Set options
Scheduler::setLocationOptions($locationid, $wol, $sd, $woloffset, $sdoffset);
}
}
private static function getTime($str)
{
$str = explode(':', $str);
if (count($str) !== 2)
return false;
if ($str[0] < 0 || $str[0] > 23 || $str[1] < 0 || $str[1] > 59)
return false;
return $str[0] * 60 + $str[1];
}
private static function updateLocation()
{
$locationId = Request::post('locationid', false, 'integer');
$del = Request::post('deletelocation', false, 'integer');
if ($locationId === false) {
Message::addError('parameter-missing', 'locationid');
Util::redirect('?do=Locations');
}
$location = Database::queryFirst('SELECT locationid, parentlocationid, locationname FROM location'
. ' WHERE locationid = :lid', array('lid' => $locationId));
if ($location === false) {
Message::addError('main.value-invalid', 'locationid', $locationId);
Util::redirect('?do=Locations');
}
$change = false;
// Delete location?
if ($locationId === $del) {
User::assertPermission("location.delete", $locationId, '?do=locations');
self::deleteLocation($location);
$change = true;
}
// Update subnets
$change |= self::updateLocationSubnets();
// Insert subnets
$change |= self::addNewLocationSubnets($location);
// Update location!
$change |= self::updateLocationData($location);
if ($change) {
// In case subnets or tree layout changed, recalc this
AutoLocation::rebuildAll();
}
Util::redirect('?do=Locations');
}
private static function deleteLocation($location)
{
$locationId = (int)$location['locationid'];
if (Request::post('recursive', false) === 'on') {
$rows = Location::queryLocations();
$rows = Location::buildTree($rows, $locationId);
$ids = Location::extractIds($rows);
} else {
$ids = [$locationId];
}
$locs = Database::exec("DELETE FROM location WHERE locationid IN (:ids)", ['ids' => $ids]);
Database::exec('UPDATE location SET parentlocationid = :newparent WHERE parentlocationid = :oldparent', array(
'newparent' => $location['parentlocationid'],
'oldparent' => $location['locationid']
));
AutoLocation::rebuildAll($ids);
Message::addSuccess('location-deleted', $locs, implode(', ', $ids));
Util::redirect('?do=Locations');
}
private static function updateLocationData($location)
{
$locationId = (int)$location['locationid'];
$newParent = Request::post('parentlocationid', false, 'integer');
$newName = Request::post('locationname', false, 'string');
if (!User::hasPermission('location.edit.name', $locationId)) {
$newName = $location['locationname'];
} elseif ($newName === false || preg_match('/^\s*$/', $newName)) {
if ($newName !== false) {
Message::addWarning('main.value-invalid', 'location name', $newName);
}
$newName = $location['locationname'];
}
if ($newParent === false || !User::hasPermission('location.edit.parent', $locationId)
|| !User::hasPermission('location.edit.parent', $newParent)
|| !User::hasPermission('location.edit.*', $location['parentlocationid'])) {
$newParent = $location['parentlocationid'];
} else if ($newParent !== 0) {
$rows = Location::queryLocations();
$all = Location::extractIds(Location::buildTree($rows));
if (!in_array($newParent, $all) || $newParent === $locationId) {
Message::addWarning('main.value-invalid', 'parent', $newParent);
$newParent = $location['parentlocationid'];
} else {
$rows = Location::extractIds(Location::buildTree($rows, $locationId));
if (in_array($newParent, $rows)) {
Message::addWarning('main.value-invalid', 'parent', $newParent);
$newParent = $location['parentlocationid'];
}
}
}
// TODO: Check permissions for new parent (only if changed)
$ret = Database::exec('UPDATE location SET parentlocationid = :parent, locationname = :name'
. ' WHERE locationid = :lid', array(
'lid' => $locationId,
'parent' => $newParent,
'name' => $newName
));
if ($ret > 0) {
Message::addSuccess('location-updated', $newName);
}
return $newParent != $location['parentlocationid'];
}
private static function updateLocationSubnets()
{
$locationId = Request::post('locationid', false, 'integer');
if (!User::hasPermission('location.edit.subnets', $locationId))
return false;
$change = false;
// Deletion first
$dels = Request::post('deletesubnet', false);
$deleteCount = 0;
if (is_array($dels)) {
$stmt = Database::prepare('DELETE FROM subnet WHERE subnetid = :id');
foreach ($dels as $subnetid => $value) {
if (!is_numeric($subnetid) || $value !== 'on')
continue;
if ($stmt->execute(array('id' => $subnetid))) {
$deleteCount += $stmt->rowCount();
}
}
}
// Now actual updates
$starts = Request::post('startaddr', false);
$ends = Request::post('endaddr', false);
if (!is_array($starts) || !is_array($ends)) {
return $change;
}
$editCount = 0;
$stmt = Database::prepare('UPDATE subnet SET startaddr = :start, endaddr = :end'
. ' WHERE subnetid = :id');
foreach ($starts as $subnetid => $start) {
if (!isset($ends[$subnetid]) || !is_numeric($subnetid))
continue;
$start = trim($start);
$end = trim($ends[$subnetid]);
if (empty($start) && empty($end)) {
$ret = Database::exec('DELETE FROM subnet WHERE subnetid = :id', ['id' => $subnetid]);
$deleteCount += $ret;
continue;
}
$range = LocationUtil::rangeToLongVerbose($start, $end);
if ($range === false)
continue;
list($startLong, $endLong) = $range;
if ($stmt->execute(array('id' => $subnetid, 'start' => $startLong, 'end' => $endLong))) {
$editCount += $stmt->rowCount();
}
}
if ($editCount > 0) {
Message::addSuccess('subnets-updated', $editCount);
$change = true;
}
if ($deleteCount > 0) {
Message::addSuccess('subnets-deleted', $deleteCount);
$change = true;
}
return $change;
}
private static function addNewLocationSubnets($location)
{
$locationId = (int)$location['locationid'];
if (!User::hasPermission('location.edit.subnets', $locationId))
return false;
$change = false;
$starts = Request::post('newstartaddr', false);
$ends = Request::post('newendaddr', false);
if (!is_array($starts) || !is_array($ends)) {
return $change;
}
$count = 0;
$stmt = Database::prepare('INSERT INTO subnet SET startaddr = :start, endaddr = :end, locationid = :location');
foreach ($starts as $key => $start) {
if (!isset($ends[$key]) || !is_numeric($key))
continue;
$end = $ends[$key];
list($startLong, $endLong) = LocationUtil::rangeToLong($start, $end);
if ($startLong === false) {
Message::addWarning('main.value-invalid', 'new start addr', $start);
}
if ($endLong === false) {
Message::addWarning('main.value-invalid', 'new end addr', $start);
}
if ($startLong === false || $endLong === false)
continue;
if ($startLong > $endLong) {
Message::addWarning('main.value-invalid', 'range', $start . ' - ' . $end);
continue;
}
if ($stmt->execute(array('location' => $locationId, 'start' => $startLong, 'end' => $endLong))) {
$count += $stmt->rowCount();
}
}
if ($count > 0) {
Message::addInfo('subnets-created', $count);
$change = true;
}
return $change;
}
private static function ajaxShowLocation()
{
$locationId = Request::any('locationid', 0, 'integer');
User::assertPermission("location.view", $locationId);
$loc = Database::queryFirst('SELECT locationid, parentlocationid, locationname FROM location WHERE locationid = :lid',
array('lid' => $locationId));
if ($loc === false) {
die('Unknown locationid');
}
$res = Database::simpleQuery("SELECT subnetid, startaddr, endaddr FROM subnet WHERE locationid = :lid",
array('lid' => $locationId));
$rows = array();
while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
$row['startaddr'] = long2ip($row['startaddr']);
$row['endaddr'] = long2ip($row['endaddr']);
$rows[] = $row;
}
$data = array(
'locationid' => $loc['locationid'],
'locationname' => $loc['locationname'],
'list' => $rows,
'roomplanner' => Module::get('roomplanner') !== false,
'parents' => Location::getLocations($loc['parentlocationid'], $locationId, true)
);
// Disable locations in the parent selector where the user cannot change to
if (!User::hasPermission('location.edit.*', $loc['parentlocationid'])
|| !User::hasPermission('location.edit.parent', $locationId)) {
$allowedLocs = [];
} else {
$allowedLocs = User::getAllowedLocations("location.edit.*");
foreach ($data['parents'] as &$parent) {
if (!(in_array($parent["locationid"], $allowedLocs) || $parent["locationid"] == $loc['parentlocationid'])) {
$parent["disabled"] = "disabled";
}
}
}
if (Module::get('dozmod') !== false) {
$lectures = Database::queryFirst('SELECT Count(*) AS cnt FROM sat.lecture l '
. ' INNER JOIN sat.lecture_x_location ll ON (l.lectureid = ll.lectureid AND ll.locationid = :lid)',
array('lid' => $locationId));
$data['lectures'] = $lectures['cnt'];
$data['haveDozmod'] = true;
}
// Get clients matching this location's subnet(s)
$count = $online = $used = 0;
if (Module::get('statistics') !== false) {
$mres = Database::simpleQuery("SELECT state FROM machine"
. " WHERE machine.locationid = :lid", array('lid' => $locationId));
while ($row = $mres->fetch(PDO::FETCH_ASSOC)) {
$count++;
if ($row['state'] === 'IDLE') {
$online++;
}
if ($row['state'] === 'OCCUPIED') {
$online++;
$used++;
}
}
$data['haveStatistics'] = true;
// Link
if (User::hasPermission('.statistics.view.list')) {
$data['statsLink'] = 'list';
} elseif (User::hasPermission('.statistics.view.summary')) {
$data['statsLink'] = 'summary';
}
}
$data['machines'] = $count;
$data['machines_online'] = $online;
$data['machines_used'] = $used;
$data['used_percent'] = $count === 0 ? 0 : round(($used / $count) * 100);
Permission::addGlobalTags($data['perms'], $locationId,
['location.edit.name', 'location.edit.subnets', 'location.delete', 'location.edit.openingtimes', '.roomplanner.edit'],
'save_button');
if (empty($allowedLocs)) {
$data['perms']['location']['edit']['parent']['disabled'] = 'disabled';
} else {
unset($data['perms']['save_button']);
}
if (Module::get('rebootcontrol') !== false) {
$res = Database::queryFirst("SELECT action, nextexecution FROM `reboot_scheduler`
WHERE locationid = :id", ['id' => $locationId]);
if ($res !== false) {
$data['next_action'] = $res['action'];
$data['next_time'] = Util::prettyTime($res['nextexecution']);
}
}
echo Render::parse('location-subnets', $data);
}
private static function ajaxOpeningTimes($id)
{
User::assertPermission('location.edit.openingtimes', $id);
$data = ['id' => $id];
$openTimes = Database::queryFirst("SELECT openingtime FROM `location`
WHERE locationid = :id", array('id' => $id));
if ($openTimes === false) {
Message::addError('invalid-location-id', $id);
return;
}
if ($openTimes['openingtime'] !== null) {
$openingTimes = json_decode($openTimes['openingtime'], true);
} else {
$openingTimes = OpeningTimes::forLocation($id);
$data['openingtimes_inherited'] = 'checked';
}
if (!isset($openingTimes) || !is_array($openingTimes)) {
$openingTimes = array();
}
$data['expertMode'] = !self::isSimpleMode($openingTimes);
$data['schedule_data'] = json_encode($openingTimes);
$rebootcontrol = Module::isAvailable('rebootcontrol');
$data['rebootcontrol'] = $rebootcontrol;
if ($rebootcontrol) {
$data['scheduler-options'] = Scheduler::getLocationOptions($id);
}
echo Render::parse('ajax-opening-location', $data);
}
private static function isSimpleMode(&$array)
{
if (empty($array))
return true;
// Decompose by day
$new = array();
foreach ($array as $row) {
$s = self::getTime($row['openingtime']);
$e = self::getTime($row['closingtime']);
if ($s === false || $e === false || $e <= $s)
continue;
foreach ($row['days'] as $day) {
self::addDay($new, $day, $s, $e);
}
}
// Merge by timespan, but always keep saturday and sunday separate
$merged = array();
foreach ($new as $day => $ranges) {
foreach ($ranges as $range) {
if ($day === 'Saturday' || $day === 'Sunday') {
$add = $day;
} else {
$add = '';
}
$key = '#' . $range[0] . '#' . $range[1] . '#' . $add;
if (!isset($merged[$key])) {
$merged[$key] = array();
}
$merged[$key][$day] = true;
}
}
// Check if it passes as simple mode
if (count($merged) > 3)
return false;
foreach ($merged as $days) {
if (count($days) === 5) {
$res = array_keys($days);
$res = array_intersect($res, array("Monday", "Tuesday", "Wednesday", "Thursday", "Friday"));
if (count($res) !== 5)
return false;
} elseif (count($days) === 1) {
if (!isset($days['Saturday']) && !isset($days['Sunday'])) {
return false;
}
} else {
return false;
}
}
// Valid simple mode, finally transform back to what we know
$new = array();
foreach ($merged as $span => $days) {
preg_match('/^#(\d+)#(\d+)#/', $span, $out);
$new[] = array(
'days' => array_keys($days),
'openingtime' => floor($out[1] / 60) . ':' . ($out[1] % 60),
'closingtime' => floor($out[2] / 60) . ':' . ($out[2] % 60),
);
}
$array = $new;
return true;
}
private static function addDay(&$array, $day, $s, $e)
{
if (!isset($array[$day])) {
$array[$day] = array(array($s, $e));
return;
}
foreach (array_keys($array[$day]) as $key) {
$current = $array[$day][$key];
if ($s <= $current[0] && $e >= $current[1]) {
// Fully dominated
unset($array[$day][$key]);
continue; // Might partially overlap with additional ranges, keep going
}
if ($current[0] <= $s && $current[1] >= $s) {
// $start lies within existing range
if ($current[0] <= $e && $current[1] >= $e)
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
$s = $current[0];
unset($array[$day][$key]);
continue;
}
// Last possibility: $start is before range, $end within range
if ($current[0] <= $e && $current[1] >= $e) {
// $start must lie before range start, otherwise we'd have hit the case above
$e = $current[1];
unset($array[$day][$key]);
continue;
}
}
$array[$day][] = array($s, $e);
}
}