summaryrefslogblamecommitdiffstats
path: root/modules-available/locationinfo/page.inc.php
blob: 1a58131ffbce9a38246b8c22036476215373bcc1 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11



                                    
                      
 




                                                                                         

                                                                    


                                           
                             



                                                                      

                                                     
                                                  
                                         
                                                              

                                                     
                                                      
                                             
                                         
                                                               
                                                      
                                           









                                                                                                  
                 
                                                                 
                                            







                                                                      
                                                                     
                         
                                                                         
                 






                                                                                        
                                                                                  
                                                               

                                                          




                                                                     
                 


                                                                                                           


                                                    
                                
                                                                  
                              
                                  
                                                 
                              
                              
                                                 
                              


                                                
                        
                                                           
                 

         
           

                                          
                                                
         
                                                       
                                
                                                               

                               





                                                                                                                         
                                            


                                                             
                                                                            

                               
                                                                



                                                                                                                    


                                                                    

         
                                                          
         
                                          



                                                                               

                                              
 
                                                    



                                                                        
                                                                                  
                               

                                                           
                                                                                       
                               
                 

                                                                     

                                                                
                                         
                 
                                                                                    
 

                                                                                           






                                                    
                              
 

                                                                                                                                                  
                                                                                                                                                                         
                                                                                                        
                                            
                                                            
                                                
                                                                
                                                         
                                      
                   
 








                                                                                  
                                                                                                                                                  


                                                                         


                                                      
                 


           
                                                                                                    


                                                                                  

                                                                                                                 
           
                                                           
         
                                                                                               
                                                          

                                                                                                                         
                                          
                                                                                 

                                                           





                                        
                                                 













                                                                                                  

                                                                     




                                                                            
                             
                                                                                                     
 




















                                                                                                                                              


                                                           
                                                           

                                  
                                                                 


                                                                       
                                         
                              



                                                                                
                                                                                   
                                                                                    
                                                                          






                                                                                         
                                                                               
                  

                                                 
                 


                                                








                                                                                                                                 
                                                                                                                      


                                                                                                                                     
                                                                                                                 
                                                                                                                           





                                                                              

                                                                               
 


                                                           
                                                       
         



                                                                             
                                                                                          






                                                                            


                                                                                              
                                                                                             

                                                                                                                  
                                                                                 
                                                                                   
                                                                                   
                                                                             
                                                             
                                                                                
                                                                                  

                                                                     

         


                                                           
                                                           
         



                                                                                
                                                                                     




                                                                                
                                  
                                                                 
                                                                               

         


                                                 
                                                     
         
                                                       

                                                                         
                                                                  
                                                                   

                                         
                                                                               


                                                           
                                                                     

                                           
                                                  
                                                                                                     
                 
                                
                                              



                                                                      
                                                                                                                      
                                                                                       
                                                                         
                        
                                                  
                                                                           
                                                                                                              

                                                                        
                 

         
           



                                                                       
                                                                 
         
                                      
                                                                                             
                 
                                                        
 
                                                                                
                                                                 
                                                                                             
 

                                                                                      
                                                                                                                    

                               

                                                                            
 
                                     
                                                           
                 
 
                                                                                      

         
                                              
         
                                                       
                                       

                                                   
                                                                       
                                                                           
                 
                                                 
                                      
                                                                                                                        
                                             





                                                                                    

                                                    

                                                                          
                                                                                       

                                                      
                                 

                                                                
                         
                                             
                 

                                   
 


                                    
                                                                   
         

                                                    
                                                    



                                                                                                   

         
                                               














                                                                                                    
                                        







                                                                                                                 
                                                   
         




                                                                               


                                                                       
                                                                                                                                                                              
                                                                

                                                                                     
 
                                            
                                                         
                                                                                               
                                         




                                                                                                     
                                                    
                                                         
                                                            
                                                      
                                                                                                             
                                                                                               


                          


                                                    
                                                                                                    




                                                              
                                                                                                                                                   
                                         





                                                                               




                                                            
                                                
         
                                                                            


                                                     
                                                                         


                                                  
                                                                                    


                                                    



                                                                
                                                                                                            

                                                             



                                                                                   

                                                           
                                        


                                                                                     

                                                                                                     

                                                                          
                                              
                                                                                                                 

                                                 



                                                                                                                                   
                                            
                                                                                   
                                                                                                                            


                                                                         
                                                            

                                                                                 
                         


                                                                                                   

                                         
                                                                                    

         
           

               






                                                 
                                                   

                                                       
                                                         
                                                       


                 


                                    
                                  
           
                                                          
         
                                                       
                                                                                             
                                                                                                             
 
                                     




                                                                                       




                                                       
                                                                          






                                                                                                      
                                                                
                                                                                                    
                                                                                                                                                  
                                        
                                                               
                                 
                                                                                                           

                                                                                                                            
                         
                                                     
                 
                                                                           

                                                                     

                                                                 

         


                              
                                            
           
                                                          
         
                                                             



                                                                                                               

                                                                                     

                                                                                         
                 
                                                                        

                                                
 







                                                                                                                                  
                                                        









                                                                                                                          


                                                                                                                                    
                                        

                                                                         
                         
                                             
                 




                                                                             
                                                                              
                  

                                                                  

         
           


                                                                 
                                                 
          
                                                   
           
                                                           
         
                                  
                                  
                                   

                                                                                                                      
                                          


                                                                             

                                                        


                                                            


                                                                  

                                    

                                                     


                                                                     
                                 
                                                             
                         
                 














                                                                                                      
                                                                       
           
                                                                  















                                                                                                                      
                                
                                                                                  

                                                                                                                         
                                                                                         


                                                      

                         
                                              




























                                                                                                                                             
                                           


                                               
         
 
           
                                      
           
                                                
         



                                                                            
                 
















                                                         






                                                     








                                                                                                            
 
                                                                           
                                                             
                                                              
                         

                 


                                                                   





                                                                             
 
                                                        




                                                                          
                         
                 
 








                                                                                           
                                                                                               
                                                                        
                                                                  






                                                                                                     

                                                                        
                                                                                 
                                                                                           
                           
                                                          














                                                                                      
                                                                
 




                                                                           
                                                                        
                                                                                          
                                                                                  

                                                                                               


                                                                                                 
                                                                                          
                                                               
                                                                                              
                           
                        




                                                                               
                                                                        
                                                                        

                                                                        
                                                                                 

                           
         
 
                                          






                                                              
                                     







                                                               
                                

                                                                              
                                                 




                                                                  

                 






                                                                  
                                                                                                 










                                                                                   
                                                                                                 



                                                   

         

                                                                                                      
                                            
           
                                                                                                                         









                                                                                                            
                                       


                                                                  

                                          
                                       




                                                                                 









                                                                                           
 
