summaryrefslogblamecommitdiffstats
path: root/modules-available/locations/inc/location.inc.php
blob: 807f8577c437ca06bc80644ad6d006d8803ea873 (plain) (tree)
1
2
3
4
5
6
7
8
9






                                                   
                                          
                                               
 
                                               







                                                                             
                                                      


                                                                                                                
                                        



                                       
 

                                                          

                                                             
                                                   



                                                                                                                            




                                                                                   
                                                       
         


                                                          



                                                                              
 


                                                                                

                                                                                                       
                                                            
         


                                                          









                                                                                                   


                                                          
                                                



                                                                                  
 
                                                                                              
         
                                  
                                                                                                                      
                 

                                          
                                                                                                                                                       
                                                                  
                                                                         
                                                                                     
                                                      

                                                        
                                                                        

                                                  

                                                        





                                                                                                                                                            

                         




                                                                              


                               

                                                                           
                                                            



                                                                          
                                                                                                                                             

                                                         
                                                






                                                         
                           









                                                                                 
                                                                                                                                  

                                                        
                                                     




                                                          

                                                              

                           

                                     


                                           



                                                                                     
                                                                          

                                                                     
                                                                                        



                                                 
                                             


















                                                                                                             
                                                               







                                                       
                                                                                   


















                                                                                               
                                                                               
         
                                  
                                                                                                                      
                 

                                          
                                                                  
                                                                    
                                                                                

                                                                          
                                                                            


                                                        
                                                                                            




                               

                                                            



                                                                                                 
                                     

         
                                                             


                                          
                                                          





                                                                                              
 
           
                                                      
          
                                           
                                                          
           
                                                               




                                                                                                                         
                                                          




                                               
                                                                                
                                                                      

                                                 
                                                                                                   
                                                             
           
                                                                                    
         

                                                                                  








                                                                                                        
                                             

                                                                


                                             
                 
                                                  

         
           



                                                                            
                                                 

                                                                  
           
                                                                          


                                              


                                                                                                                 
                                             

                                                                                                                                                                      
                                                                                   
                                                               

                                 



                                   
                                                                           
         

                                       













                                                                                                         

         
           

                                                                       

                                                            
                                                                           
         

                                                          
                 




                                                                           


           

                                                         
                                                  
         
                                                                                                  
                                   
                                        





                                                           
           
                                                                       
           
                                                                              































                                                                            
                                          
                                                                    
           
                                                          



























                                                                                                                                               



                                                                              






                                                                                                                                       
                            

         
 
<?php

class Location
{

	private static $flatLocationCache = false;
	private static $assocLocationCache = false;
	private static $treeCache = false;
	private static $subnetMapCache = false;

	public static function getTree(): array
	{
		if (self::$treeCache === false) {
			self::$treeCache = self::queryLocations();
			self::$treeCache = self::buildTree(self::$treeCache);
		}
		return self::$treeCache;
	}

	public static function queryLocations(): array
	{
		$res = Database::simpleQuery("SELECT locationid, parentlocationid, locationname FROM location");
		$rows = array();
		foreach ($res as $row) {
			$rows[] = $row;
		}
		return $rows;
	}

	/**
	 * Return row from location table for $locationId.
	 * @return array|bool row from DB, false if not found
	 */
	public static function get(int $locationId)
	{
		return Database::queryFirst("SELECT * FROM location WHERE locationid = :locationId", compact('locationId'));
	}

	/**
	 * Get name of location
	 * @param int $locationId id of location to get name for
	 * @return string|false Name of location, false if locationId doesn't exist
	 */
	public static function getName(int $locationId)
	{
		if (self::$assocLocationCache === false) {
			self::getLocationsAssoc();
		}
		if (!isset(self::$assocLocationCache[$locationId]))
			return false;
		return self::$assocLocationCache[$locationId]['locationname'];
	}

	/**
	 * Get all the names of the given location and its parents, up
	 * to the root element. Array keys will be locationids, value the names.
	 * @return array|false locations, from furthest to nearest or false if locationId doesn't exist
	 */
	public static function getNameChain(int $locationId)
	{
		if (self::$assocLocationCache === false) {
			self::getLocationsAssoc();
		}
		if (!isset(self::$assocLocationCache[$locationId]))
			return false;
		$ret = array();
		while (isset(self::$assocLocationCache[$locationId])) {
			$ret[$locationId] = self::$assocLocationCache[$locationId]['locationname'];
			$locationId = self::$assocLocationCache[$locationId]['parentlocationid'];
		}
		return array_reverse($ret, true);
	}

