summaryrefslogblamecommitdiffstats
path: root/modules-available/locations/pages/details.inc.php
blob: 2f444157378c12418bb64304076b02940a683632 (plain) (tree)
1
2
3
4
5
6
7
8
9
10









                                                    


                                                              













                                                 



                                                              



                             



                                                                                           



                                                                   
 
                                                                                                     












































                                                                                                                                         





















                                                                                                       









                                                                                             












































































































                                                                                                                              
                                 
                                      
                                                                                             

                                                                              
                                                 

                                                                               

                                 







                                                             
                               

                                                                                                







                                                                                                                      
                                         
                         



                                                                                

                                                                                                                 

                         





                                                                             




































































































































                                                                                                                                                                               












                                                                                                                                   


                                                                      
                                                                                                                                                            



                                                                                    
                                                                                                                                                          





                                                                                  































































































                                                                                                                                             
 
<?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()
	{
		$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.*', $locationid); // TODO: Introduce permission

		// Construct opening-times for database
		if ($openingTimes !== '') {
			$openingTimes = json_decode($openingTimes, true);
			if (!is_array($openingTimes)) {
				$openingTimes = '';
			} 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')) {
			if ($wol) {
				$options = array();
				// Sanity checks
				if ($woloffset > 15) $woloffset = 15;
				else if ($woloffset < 0) $woloffset = 0;
				$options['wol-offset'] = $woloffset;
				Scheduler::updateSchedule($locationid, 'wol', $options, $openingTimes);
			} else {
				Scheduler::deleteSchedule($locationid, 'wol');
			}
			if ($sd) {
				$options = array();
				// Sanity checks
				if ($sdoffset > 15) $sdoffset = 15;
				else if ($sdoffset < 0) $sdoffset = 0;
				$options['sd-offset'] = $sdoffset;
				Scheduler::updateSchedule($locationid, 'sd', $options, $openingTimes);
			} else {
				Scheduler::deleteSchedule($locationid, 'sd');
			}
		}
	}

	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', '.roomplanner.edit'], 'save_button');
		if (empty($allowedLocs)) {
			$data['perms']['location']['edit']['parent']['disabled'] = 'disabled';
		} else {
			unset($data['perms']['save_button']);
		}

		echo Render::parse('location-subnets', $data);
	}

	private static function ajaxOpeningTimes($id) {
		User::assertPermission('location.edit', $id);
		$openTimes = Database::queryFirst("SELECT openingtime FROM `location` WHERE locationid = :id", array('id' => $id));
		if ($openTimes !== false) {
			$openingTimes = json_decode($openTimes['openingtime'], true);
		}
		if (!isset($openingTimes) || !is_array($openingTimes)) {
			$openingTimes = array();
		}
		$data = array('id' => $id);
		$data['expertMode'] = !self::isSimpleMode($openingTimes);
		$data['schedule_data'] = json_encode($openingTimes);

		$rebootcontrol = Module::isAvailable('rebootcontrol');
		$data['rebootcontrol'] = $rebootcontrol;
		if ($rebootcontrol) {
			$wol = Database::queryFirst("SELECT options FROM `reboot_scheduler` WHERE locationid = :id AND action = 'wol'", array('id' => $id));
			if ($wol !== false) {
				$data['wol'] = true;
				$data['wol-options'] = json_decode($wol['options']);
			}
			$sd = Database::queryFirst("SELECT options FROM `reboot_scheduler` WHERE locationid = :id AND action = 'sd'", array('id' => $id));
			if ($sd !== false) {
				$data['sd'] = true;
				$data['sd-options'] = json_decode($sd['options']);
			}
		}

		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);
	}
}