<?php

class Page_LocationInfo extends Page
{
	private $show;

	/**
	 * Called before any page rendering happens - early hook to check parameters etc.
	 */
	protected function doPreprocess()
	{
		$this->show = Request::any('show', false, 'string');
		if ($this->show === 'panel') {
			$this->showPanel();
			exit(0);
		}
		User::load();
		if (!User::isLoggedIn()) {
			Message::addError('main.no-permission');
			Util::redirect('?do=Main'); // does not return
		}
		$action = Request::post('action');
		if ($action === 'writePanelConfig') {
			$this->writePanelConfig();
			$show = 'panels';
		} elseif ($action === 'writeLocationConfig') {
			$this->writeLocationConfig();
			$show = 'locations';
		} elseif ($action === 'deletePanel') {
			$this->deletePanel();
			$show = 'panels';
		} elseif ($action === 'updateServerSettings') {
			$this->updateServerSettings();
			$show = 'backends';
		} else {
			if (($id = Request::post('del-serverid', false, 'int')) !== false) {
				$this->deleteServer($id);
				$show = 'backends';
			} elseif (($id = Request::post('chk-serverid', false, 'int')) !== false) {
				$this->checkConnection($id);
				$show = 'backends';
			} elseif (Request::isPost()) {
				Message::addWarning('main.invalid-action', $action);
			}
		}
		if (Request::isPost() || $this->show === false) {
			if (!empty($show)) {
				//
			} elseif (User::hasPermission('panel.list')) {
				$show = 'panels';
			} elseif (User::hasPermission('location.*')) {
				$show = 'locations';
			} elseif (User::hasPermission('backend.*')) {
				$show = 'backends';
			} else {
				User::assertPermission('panel.list');
			}
			Util::redirect('?do=locationinfo&show=' . $show);
		}
	}

	/**
	 * Menu etc. has already been generated, now it's time to generate page content.
	 */
	protected function doRender()
	{
		$data = array('class-' . $this->show => 'active', 'errors' => []);
		// Do this here so we always see backend errors
		if (User::hasPermission('backend.*')) {
			$backends = $this->loadBackends();
			foreach ($backends as $backend) {
				if (!empty($backend['error'])) {
					$data['errors'][] = $backend;
				}
			}
		}
		Permission::addGlobalTags($data['perms'], null, ['backend.*', 'location.*', 'panel.list']);
		Render::addTemplate('page-tabs', $data);
		switch ($this->show) {
		case 'locations':
			$this->showLocationsTable();
			break;
		case 'backends':
			$this->showBackendsTable($backends ?? []);
			break;
		case 'edit-panel':
			$this->showPanelConfig();
			break;
		case 'panels':
			$this->showPanelsTable();
			break;
		case 'backendlog':
			$this->showBackendLog();
			break;
		default:
			Util::redirect('?do=locationinfo');
		}
	}

	/**
	 * Deletes the server from the db.
	 */
	private function deleteServer($id): void
	{
		User::assertPermission('backend.edit');
		if ($id === 0) {
			Message::addError('server-id-missing');
			return;
		}
		$res = Database::exec("DELETE FROM `locationinfo_coursebackend` WHERE serverid=:id", array('id' => $id));
		if ($res !== 1) {
			Message::addWarning('invalid-server-id', $id);
		}
	}

	private function deletePanel(): void
	{
		$id = Request::post('uuid', false, 'string');
		if ($id === false) {
			Message::addError('main.parameter-missing', 'uuid');
			return;
		}
		$this->assertPanelPermission($id, 'panel.edit');
		$res = Database::exec("DELETE FROM `locationinfo_panel` WHERE paneluuid = :id", array('id' => $id));
		if ($res !== 1) {
			Message::addWarning('invalid-panel-id', $id);
		}
		if (Module::isAvailable('runmode')) {
			RunMode::deleteMode(Page::getModule(), $id);
		}
	}

	private static function getTime(string $str): ?int
	{
		$str = explode(':', $str);
		if (count($str) !== 2)
			return null;
		if ($str[0] < 0 || $str[0] > 23 || $str[1] < 0 || $str[1] > 59)
			return null;
		return $str[0] * 60 + $str[1];
	}