	public static function getLocationsAssoc()
	{
		if (self::$assocLocationCache === false) {
			$rows = self::getTree();
			self::$assocLocationCache = self::flattenTreeAssoc($rows);
		}
		return self::$assocLocationCache;
	}

	private static function flattenTreeAssoc($tree, $parents = array(), $depth = 0): array
	{
		if ($depth > 20) {
			ErrorHandler::traceError('Recursive location definition detected at ' . print_r($tree, true));
		}
		$output = array();
		foreach ($tree as $node) {
			$cc = empty($node['children']) ? array() : array_map(function ($item) { return (int)$item['locationid']; }, $node['children']);
			$output[(int)$node['locationid']] = array(
				'locationid' => (int)$node['locationid'],
				'parentlocationid' => (int)$node['parentlocationid'],
				'parents' => $parents,
				'children' => $cc,
				'directchildren' => $cc,
				'locationname' => $node['locationname'],
				'depth' => $depth,
				'isleaf' => true,
			);
			if (!empty($node['children'])) {
				$childNodes = self::flattenTreeAssoc($node['children'], array_merge($parents, array((int)$node['locationid'])), $depth + 1);
				$output[(int)$node['locationid']]['children'] = array_merge($output[(int)$node['locationid']]['children'],
					array_reduce($childNodes, function ($carry, $item) {
						return array_merge($carry, $item['children']);
					}, array()));
				$output += $childNodes;
			}
		}
		foreach ($output as &$entry) {
			if (!isset($output[$entry['parentlocationid']]))
				continue;
			$output[$entry['parentlocationid']]['isleaf'] = false;
		}
		return $output;
	}

	/**
	 * @param int|int[] $selected Which locationIDs to mark as selected
	 * @param int $excludeId Which locationID to exclude
	 * @param bool $addNoParent Add entry for "no location" at the top
	 * @param bool $keepArrayKeys Keep location IDs as array index
	 * @return array Locations
	 */
	public static function getLocations($selected = 0, int $excludeId = 0, bool $addNoParent = false, bool $keepArrayKeys = false): array
	{
		if (self::$flatLocationCache === false) {
			$rows = self::getTree();
			$rows = self::flattenTree($rows);
			self::$flatLocationCache = $rows;
		} else {
			$rows = self::$flatLocationCache;
		}
		$del = false;
		unset($row);
		$index = 0;
		foreach ($rows as $key => &$row) {
			if ($del === false && $row['locationid'] == $excludeId) {
				$del = $row['depth'];
			} elseif ($del !== false && $row['depth'] <= $del) {
				$del = false;
			}
			if ($del !== false) {
				unset($rows[$key]);
				continue;
			}
			if ((is_array($selected) && in_array($row['locationid'], $selected)) || $row['locationid'] == $selected) {
				$row['selected'] = true;
			}
			$row['sortIndex'] = $index++;
		}
		if ($addNoParent) {
			array_unshift($rows, array(
				'locationid' => 0,
				'locationname' => '-----',
				'selected' => $selected === 0,
				'locationpad' => '',
			));
		}
		if ($keepArrayKeys)
			return $rows;
		return array_values($rows);
	}

	/**
	 * Get nested array of all the locations and children of given locationid(s).
	 *
	 * @param int[]|int $idList List of location ids
	 * @param ?array $locationTree used in recursive calls, don't pass
	 * @return array list of passed locations plus their children
	 */
	public static function getRecursive($idList, ?array $locationTree = null): array
	{
		if (!is_array($idList)) {
			$idList = array($idList);
		}
		if ($locationTree === null) {
			$locationTree = self::getTree();
		}
		$ret = array();
		foreach ($locationTree as $location) {
			if (in_array($location['locationid'], $idList)) {
				$ret[] = $location;
			} elseif (!empty($location['children'])) {
				$ret = array_merge($ret, self::getRecursive($idList, $location['children']));
			}
		}
		return $ret;
	}

	/**
	 * Get flat array of all the locations and children of given locationid(s).
	 *
	 * @param int[]|int $idList List of location ids
	 * @return array list of passed locations plus their children
	 */
	public static function getRecursiveFlat($idList): array
	{
		$ret = self::getRecursive($idList);
		if (!empty($ret)) {
			$ret = self::flattenTree($ret);
		}
		return $ret;
	}

