1<?php 2 3/** 4 * Observium 5 * 6 * This file is part of Observium. 7 * 8 * @package observium 9 * @subpackage geolocation 10 * @author Adam Armstrong <adama@observium.org> 11 * @copyright (C) 2006-2013 Adam Armstrong, (C) 2013-2019 Observium Limited 12 * 13 */ 14 15// This function returns an array of location data when given an address. 16// The open&free geocoding APIs are not very flexible, so addresses must be in standard formats. 17 18// DOCME needs phpdoc block 19// TESTME needs unit testing 20function get_geolocation($address, $geo_db = array(), $dns_only = FALSE) 21{ 22 global $config; 23 24 $ok = FALSE; 25 $address = trim($address); 26 $location = array('location' => $address); // Init location array 27 $location['location_geoapi'] = strtolower(trim($config['geocoding']['api'])); 28 if (!isset($config['geo_api'][$location['location_geoapi']])) 29 { 30 // Use default if unknown api 31 $location['location_geoapi'] = 'geocodefarm'; 32 } 33 34 // v3 of geo definitions :D 35 $geo_def = $config['geo_api'][$location['location_geoapi']]; 36 37 // API Rate limit 38 $ratelimit = FALSE; 39 if (strlen($config['geocoding']['api_key']) && isset($geo_def['ratelimit_key'])) 40 { 41 // Ratelimit if used api key 42 $ratelimit = $geo_def['ratelimit_key']; 43 } 44 elseif (isset($geo_def['ratelimit'])) 45 { 46 $ratelimit = $geo_def['ratelimit']; 47 } 48 49 if (isset($config['geocoding']['enable']) && $config['geocoding']['enable']) 50 { 51 $geo_type = 'forward'; // by default forward geocoding 52 $debug_msg = "Geocoding ENABLED, try detect device coordinates:".PHP_EOL; 53 54 // If device coordinates set manually, use Reverse Geocoding. 55 if ($geo_db['location_manual']) 56 { 57 $location['location_lat'] = $geo_db['location_lat']; 58 $location['location_lon'] = $geo_db['location_lon']; 59 $geo_type = 'reverse'; 60 $debug_msg .= ' MANUAL coordinates - SET'.PHP_EOL; 61 } 62 // If DNS LOC support is enabled and DNS LOC record is set, use Reverse Geocoding. 63 elseif ($config['geocoding']['dns']) 64 { 65 /** 66 * Ack! dns_get_record not only cannot retrieve LOC records, but it also actively filters them when using 67 * DNS_ANY as query type (which, admittedly would not be all that reliable as per the manual). 68 * 69 * Example LOC: 70 * "20 31 55.893 N 4 57 38.269 E 45.00m 10m 100m 10m" 71 * 72 * From Wikipedia: d1 [m1 [s1]] {"N"|"S"} d2 [m2 [s2]] {"E"|"W"} 73 * 74 * Parsing this is something for Net_DNS2 as it has the code for it. 75 */ 76 if ($geo_db['hostname']) 77 { 78 //include_once('Net/DNS2.php'); 79 //include_once('Net/DNS2/RR/LOC.php'); 80 81 $resolver = new Net_DNS2_Resolver(); 82 try { 83 $response = $resolver->query($geo_db['hostname'], 'LOC', 'IN'); 84 } catch(Net_DNS2_Exception $e) { 85 $response = FALSE; 86 print_debug(' Resolver error: '.$e->getMessage().' (hostname: '.$geo_db['hostname'].')'); 87 } 88 } else { 89 $response = FALSE; 90 print_debug(" DNS LOC enabled, but device hostname empty."); 91 } 92 if ($response) 93 { 94 print_debug_vars($response->answer); 95 foreach ($response->answer as $answer) 96 { 97 if (is_numeric($answer->latitude) && is_numeric($answer->longitude)) 98 { 99 $location['location_lat'] = $answer->latitude; 100 $location['location_lon'] = $answer->longitude; 101 $geo_type = 'reverse'; 102 break; 103 } 104 else if (is_numeric($answer->degree_latitude) && is_numeric($answer->degree_longitude)) 105 { 106 $ns_multiplier = ($answer->ns_hem == 'N' ? 1 : -1); 107 $ew_multiplier = ($answer->ew_hem == 'E' ? 1 : -1); 108 109 $location['location_lat'] = round($answer->degree_latitude + $answer->min_latitude/60 + $answer->sec_latitude/3600,7) * $ns_multiplier; 110 $location['location_lon'] = round($answer->degree_longitude + $answer->min_longitude/60 + $answer->sec_longitude/3600,7) * $ew_multiplier; 111 $geo_type = 'reverse'; 112 break; 113 } 114 } 115 if (isset($location['location_lat'])) 116 { 117 $debug_msg .= ' DNS LOC records - FOUND'.PHP_EOL; 118 } else { 119 $debug_msg .= ' DNS LOC records - NOT FOUND'.PHP_EOL; 120 if ($dns_only) 121 { 122 // If we check only DNS LOC records but it not found, exit 123 print_debug($debug_msg); 124 return FALSE; 125 } 126 } 127 } 128 } 129 130 /** 131 * If location string contains coordinates use Reverse Geocoding. 132 * Valid strings: 133 * Some location [33.234, -56.22] 134 * Some location (33.234 -56.22) 135 * Some location [33.234;-56.22] 136 * 33.234,-56.22 137 */ 138 $pattern = '/(?:^|[\[(])\s*(?<lat>[+-]?\d+(?:\.\d+)*)\s*[,:; ]\s*(?<lon>[+-]?\d+(?:\.\d+)*)\s*(?:[\])]|$)/'; 139 if ($geo_type == 'forward' && preg_match($pattern, $address, $matches)) 140 { 141 if ($matches['lat'] >= -90 && $matches['lat'] <= 90 && 142 $matches['lon'] >= -180 && $matches['lon'] <= 180) 143 { 144 $location['location_lat'] = $matches['lat']; 145 $location['location_lon'] = $matches['lon']; 146 $geo_type = 'reverse'; 147 } 148 } 149 150 // Excluded bad location strings like <none>, Unknown, '' 151 $valid_address = strlen($address) > 4 && !preg_match('/^[<\\\(]?(unknown|private|none|office|location|snmplocation)[>\\\)]?$/i', $address); 152 if ($geo_type == 'reverse' || $valid_address) 153 { 154 // We have correct non empty address or reverse coordinates 155 156 // Have api specific file include and definition? 157 $is_geo_def = isset($geo_def[$geo_type]); 158 $is_geo_file = is_file($config['install_dir'] . '/includes/geolocation/' . $location['location_geoapi'] . '.inc.php'); 159 160 if ($geo_type == 'reverse') 161 { 162 $debug_msg .= ' by REVERSE query (API: '.strtoupper($config['geocoding']['api']).', LAT: '.$location['location_lat'].', LON: '.$location['location_lon'].') - '; 163 } else { 164 $debug_msg .= ' by FORWARD query (API: '.strtoupper($config['geocoding']['api']).', sysLocation: ' . $address . ') - '; 165 } 166 167 if ($is_geo_def) 168 { 169 // Generate geolocation tags, used for rewrites in definition 170 $tags = generate_geolocation_tags($location['location_geoapi'], $location); 171 172 // Generate context/options with encoded data and geo specific api headers 173 $options = generate_http_context($geo_def[$geo_type], $tags); 174 175 // API URL to POST to 176 $url = generate_http_url($geo_def[$geo_type], $tags); 177 178 // First request 179 $mapresponse = get_http_request($url, $options, $ratelimit); 180 181 // Send out API call and parse response 182 if (!test_http_request($geo_def[$geo_type], $mapresponse)) 183 { 184 // False response 185 $geo_status = strtoupper($GLOBALS['response_headers']['status']); 186 $debug_msg .= $geo_status . PHP_EOL; 187 if (OBS_DEBUG < 2 && strlen($tags['key'])) 188 { 189 // Hide API KEY from output 190 $url = str_replace('=' . $tags['key'], '=***', $url); 191 } 192 $debug_msg .= ' GEO API REQUEST: ' . $url; 193 print_debug($debug_msg); 194 // Return old array with new status (for later recheck) 195 unset($geo_db['hostname'], $geo_db['location_updated']); 196 $location['location_status'] = $debug_msg; 197 $location['location_updated'] = format_unixtime($config['time']['now'], 'Y-m-d G:i:s'); 198 //print_vars($location); 199 //print_vars($geo_db); 200 return array_merge($geo_db, $location); 201 } 202 203 switch ($geo_def[$geo_type]['response_format']) 204 { 205 case 'xml': 206 // Hrm, currently unused 207 break; 208 case 'json': 209 default: 210 $data = json_decode($mapresponse, TRUE); 211 } 212 213 if ($geo_type == 'forward') 214 { 215 // We seem to have hit a snag geocoding. It might be that the first element of the address is a business name. 216 // Lets drop the first element and see if we get anything better! This works more often than one might expect. 217 if (str_contains($address, ' - ')) 218 { 219 // Rack: NK-76 - Nikhef, Amsterdam, NL 220 list(, $address_second) = explode(' - ', $address, 2); 221 $address_second = trim($address_second); 222 } 223 elseif (str_contains($address, ',')) 224 { 225 // ZRH2, Badenerstrasse 569, Zurich, Switzerland 226 list(, $address_second) = explode(',', $address, 2); 227 $address_second = trim($address_second); 228 } else { 229 $address_second = NULL; 230 } 231 232 // Coordinates not found, try second request 233 if (isset($geo_def[$geo_type]['location_lat']) && 234 !strlen(array_get_nested($data, $geo_def[$geo_type]['location_lat'])) && 235 strlen($address_second) > 4) 236 { 237 238 // Re-Generate geolocation tags, override location 239 $tags['location'] = $address_second; 240 241 // Generate context/options with encoded data and geo specific api headers 242 $options_new = generate_http_context($geo_def[$geo_type], $tags); 243 244 // API URL to POST to 245 $url_new = generate_http_url($geo_def[$geo_type], $tags); 246 247 // Second request 248 $mapresponse = get_http_request($url_new, $options_new, $ratelimit); 249 if (test_http_request($geo_def[$geo_type], $mapresponse)) 250 { 251 // Valid response 252 $data_new = json_decode($mapresponse, TRUE); 253 if (strlen(array_get_nested($data, $geo_def[$geo_type]['location_lat']))) 254 { 255 $data = $data_new; 256 $url = $url_new; 257 } 258 } 259 } 260 } // End second forward request 261 262 } 263 264 //print_vars($data); 265 $geo_status = 'NOT FOUND'; 266 267 if ($is_geo_file) 268 { 269 // API specific parser 270 require($config['install_dir'] . '/includes/geolocation/' . $location['location_geoapi'] . '.inc.php'); 271 272 if ($data === FALSE) 273 { 274 // Return old array with new status (for later recheck) 275 unset($geo_db['hostname'], $geo_db['location_updated']); 276 //$location['location_status'] = $debug_msg; 277 $location['location_updated'] = format_unixtime($config['time']['now'], 'Y-m-d G:i:s'); 278 //print_vars($location); 279 //print_vars($geo_db); 280 return array_merge($geo_db, $location); 281 } 282 283 } 284 elseif ($is_geo_def) 285 { 286 // Set lat/lon and others by definitions 287 if ($geo_type == 'forward') 288 { 289 $location['location_lat'] = array_get_nested($data, $geo_def[$geo_type]['location_lat']); 290 $location['location_lon'] = array_get_nested($data, $geo_def[$geo_type]['location_lon']); 291 } 292 foreach (['city', 'county', 'state', 'country'] as $entry) 293 { 294 $param = 'location_' . $entry; 295 foreach ((array)$geo_def[$geo_type][$param] as $field) 296 { 297 // Possible to use multiple fields, use first not empty 298 if ($location[$param] = array_get_nested($data, $field)) { break; } 299 } 300 } 301 } 302 303 print_debug_vars($data); 304 } else { 305 $geo_status = 'NOT REQUESTED'; 306 } 307 } 308 309 // Use defaults if empty values 310 if (!strlen($location['location_lat']) || !strlen($location['location_lon'])) 311 { 312 // Reset to empty coordinates 313 $location['location_lat'] = array('NULL'); 314 $location['location_lon'] = array('NULL'); 315 //$location['location_lat'] = $config['geocoding']['default']['lat']; 316 //$location['location_lon'] = $config['geocoding']['default']['lon']; 317 //if (is_numeric($config['geocoding']['default']['lat']) && is_numeric($config['geocoding']['default']['lon'])) 318 //{ 319 // $location['location_manual'] = 1; // Set manual key for ability reset from WUI 320 //} 321 } else { 322 // Always round lat/lon same as DB precision (DECIMAL(10,7)) 323 $location['location_lat'] = round($location['location_lat'], 7); 324 $location['location_lon'] = round($location['location_lon'], 7); 325 } 326 327 // Remove duplicate County/State words 328 foreach (array('city', 'county', 'state') as $entry) 329 { 330 $param = 'location_' . $entry; 331 $location[$param] = strlen($location[$param]) ? str_ireplace(' '.$entry, '', $location[$param]) : 'Unknown'; 332 } 333 // Unificate Country name 334 if (strlen($location['location_country'])) 335 { 336 $location['location_country'] = country_from_code($location['location_country']); 337 $geo_status = 'FOUND'; 338 } else { 339 $location['location_country'] = 'Unknown'; 340 } 341 342 // Print some debug information 343 $debug_msg .= $geo_status . PHP_EOL; 344 if (OBS_DEBUG < 2 && strlen($tags['key'])) 345 { 346 // Hide API KEY from output 347 $url = str_replace('=' . $tags['key'], '=***', $url); 348 } 349 $debug_msg .= ' GEO API REQUEST: ' . $url; 350 351 if ($geo_status == 'FOUND') 352 { 353 $debug_msg .= PHP_EOL . ' GEO LOCATION: '; 354 $debug_msg .= $location['location_country'].' (Country), '.$location['location_state'].' (State), '; 355 $debug_msg .= $location['location_county'] .' (County), ' .$location['location_city'] .' (City)'; 356 $debug_msg .= PHP_EOL . ' GEO COORDINATES: '; 357 $debug_msg .= $location['location_lat'] .' (Latitude), ' .$location['location_lon'] .' (Longitude)'; 358 } else { 359 $debug_msg .= PHP_EOL . ' QUERY DATE: '.date('r'); // This is requered for increase data in DB 360 } 361 print_debug($debug_msg); 362 $location['location_status'] = $debug_msg; 363 364 return $location; 365} 366 367/** 368 * Generate geolocation tags, used for transform any other parts of geo api definition. 369 * 370 * @global array $config 371 * @param string $api GEO api key (see geo definitions) 372 * @param array $tags (optional) Location array and other tags 373 * @param array $params (optional) Array of requested params with key => value entries (used with request method POST) 374 * @param string $location (optional) Location string, if passed override location tag 375 * @return array HTTP Context which can used in get_http_request_test() or get_http_request() 376 */ 377function generate_geolocation_tags($api, $tags = array(), $params = array(), $location = NULL) 378{ 379 global $config; 380 381 $tags = array_merge($tags, $params); 382 383 // Override location tag if passed as argument 384 if (strlen($location)) 385 { 386 $tags['location'] = $location; 387 } 388 // Add lat/lon tags 389 if (isset($tags['location_lon'])) 390 { 391 $tags['lon'] = $tags['location_lon']; 392 } 393 if (isset($tags['location_lon'])) 394 { 395 $tags['lat'] = $tags['location_lat']; 396 } 397 398 // Common used params for geo apis 399 $tags['id'] = OBSERVIUM_PRODUCT . '-' . substr(get_unique_id(), 0, 8); 400 $tags['uuid'] = get_unique_id(); 401 402 // API key if not empty 403 if (isset($config['geo_api'][$api]['key']) && strlen($config['geo_api'][$api]['key'])) 404 { 405 // Ability to store API keys for each GEO API separately 406 $tags['key'] = escape_html($config['geo_api'][$api]['key']); // KEYs is never used special characters 407 } 408 elseif (strlen($config['geocoding']['api_key'])) 409 { 410 $tags['key'] = escape_html($config['geocoding']['api_key']); // KEYs is never used special characters 411 } 412 413 //print_vars($tags); 414 return $tags; 415} 416 417// EOF 418