	private function writeLocationConfig(): void
	{
		// Check locations
		$locationid = Request::post('locationid', false, 'int');
		if ($locationid === false) {
			Message::addError('main.parameter-missing', 'locationid');
			return;
		}
		if (Location::get($locationid) === false) {
			Message::addError('location.invalid-location-id', $locationid);
			return;
		}
		User::assertPermission('location.edit', $locationid);

		$serverid = Request::post('serverid', 0, 'int');
		if ($serverid === 0) {
			$serverid = null;
		}
		$serverlocationid = Request::post('serverlocationid', '', 'string');

		$changeServerRecursive = (Request::post('recursive', '', 'string') !== '');
		if (empty($serverlocationid) && !$changeServerRecursive) {
			$insertServerId = null;
			$ignoreServer = 1;
		} else {
			$insertServerId = $serverid;
			$ignoreServer = 0;
		}

		$NOW = time();

		Database::exec("INSERT INTO `locationinfo_locationconfig` (locationid, serverid, serverlocationid, lastcalendarupdate, lastchange)
				VALUES (:id, :insertserverid, :serverlocationid, 0, :now)
				ON DUPLICATE KEY UPDATE serverid = IF(:ignore_server AND serverid IS NULL, NULL, :serverid), serverlocationid = VALUES(serverlocationid),
					lastcalendarupdate = 0, lastchange = VALUES(lastchange)", array(
			'id' => $locationid,
			'insertserverid' => $insertServerId,
			'serverid' => $serverid,
			'serverlocationid' => $serverlocationid,
			'ignore_server' => $ignoreServer,
			'now' => $NOW,
		));