	public static function buildTree(array $elements, int $parentId = 0): array
	{
		$branch = array();
		$sort = array();
		foreach ($elements as $element) {
			if ($element['locationid'] == 0 || $element['locationid'] == $parentId)
				continue;
			if ($element['parentlocationid'] == $parentId) {
				$children = self::buildTree($elements, $element['locationid']);
				if (!empty($children)) {
					$element['children'] = $children;
				}
				$branch[] = $element;
				$sort[] = $element['locationname'];
			}
		}
		array_multisort($sort, SORT_ASC, $branch);
		return $branch;
	}

	private static function flattenTree(array $tree, int $depth = 0): array
	{
		if ($depth > 20) {
			ErrorHandler::traceError('Recursive location definition detected at ' . print_r($tree, true));
		}
		$output = array();
		foreach ($tree as $node) {
			$output[(int)$node['locationid']] = array(
				'locationid' => $node['locationid'],
				'parentlocationid' => $node['parentlocationid'],
				'locationname' => $node['locationname'],
				'locationpad' => str_repeat('--', $depth),
				'isleaf'	=> empty($node['children']),
				'depth' => $depth
			);
			if (!empty($node['children'])) {
				$output += self::flattenTree($node['children'], $depth + 1);
			}
		}
		return $output;
	}

	public static function isLeaf(int $locationid): bool
	{
		$result = Database::queryFirst('SELECT COUNT(locationid) = 0 AS isleaf '
			. 'FROM location '
			. 'WHERE parentlocationid = :locationid', ['locationid' => $locationid]);
		$result = $result['isleaf'];
		return (bool)$result;
	}

	public static function extractIds(array $tree): array
	{
		$ids = array();
		foreach ($tree as $node) {
			$ids[] = (int)$node['locationid'];
			if (!empty($node['children'])) {
				$ids = array_merge($ids, self::extractIds($node['children']));
			}
		}
		return $ids;
	}

	/**
	 * Get location id for given machine (by uuid)
	 *
	 * @param string $uuid machine uuid
	 * @return false|int locationid, false if no match
	 */
	public static function getFromMachineUuid(string $uuid)
	{
		// Only if we have the statistics module which supplies the machine table
		if (Module::get('statistics') === false)
			return false;
		$ret = Database::queryFirst("SELECT locationid FROM machine WHERE machineuuid = :uuid", compact('uuid'));
		if ($ret === false || !$ret['locationid'])
			return false;
		return (int)$ret['locationid'];
	}