		if ($changeServerRecursive) {
			// Recursive overwriting of serverid
			$children = Location::getRecursiveFlat($locationid);
			$array = array();
			foreach ($children as $loc) {
				$array[] = $loc['locationid'];
			}
			if (!empty($array)) {
				Database::exec("UPDATE locationinfo_locationconfig
				SET serverid = :serverid, lastcalendarupdate = IF(serverid <> :serverid, 0, lastcalendarupdate), lastchange = :now
				WHERE locationid IN (:locations)", array(
					'serverid' => $serverid,
					'locations' => $array,
					'now' => $NOW,
				));
			}
		}
	}

	/**
	 * Get all location ids from the locationids parameter, which is comma separated, then split
	 * and remove any ids that don't exist. The cleaned list will be returned.
	 * Will show error and redirect to main page if parameter is missing
	 *
	 * @param bool $failIfEmpty Show error and redirect to main page if parameter is missing or list is empty
	 * @return array list of locations from parameter
	 */
	private function getLocationIdsFromRequest(): array
	{
		$locationids = Request::post('locationids', Request::REQUIRED_EMPTY, 'string');
		$locationids = explode(',', $locationids);
		$all = array_map(function ($item) { return $item['locationid']; }, Location::queryLocations());
		$locationids = array_filter($locationids, function ($item) use ($all) { return in_array($item, $all); });
		if (empty($locationids)) {
			Message::addError('main.parameter-empty', 'locationids');
			Util::redirect('?do=locationinfo');
		}
		return $locationids;
	}

	/**
	 * Updated the config in the db.
	 */
	private function writePanelConfig(): void
	{
		// UUID - existing or new
		$paneluuid = Request::post('uuid', false, 'string');
		if (($paneluuid === false || strlen($paneluuid) !== 36) && $paneluuid !== 'new') {
			Message::addError('invalid-panel-id', $paneluuid);
			Util::redirect('?do=locationinfo');
		}
		// Check panel type
		$paneltype = Request::post('ptype', false, 'string');

		if ($paneltype === 'DEFAULT') {
			$params = $this->preparePanelConfigDefault();
		} elseif ($paneltype === 'URL') {
			$params = $this->preparePanelConfigUrl();
		}  elseif ($paneltype === 'SUMMARY') {
			$params = $this->preparePanelConfigSummary();
		} else {
			Message::addError('invalid-panel-type', $paneltype);
			Util::redirect('?do=locationinfo');
		}

		// Permission
		$this->assertPanelPermission($paneluuid, 'panel.edit', $params['locationids'] ?? []);

		if ($paneluuid === 'new') {
			$paneluuid = Util::randomUuid();
			$query = "INSERT INTO `locationinfo_panel` (paneluuid, panelname, locationids, paneltype, panelconfig, lastchange)
				VALUES (:id, :name, :locationids, :type, :config, :now)";
		} else {
			$query = "UPDATE `locationinfo_panel`
				SET panelname = :name, locationids = :locationids, paneltype = :type, panelconfig = :config, lastchange = :now
				WHERE paneluuid = :id";
		}
		$params['id'] = $paneluuid;
		$params['name'] = Request::post('name', '-', 'string');
		$params['type'] = $paneltype;
		$params['now'] = time();
		$params['config'] = json_encode($params['config']);
		$params['locationids'] = implode(',', $params['locationids']);
		Database::exec($query, $params);

		Message::addSuccess('config-saved');
		Util::redirect('?do=locationinfo');
	}

	/**
	 * @return array{config: array, locationids: array}
	 */
	private function preparePanelConfigDefault(): array
	{
		// Check locations
		$locationids = self::getLocationIdsFromRequest();
		if (count($locationids) > 4) {
			$locationids = array_slice($locationids, 0, 4);
		}
		// Build struct from POST
		$conf = array(
			'language' => Request::post('language', 'en', 'string'),
			'mode' => Request::post('mode', 1, 'int'),
			'vertical' => Request::post('vertical', false, 'bool'),
			'eco' => Request::post('eco', false, 'bool'),
			'prettytime' => Request::post('prettytime', false, 'bool'),
			'roomplanner' => Request::post('roomplanner', true, 'bool'),
			'startday' => Request::post('startday', 0, 'int'),
			'scaledaysauto' => Request::post('scaledaysauto', false, 'bool'),
			'daystoshow' => Request::post('daystoshow', 7, 'int'),
			'rotation' => Request::post('rotation', 0, 'int'),
			'scale' => Request::post('scale', 50, 'int'),
			'switchtime' => Request::post('switchtime', 20, 'int'),
			'calupdate' => Request::post('calupdate', 120, 'int'),
			'roomupdate' => Request::post('roomupdate', 30, 'int'),
			'hostname' => Request::post('hostname', false, 'bool'),
		);
		if ($conf['roomupdate'] < 15) {
			$conf['roomupdate'] = 15;
		}
		if ($conf['calupdate'] < 30) {
			$conf['calupdate'] = 30;
		}

		$overrides = array();
		for ($i = 0; $i < sizeof($locationids); $i++) {
			$overrideLoc = Request::post('override'.$locationids[$i], false, 'bool');
			if ($overrideLoc) {
				$overrideArray = array(
					'mode' => Request::post('override'.$locationids[$i].'mode', 1, 'int'),
					'roomplanner' => Request::post('override'.$locationids[$i].'roomplanner', false, 'bool'),
					'vertical' => Request::post('override'.$locationids[$i].'vertical', false, 'bool'),
					'startday' => Request::post('override'.$locationids[$i].'startday', 0, 'int'),
					'scaledaysauto' => Request::post('override'.$locationids[$i].'scaledaysauto', false, 'bool'),
					'daystoshow' => Request::post('override'.$locationids[$i].'daystoshow', 7, 'int'),
					'rotation' => Request::post('override'.$locationids[$i].'rotation', 0, 'int'),
					'scale' => Request::post('override'.$locationids[$i].'scale', 50, 'int'),
					'switchtime' => Request::post('override'.$locationids[$i].'switchtime', 60, 'int'),
				);
				$overrides[$locationids[$i]] = $overrideArray;
			}
		}
		$conf['overrides'] = $overrides;

		return array('config' => $conf, 'locationids' => $locationids);
	}

	/**
	 * @return array{config: array, locationids: array}
	 */
	private function preparePanelConfigUrl(): array
	{
		$bookmarkNames = Request::post('bookmarkNames', [], 'array');
		$bookmarkUrls = Request::post('bookmarkUrls', [], 'array');
		$bookmarkString = '';
		for ($i = 0; $i < count($bookmarkNames); $i++) {
			if ($bookmarkNames[$i] == '' || $bookmarkUrls[$i] == '') continue;
			$bookmarkString .= rawurlencode($bookmarkNames[$i]);
			$bookmarkString .= ",";
			$bookmarkString .= rawurlencode($bookmarkUrls[$i]);
			$bookmarkString .= " ";
		}
		$bookmarkString = substr($bookmarkString, 0, -1);

		$conf = array(
			'url' => Request::post('url', 'https://www.bwlehrpool.de/', 'string'),
			'insecure-ssl' => Request::post('insecure-ssl', 0, 'int'),
			'reload-minutes' => max(0, Request::post('reloadminutes', 0, 'int')),
			'whitelist' => preg_replace("/[\r\n]+/m", "\n", Request::post('whitelist', '', 'string')),
			'blacklist' => preg_replace("/[\r\n]+/m", "\n", Request::post('blacklist', '', 'string')),
			'split-login' => Request::post('split-login', 0, 'bool'),
			'browser' => Request::post('browser', 'firefox', 'string'),
			'interactive' => Request::post('interactive', '0', 'bool'),
			'hw-video' => Request::post('hw-video', '0', 'bool'),
			'bookmarks' => $bookmarkString ?: '',
			'allow-tty' => Request::post('allow-tty', '', 'string'),
			'zoom-factor' => Request::post('zoom-factor', 100, 'int'),
		);
		return array('config' => $conf, 'locationids' => []);
	}

	/**
	 * @return array{config: array, locationids: array}
	 */
	private function preparePanelConfigSummary(): array
	{
		// Build json structure
		$conf = array(
			'language' => Request::post('language', 'en', 'string'),
			'eco' => Request::post('eco', false, 'bool'),
			'roomplanner' => Request::post('roomplanner', false, 'bool'),
			'panelupdate' => Request::post('panelupdate', 30, 'int')
		);
		if ($conf['panelupdate'] < 15) {
			$conf['panelupdate'] = 15;
		}
		// Check locations
		$locationids = self::getLocationIdsFromRequest();
		return array('config' => $conf, 'locationids' => $locationids);
	}

	/**
	 * Updates the server settings in the db.
	 */
	private function updateServerSettings(): void
	{
		User::assertPermission('backend.edit');
		$serverid = Request::post('id', -1, 'int');
		$servername = Request::post('name', 'unnamed', 'string');
		$servertype = Request::post('type', '', 'string');
		$backend = CourseBackend::getInstance($servertype);

		if ($backend === false) {
			Message::addError('invalid-backend-type', $servertype);
			Util::redirect('?do=locationinfo');
		}

		$tmptypeArray = $backend->getCredentialDefinitions();

		$credentialsJson = array();
		foreach ($tmptypeArray as $cred) {
			$credentialsJson[$cred->property] = Request::post('prop-' . $cred->property);
		}
		$params = array(
			'name' => $servername,
			'type' => $servertype,
			'credentials' => json_encode($credentialsJson)
		);
		if ($serverid === 0) {
			Database::exec('INSERT INTO `locationinfo_coursebackend` (servername, servertype, credentials)
					VALUES (:name, :type, :credentials)', $params);
			$this->checkConnection(Database::lastInsertId());
		} else {
			$params['id'] = $serverid;
			Database::exec('UPDATE `locationinfo_coursebackend`
					SET servername = :name, servertype = :type, credentials = :credentials
					WHERE serverid = :id', $params);
			$this->checkConnection($serverid);
		}
	}

	/**
	 * Checks if the server connection to a backend is valid.
	 *
	 * @param int $id Server id which connection should be checked.
	 */
	private function checkConnection(int $serverid = 0): void
	{
		if ($serverid === 0) {
			ErrorHandler::traceError('checkConnection called with no server id');
		}
		User::assertPermission('backend.check');

		$dbresult = Database::queryFirst("SELECT servertype, credentials
				FROM `locationinfo_coursebackend`
				WHERE serverid = :serverid", array('serverid' => $serverid));

		$serverInstance = CourseBackend::getInstance($dbresult['servertype']);
		if ($serverInstance === false) {
			LocationInfo::setServerError($serverid, 'Unknown backend type: ' . $dbresult['servertype']);
			return;
		}
		$credentialsOk = $serverInstance->setCredentials($serverid,
			(array)json_decode($dbresult['credentials'], true));

		if ($credentialsOk) {
			$serverInstance->checkConnection();
		}

		LocationInfo::setServerError($serverid, $serverInstance->getErrors());
	}

	private function loadBackends(): array
	{
		// Get a list of all the backend types.
		$servertypes = array();
		$s_list = CourseBackend::getList();
		foreach ($s_list as $s) {
			$typeInstance = CourseBackend::getInstance($s);
			$servertypes[$s] = $typeInstance->getDisplayName();
		}
		// Build list of defined backends
		$serverlist = array();
		$dbquery2 = Database::simpleQuery("SELECT * FROM `locationinfo_coursebackend` ORDER BY servername ASC");
		foreach ($dbquery2 as $row) {
			if (isset($servertypes[$row['servertype']])) {
				$row['typename'] = $servertypes[$row['servertype']];
			} else {
				$row['typename'] = '[' . $row['servertype'] . ']';
				$row['disabled'] = 'disabled';
			}

			if (!empty($row['error'])) {
				$error = json_decode($row['error'], true);
				if (isset($error['timestamp'])) {
					$time = date('Y-m-d H:i', $error['timestamp']);
				} else {
					$time = '???';
				}
				$row['error'] = $error['error'];
				$row['errtime'] = $time;
			}
			$serverlist[] = $row;
		}
		return $serverlist;
	}

	/**
	 * Show the list of backends
	 */
	private function showBackendsTable(array $serverlist): void
	{
		User::assertPermission('backend.*');
		$data = array(
			'serverlist' => $serverlist,
		);
		Permission::addGlobalTags($data['perms'], null, ['backend.edit', 'backend.check']);
		// Pass the data to the html and render it.
		Render::addTemplate('page-servers', $data);
	}

	private function showBackendLog(): void
	{
		$id = Request::get('serverid', false, 'int');
		if ($id === false) {
			Message::addError('main.parameter-missing', 'serverid');
			Util::redirect('?do=locationinfo');
		}
		$server = Database::queryFirst('SELECT servername FROM locationinfo_coursebackend
				WHERE serverid = :id', ['id' => $id]);
		if ($server === false) {
			Message::addError('invalid-server-id', $id);
			Util::redirect('?do=locationinfo');
		}
		$server['list'] = [];
		$res = Database::simpleQuery('SELECT dateline, message FROM locationinfo_backendlog
				WHERE serverid = :id ORDER BY logid DESC LIMIT 100', ['id' => $id]);
		foreach ($res as $row) {
			$row['dateline_s'] = Util::prettyTime($row['dateline']);
			$row['class'] = substr($row['message'], 0, 3) === '[F]' ? 'text-danger' : 'text-warning';
			$row['message'] = Substr($row['message'], 3);
			$server['list'][] = $row;
		}
		Render::addTemplate('page-server-log', $server);
	}

	private function showLocationsTable(): void
	{
		$allowedLocations = User::getAllowedLocations('location.edit');
		if (empty($allowedLocations)) {
			Message::addError('main.no-permission');
			return;
		}
		$locations = Location::getLocations(0, 0, false, true);

		// Get hidden state of all locations
		$dbquery = Database::simpleQuery("SELECT li.locationid, li.serverid, li.serverlocationid, loc.openingtime, li.lastcalendarupdate, cb.servertype, cb.servername
			FROM `locationinfo_locationconfig` AS li
			LEFT JOIN `locationinfo_coursebackend` AS cb USING (serverid)
			LEFT JOIN `location` AS loc USING (locationid)");

		foreach ($dbquery as $row) {
			$locid = (int)$row['locationid'];
			if (!isset($locations[$locid]) || !in_array($locid, $allowedLocations))
				continue;
			$glyph = !empty($row['openingtime']) ? 'ok' : '';
			$backend = '';
			if (!empty($row['serverid']) && !empty($row['serverlocationid'])) {
				$backend = $row['servername'] . '(' . $row['serverlocationid'] . ')';
			}
			$locations[$locid] += array(
				'openingGlyph' => $glyph,
				'strong' => $glyph === 'ok',
				'backend' => $backend,
				'lastCalendarUpdate' => Util::prettyTime($row['lastcalendarupdate']), // TODO
				'backendMissing' => !CourseBackend::exists($row['servertype']),
			);
		}

		$stack = array();
		$depth = -1;
		foreach ($locations as &$location) {
			$location['allowed'] = in_array($location['locationid'], $allowedLocations);
			while ($location['depth'] <= $depth) {
				array_pop($stack);
				$depth--;
			}
			while ($location['depth'] > $depth) {
				array_push($stack, empty($location['openingGlyph']) && ($depth === -1 || empty($stack[$depth])) ? '' : 'arrow-up');
				$depth++;
			}
			if ($depth > 0 && empty($location['openingGlyph'])) {
				$location['openingGlyph'] = $stack[$depth - 1];
			}
		}

		Render::addTemplate('page-locations', array(
			'list' => array_values($locations),
		));
	}

	private function showPanelsTable(): void
	{
		$visibleLocations = User::getAllowedLocations('panel.list');
		if (in_array(0, $visibleLocations)) {
			$visibleLocations = true;
		}
		$editLocations = User::getAllowedLocations('panel.edit');
		if (in_array(0, $editLocations)) {
			$editLocations = true;
		}
		$assignLocations = USer::getAllowedLocations('panel.assign-client');
		if (in_array(0, $assignLocations)) {
			$assignLocations = true;
		}
		if (empty($visibleLocations)) {
			Message::addError('main.no-permission');
			return;
		}
		$res = Database::simpleQuery('SELECT p.paneluuid, p.panelname, p.locationids, p.panelconfig,
			p.paneltype FROM locationinfo_panel p
			ORDER BY panelname ASC');
		$hasRunmode = Module::isAvailable('runmode');
		if ($hasRunmode) {
			$runmodes = RunMode::getForModule(Page::getModule(), true);
		}
		$panels = array();
		$locations = Location::getLocationsAssoc();
		foreach ($res as $row) {
			if ($row['paneltype'] === 'URL') {
				$url = json_decode($row['panelconfig'], true)['url'];
				$row['locations'] = $row['locationurl'] = $url;
				$row['edit_disabled'] = empty($editLocations) ? 'disabled' : '';
				$row['runmode_disabled'] = empty($assignLocations) ? 'disabled' : '';
			} else {
				$lids = explode(',', $row['locationids']);
				// Permissions
				if ($visibleLocations !== true && !empty(array_diff($lids, $visibleLocations))) {
					continue;
				}
				$row['edit_disabled'] = $editLocations !== true && !empty(array_diff($lids, $editLocations))
					? 'disabled' : '';
				$row['runmode_disabled'] = $assignLocations !== true && !empty(array_diff($lids, $assignLocations))
					? 'disabled' : '';
				// Locations
				$locs = array_map(function ($id) use ($locations) {
					return isset($locations[$id]) ? $locations[$id]['locationname'] : "<<deleted=$id>>";
				}, $lids);
				$row['locations'] = implode(', ', $locs);
			}
			$len = mb_strlen($row['panelname']);
			if ($len < 3) {
				$row['panelname'] .= str_repeat(' ', 3 - $len);
			}
			if ($hasRunmode && isset($runmodes[$row['paneluuid']])) {
				$row['assignedMachineCount'] = count($runmodes[$row['paneluuid']]);
			}
			$panels[] = $row;
		}
		Render::addTemplate('page-panels', compact('panels', 'hasRunmode'));
	}

	/**
	 * AJAX
	 */
	protected function doAjax()
	{
		User::load();
		if (!User::isLoggedIn()) {
			die('Unauthorized');
		}
		$action = Request::any('action');
		$id = Request::any('id', 0, 'int');
		if ($action === 'config-location') {
			$this->ajaxConfigLocation($id);
		} elseif ($action === 'serverSettings') {
			$this->ajaxServerSettings($id);
		}
	}

	/**
	 * Ajax the server settings.
	 *
	 * @param int $id Serverid
	 */
	private function ajaxServerSettings(int $id): void
	{
		User::assertPermission('backend.edit');
		$oldConfig = Database::queryFirst('SELECT servername, servertype, credentials
				FROM `locationinfo_coursebackend` WHERE serverid = :id', array('id' => $id));

		// Credentials stuff.
		if ($oldConfig !== false) {
			$oldCredentials = json_decode($oldConfig['credentials'], true);
		} else {
			$oldCredentials = array();
		}

		// Get a list of all the backend types.
		$serverBackends = array();
		$s_list = CourseBackend::getList();
		foreach ($s_list as $s) {
			$backendInstance = CourseBackend::getInstance($s);
			$backend = array(
				'backendtype' => $s,
				'display' => $backendInstance->getDisplayName(),
				'active' => ($oldConfig !== false && $s === $oldConfig['servertype']),
			);
			$backend['credentials'] = $backendInstance->getCredentialDefinitions();
			foreach ($backend['credentials'] as $cred) {
				/* @var BackendProperty $cred */
				if ($backend['active'] && isset($oldCredentials[$cred->property])) {
					$cred->initForRender($backendInstance->mangleProperty($cred->property, $oldCredentials[$cred->property]));
				} else {
					$cred->initForRender();
				}
				$cred->title = Dictionary::translateFile('backend-' . $s, $cred->property);
				$cred->helptext = Dictionary::translateFile('backend-' . $s, $cred->property . "_helptext");
				$cred->credentialsHtml = Render::parse('server-prop-' . $cred->template, (array)$cred);
			}
			$serverBackends[] = $backend;
		}
		echo Render::parse('ajax-config-server', array('id' => $id,
			'name' => $oldConfig['servername'],
			'currentbackend' => $oldConfig['servertype'],
			'backendList' => $serverBackends,
			'defaultBlank' => $oldConfig === false));
	}

	/**
	 * Ajax the time table
	 *
	 * @param int $id id of the location
	 */
	private function ajaxConfigLocation(int $id): void
	{
		User::assertPermission('location.edit', $id);
		$locConfig = Database::queryFirst("SELECT info.serverid, info.serverlocationid, loc.openingtime
 			FROM `locationinfo_locationconfig` AS info
 			LEFT JOIN  `location` AS loc USING (locationid)
 			WHERE locationid = :id", array('id' => $id));
		if ($locConfig !== false) {
			$openingtimes = json_decode($locConfig['openingtime'], true);
		} else {
			$locConfig = array('serverid' => null, 'serverlocationid' => '');
		}
		if (!isset($openingtimes) || !is_array($openingtimes)) {
			$openingtimes = array();
		}

		// Preset serverid from parent if none is set
		if (is_null($locConfig['serverid'])) {
			$chain = Location::getLocationRootChain($id);
			if (!empty($chain)) {
				$res = Database::simpleQuery("SELECT serverid, locationid FROM locationinfo_locationconfig
					WHERE locationid IN (:locations) AND serverid IS NOT NULL", array('locations' => $chain));
				$chain = array_flip($chain);
				$best = false;
				foreach ($res as $row) {
					if ($best === false || $chain[$row['locationid']] < $chain[$best['locationid']]) {
						$best = $row;
					}
				}
				if ($best !== false) {
					$locConfig['serverid'] = $best['serverid'];
				}
			}
		}

		// get Server / ID list
		$res = Database::simpleQuery("SELECT serverid, servername FROM locationinfo_coursebackend ORDER BY servername ASC");
		$serverList = array();
		foreach ($res as $row) {
			if ($row['serverid'] == $locConfig['serverid']) {
				$row['selected'] = 'selected';
			}
			$serverList[] = $row;
		}

		$data = array(
			'id' => $id,
			'serverlist' => $serverList,
			'serverlocationid' => $locConfig['serverlocationid'],
			'openingtimes' => $this->compressTimes($openingtimes),
		);

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

	/**
	 * Checks if simple mode or expert mode is active.
	 * Tries to merge/compact the opening times schedule, and
	 * will actually modify the passed array iff it can be
	 * transformed into simple opening times.
	 *
	 * @return array new optimized openingtimes
	 */
	private function compressTimes(array $array): array
	{
		if (empty($array))
			return [];
		// Decompose by day
		$DAYLIST = array_flip(['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']);
		$new = [];
		foreach ($array as $row) {
			$s = Page_LocationInfo::getTime($row['openingtime']);
			$e = Page_LocationInfo::getTime($row['closingtime']);
			if ($s === null || $e === null || $e <= $s)
				continue;
			foreach ($row['days'] as $day) {
				$day = $DAYLIST[$day] ?? -1;
				if ($day === -1)
					continue;
				$this->addDay($new, $day, $s, $e);
			}
		}
		// Merge by timespan
		$merged = [];
		foreach ($new as $day => $ranges) {
			foreach ($ranges as $range) {
				$range = $range[0] . '#' . $range[1];
				if (!isset($merged[$range])) {
					$merged[$range] = [];
				}
				$merged[$range][$day] = true;
			}
		}
		// Finally transform to display struct
		$new = [];
		foreach ($merged as $span => $days) {
			$out = explode('#', $span);
			$new[] = [
				'days' => $this->buildDaysString(array_keys($days)),
				'open' => sprintf('%02d:%02d', ($out[0] / 60), ($out[0] % 60)),
				'close' => sprintf('%02d:%02d', ($out[1] / 60), ($out[1] % 60)),
			];
		}
		return $new;
	}

	/**
	 * @param array $daysArray List of days, "Monday", "Tuesday" etc. Must not contain duplicates.
	 * @return string Human-readable representation of list of days
	 */
	private function buildDaysString(array $daysArray): string
	{
		/* Dictionary::translate('monday') Dictionary::translate('tuesday') Dictionary::translate('wednesday')
		 * Dictionary::translate('thursday') Dictionary::translate('friday') Dictionary::translate('saturday')
		 * Dictionary::translate('sunday')
		 */
		$DAYLIST = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
		$output = [];
		$first = $last = -1;
		sort($daysArray);
		$daysArray[] = -1; // One trailing element to enforce a flush
		foreach ($daysArray as $day) {
			if ($first === -1) {
				$first = $last = $day;
			} elseif ($last + 1 === $day) {
				// Chain
				$last++;
			} else {
				$string = Dictionary::translate($DAYLIST[$first]);
				if ($first !== $last) {
					$string .= ($first + 1 === $last ? ",\xe2\x80\x89" : "\xe2\x80\x89-\xe2\x80\x89")
						. Dictionary::translate($DAYLIST[$last]);
				}
				$output[] = $string;
				$first = $last = $day;
			}
		}
		return implode(', ', $output);
	}

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

	/**
	 * Ajax the config of a panel.
	 */
	private function showPanelConfig(): void
	{
		$id = Request::get('uuid', false, 'string');
		if ($id === false) {
			Message::addError('main.parameter-missing', 'uuid');
			return;
		}
		$config = false;
		if ($id === 'new-default') {
			// Creating new panel
			$panel = array(
				'panelname' => '',
				'locationids' => '',
				'paneltype' => 'DEFAULT',
			);
			$id = 'new';
		} elseif ($id === 'new-summary') {
			// Creating new panel
			$panel = array(
				'panelname' => '',
				'locationids' => '',
				'paneltype' => 'SUMMARY',
			);
			$id = 'new';
		} elseif ($id === 'new-url') {
			// Creating new panel
			$panel = array(
				'panelname' => '',
				'paneltype' => 'URL',
			);
			$id = 'new';
		} else {
			// Get Config data from db
			$panel = Database::queryFirst("SELECT panelname, locationids, paneltype, panelconfig
				FROM locationinfo_panel
				WHERE paneluuid = :id", array('id' => $id));
			if ($panel === false) {
				Message::addError('invalid-panel-id', $id);
				return;
			}

			$config = json_decode($panel['panelconfig'], true);
			if (!isset($config['roomplanner'])) {
				$config['roomplanner'] = true;
			}
		}

		// Permission
		$this->assertPanelPermission($panel, 'panel.edit');

		$def = LocationInfo::defaultPanelConfig($panel['paneltype']);
		if (!is_array($config)) {
			$config = $def;
		} else {
			$config += $def;
		}

		$langs = Dictionary::getLanguages(true);
		if (isset($config['language'])) {
			foreach ($langs as &$lang) {
				if ($lang['cc'] === $config['language']) {
					$lang['selected'] = 'selected';
				}
			}
		}

		if ($panel['paneltype'] === 'DEFAULT') {
			Render::addTemplate('page-config-panel-default', array(
				'new' => $id === 'new',
				'uuid' => $id,
				'panelname' => $panel['panelname'],
				'languages' => $langs,
				'mode' => $config['mode'],
				'vertical_checked' => $config['vertical'] ? 'checked' : '',
				'eco_checked' => $config['eco'] ? 'checked' : '',
				'prettytime_checked' => $config['prettytime'] ? 'checked' : '',
				'roomplanner' => $config['roomplanner'],
				'startday' => $config['startday'],
				'scaledaysauto_checked' => $config['scaledaysauto'] ? 'checked' : '',
				'daystoshow' => $config['daystoshow'],
				'rotation' => $config['rotation'],
				'scale' => $config['scale'],
				'switchtime' => $config['switchtime'],
				'calupdate' => $config['calupdate'],
				'roomupdate' => $config['roomupdate'],
				'locations' => Location::getLocations(),
				'locationids' => $panel['locationids'],
				'overrides' => json_encode($config['overrides']),
				'hostname_checked' => $config['hostname'] ? 'checked' : '',
			));
		} elseif ($panel['paneltype'] === 'URL') {

			$bookmarksArray = [];
			if ($config['bookmarks'] !== '') {
				$bookmarksConfig = explode(' ', $config['bookmarks']);
				foreach ($bookmarksConfig AS $bookmark) {
					$bookmark = explode(',', $bookmark);
					$name = rawurldecode($bookmark[0]);
					$url = rawurldecode($bookmark[1]);
					$bookmarksArray[] = [
						'name' => $name,
						'url' => $url,
					];
				}
			}

			LocationInfo::cleanupUrlFilter($config);

			Render::addTemplate('page-config-panel-url', array(
				'new' => $id === 'new',
				'uuid' => $id,
				'panelname' => $panel['panelname'],
				'url' => $config['url'],
				'zoom-factor' => $config['zoom-factor'],
				'ssl_checked' => $config['insecure-ssl'] ? 'checked' : '',
				'reloadminutes' => (int)$config['reload-minutes'],
				'whitelist' => str_replace("\n", "\r\n", $config['whitelist']),
				'blacklist' => str_replace("\n", "\r\n", $config['blacklist']),
				'split-login_checked' => $config['split-login'] ? 'checked' : '',
				'browser' => $config['browser'],
				'interactive_checked' => $config['interactive'] ? 'checked' : '',
				'hwvideo_checked' => $config['hw-video'] ? 'checked' : '',
				'bookmarks' => $bookmarksArray,
				'allow-tty_' . $config['allow-tty'] . '_checked' => 'checked',
			));
		} else {
			Render::addTemplate('page-config-panel-summary', array(
				'new' => $id === 'new',
				'uuid' => $id,
				'panelname' => $panel['panelname'],
				'languages' => $langs,
				'panelupdate' => $config['panelupdate'],
				'roomplanner' => $config['roomplanner'],
				'locations' => Location::getLocations(),
				'locationids' => $panel['locationids'],
				'eco_checked' => $config['eco'] ? 'checked' : '',
			));
		}
	}

	private function showPanel(): void
	{
		$uuid = Request::get('uuid', false, 'string');
		if ($uuid === false) {
			http_response_code(400);
			die('Missing parameter uuid');
		}
		$type = InfoPanel::getConfig($uuid, $config);
		if ($type === null) {
			http_response_code(404);
			die('Panel with given uuid not found');
		}

		if ($type === 'URL') {
			Util::redirect($config['url']);
		}

		$data = array();
		preg_match('#^/(.*)/#', $_SERVER['PHP_SELF'], $script);
		preg_match('#^/([^?]+)/#', $_SERVER['REQUEST_URI'], $request);
		if ($script[1] !== $request[1]) {
			// Working with server-side redirects
			$data['api'] = 'api/';
		} else {
			// 1:1
			$data['api'] = 'api.php?do=locationinfo&';
		}

		if ($type === 'DEFAULT') {
			$data += array(
				'uuid' => $uuid,
				'config' => json_encode($config),
				'language' => $config['language'],
			);

			die(Render::parse('frontend-default', $data, null, $config['language']));
		}

		if ($type === 'SUMMARY') {
			$locations = LocationInfo::getLocationsOr404($uuid, false);
			$config['tree'] = Location::getRecursive($locations);
			$data += array(
				'uuid' => $uuid,
				'config' => json_encode($config),
				'language' => $config['language'],
			);

			die(Render::parse('frontend-summary', $data, null, $config['language']));
		}

		http_response_code(500);
		die('Unknown panel type ' . $type);
	}

	/**
	 * @param string|array $panelOrUuid UUID of panel, or array with keys paneltype and locationds
	 * @param int[] $additionalLocations
	 */
	private function assertPanelPermission($panelOrUuid, string $permission, array $additionalLocations = null): void
	{
		if (is_array($panelOrUuid)) {
			$panel = $panelOrUuid;
		} else {
			$panel = Database::queryFirst('SELECT paneltype, locationids FROM locationinfo_panel
					WHERE paneluuid = :uuid', ['uuid' => $panelOrUuid]);
		}
		if ($panel === false || $panel['paneltype'] === 'URL' || empty($panel['locationids'])) {
			if (empty($additionalLocations)) {
				User::assertPermission($permission, null, '?do=locationinfo');
				return;
			}
		}
		$allowed = User::getAllowedLocations($permission);
		if (in_array(0, $allowed))
			return;
		if (!empty($allowed)) {
			if (isset($panel['locationids'])) {
				$locations = explode(',', $panel['locationids']);
			} else {
				$locations = [];
			}
			if (!empty($additionalLocations)) {
				$locations = array_merge($locations, $additionalLocations);
			}
			if (empty(array_diff($locations, $allowed)))
				return;
		}
		Message::addError('main.no-permission');
		Util::redirect('?do=locationinfo');
	}

}