	/**
	 * Get closest location by matching subnets. Deepest match in tree wins.
	 * Ignores any manually assigned locationid (fixedlocationid).
	 *
	 * @param string $ip IP address of client
	 * @param bool $honorRoomPlanner consider a fixed location assigned manually by roomplanner
	 * @return false|int locationid, or false if no match
	 */
	public static function getFromIp(string $ip, bool $honorRoomPlanner = false)
	{
		if (Module::get('statistics') !== false) {
			// Shortcut - try to use subnetlocationid in machine table
			if ($honorRoomPlanner) {
				$ret = Database::queryFirst("SELECT locationid AS loc FROM machine
						WHERE clientip = :ip
						ORDER BY lastseen DESC LIMIT 1", compact('ip'));
			} else {
				$ret = Database::queryFirst("SELECT subnetlocationid AS loc FROM machine
						WHERE clientip = :ip
						ORDER BY lastseen DESC LIMIT 1", compact('ip'));
			}
			if ($ret !== false) {
				if ($ret['loc'] > 0) {
					return (int)$ret['loc'];
				}
				return false;
			}
		}
		return self::mapIpToLocation($ip);
	}

	/**
	 * Combined "intelligent" fetching of locationId by IP and UUID of
	 * client. We can't trust the UUID too much as it is provided by the
	 * client, so if it seems too fishy, the UUID will be ignored.
	 *
	 * @param string $ip IP address of client
	 * @param ?string $uuid System-UUID of client
	 * @return int|false location id, or false if none matches
	 */
	public static function getFromIpAndUuid(string $ip, ?string $uuid)
	{
		$locationId = false;
		$ipLoc = self::getFromIp($ip);
		if ($ipLoc !== false) {
			// Set locationId to ipLoc for now, it will be overwritten later if another case applies.
			$locationId = $ipLoc;
			if ($uuid !== null) {
				// Machine ip maps to a location, and we have a client supplied uuid (which might not be known if the client boots for the first time)
				$uuidLoc = self::getFromMachineUuid($uuid);
				if (self::isFixedLocationValid($uuidLoc, $ipLoc)) {
					$locationId = $uuidLoc;
				}
			}
		}
		return $locationId;
	}

	public static function isFixedLocationValid($uuidLoc, $ipLoc): bool
	{
		if ($uuidLoc === false)
			return false;
		$uuidLoc = (int)$uuidLoc;
		$ipLoc = (int)$ipLoc;
		if ($uuidLoc === $ipLoc || $uuidLoc === 0)
			return true;
		if ($ipLoc === 0)
			return false; // roomplanner assignment, but no subnet - deny
		// Validate that the location the IP maps to is in the chain we get using the
		// location determined by roomplanner
		$uuidLocations = self::getLocationRootChain($uuidLoc);
		if (!empty(self::$assocLocationCache[$uuidLoc]['children']))
			return false; // roomplanner location isn't actually leaf node, this is not valid
		$idx = array_search($ipLoc, $uuidLocations);
		// If roomplanner loc is max two levels deeper than IP loc, accept
		return $idx !== false && $idx <= 2;
	}

	/**
	 * Get all location IDs from the given location up to the root.
	 *
	 * @return int[] location ids, including $locationId
	 */
	public static function getLocationRootChain(int $locationId): array
	{
		if (self::$assocLocationCache === false) {
			self::getLocationsAssoc();
		}
		if (!isset(self::$assocLocationCache[$locationId]))
			return [];
		$chain = self::$assocLocationCache[$locationId]['parents'];
		$chain[] = $locationId;
		return array_reverse($chain);
	}

	/**
	 * @return array list of subnets as numeric array
	 */
	public static function getSubnets(): array
	{
		$res = Database::simpleQuery("SELECT startaddr, endaddr, locationid FROM subnet");
		$subnets = array();
		foreach ($res as $row) {
			settype($row['locationid'], 'int');
			$subnets[] = $row;
		}
		return $subnets;
	}

	/**
	 * @return array assoc array mapping from locationid to subnets
	 */
	public static function getSubnetsByLocation($recursive = false): array
	{
		$locs = self::getLocationsAssoc();
		$subnets = self::getSubnets();
		// Accumulate - copy up subnet definitions
		foreach ($locs as &$loc) {
			$loc['subnets'] = array();
		}
		unset($loc);
		foreach ($subnets as $subnet) {
			$lid = $subnet['locationid'];
			while (isset($locs[$lid])) {
				$locs[$lid]['subnets'][] = array(
					'startaddr' => $subnet['startaddr'],
					'endaddr' => $subnet['endaddr']
				);
				if (!$recursive)
					break;
				$lid = $locs[$lid]['parentlocationid'];
			}
		}
		return $locs;
	}

	/**
	 * Lookup $ip in subnets, try to find one that matches
	 * and return its locationid.
	 * If two+ subnets match, the one which is nested deeper wins.
	 * If two+ subnets match and have the same depth, the one which
	 * is smaller wins.
	 * If two+ subnets match and have the same depth and size, a
	 * random one will be returned.
	 *
	 * @param string $ip IP to look up
	 * @return false|int locationid ip matches, false = no match
	 */
	public static function mapIpToLocation(string $ip)
	{
		if (self::$subnetMapCache === false) {
			self::$subnetMapCache = self::getSubnetsByLocation();
		}
		$long = sprintf('%u', ip2long($ip));
		$best = false;
		$bestSize = 0;
		foreach (self::$subnetMapCache as $lid => $data) {
			if ($best !== false && self::$subnetMapCache[$lid]['depth'] < self::$subnetMapCache[$best]['depth'])
				continue; // Don't even need to take a look
			foreach ($data['subnets'] as $subnet) {
				if ($long < $subnet['startaddr'] || $long > $subnet['endaddr'])
					continue; // Nope
				if ($best !== false // Already have a best candidate
						&& self::$subnetMapCache[$lid]['depth'] === self::$subnetMapCache[$best]['depth'] // Same depth
						&& $bestSize < $subnet['endaddr'] - $subnet['startaddr']) { // Old candidate has smaller subnet
					// So we ignore this one as the old one is more specific
					continue;
				}
				$bestSize = $subnet['endaddr'] - $subnet['startaddr'];
				$best = $lid;
			}
		}
		if ($best === false)
			return false;
		return (int)$best;
	}

	/**
	 * @return false|int newly determined location
	 */
	public static function updateMapIpToLocation(string $uuid, string $ip)
	{
		$loc = self::mapIpToLocation($ip);
		if ($loc === false) {
			Database::exec("UPDATE machine SET subnetlocationid = NULL WHERE machineuuid = :uuid", compact('uuid'));
		} else {
			Database::exec("UPDATE machine SET subnetlocationid = :loc WHERE machineuuid = :uuid", compact('loc', 'uuid'));
		}
		return $loc;
	}

}