IP Geolocation project page and in the README file', array(
'@ip_geoloc' => url('http://drupal.org/project/ip_geoloc'),
'@README' => url(drupal_get_path('module', 'ip_geoloc') . '/README.txt')
));
}
}
/**
* Implements hook_menu().
*
* Defines new menu items.
*/
function ip_geoloc_menu() {
$items = array();
// Put the administrative settings under System on the Configuration page.
$items['admin/config/system/ip_geoloc'] = array(
'title' => 'IP Geolocation Views and Maps',
'description' => 'Configure how geolocation information is updated.',
'page callback' => 'drupal_get_form',
'page arguments' => array('ip_geoloc_admin_configure'),
'access arguments' => array('administer site configuration'),
'file' => 'ip_geoloc.admin.inc'
);
$items['ip-geoloc-current-location'] = array(
'title' => 'Current location recipient',
'page callback' => 'ip_geoloc_current_location_ajax_recipient',
'access arguments' => array('access content'),
'type' => MENU_CALLBACK,
);
return $items;
}
/**
* Implements hook_init().
*
* Due to the weight set in ip_geoloc.install this hook is called after all
* other hook_init() implementations have completed.
* hook_inits are called as the last step in _drupal_bootstrap_full(), file
* includes/common.inc
* Note that the {accesslog} is updated in statistics_exit(), i.e. after the
* page is loaded. This means that a second click may be required before the
* current position marker appears on the recent visitor map.
*/
function ip_geoloc_init() {
$location = ip_geoloc_get_visitor_location();
$reverse_geocode_client_timeout = ip_geoloc_reverse_geocode_timeout();
// Sample location when due or as soon as a reverse_geocode timeout is detected.
if (_ip_geoloc_check_location($location) || $reverse_geocode_client_timeout) {
if ($use_google_to_reverse_geocode = variable_get('ip_geoloc_google_to_reverse_geocode', TRUE)) {
global $user;
$roles_to_reverse_geocode = variable_get('ip_geoloc_roles_to_reverse_geocode', array(DRUPAL_ANONYMOUS_RID, DRUPAL_AUTHENTICATED_RID));
$roles_applicable = array_intersect($roles_to_reverse_geocode, array_keys($user->roles));
$use_google_to_reverse_geocode = !empty($roles_applicable);
}
// Handle first click of the session, ie $last_position_check not set,
// as well as client timeout fallback.
$last_position_check = _ip_geoloc_get_session_value('last_position_check');
if (!$use_google_to_reverse_geocode || $reverse_geocode_client_timeout || empty($last_position_check)) {
// The calls below are synchronous, $location is filled immediately upon return.
if (ip_geoloc_use_smart_ip_if_enabled($location) || ip_geoloc_use_geoip_api_if_enabled($location)) {
/*
if ($use_google_to_reverse_geocode && isset($location['latitude']) && isset($location['longitude'])) {
// Initialise street address details. These are based on the IP,
// so may reflect the provider location, rather than browser location.
// This is a synchronous server-side call, so may result in the Google
// maximum number of calls per day being reached.
if ($google_address = ip_geoloc_reverse_geocode($location['latitude'], $location['longitude'])) {
ip_geoloc_flatten_google_address($google_address, $location);
}
}
*/
if ($reverse_geocode_client_timeout) {
watchdog('IP Geolocation', 'Location timeout (waited %sec s). Fallback: %address.', array(
'%sec' => number_format($reverse_geocode_client_timeout, 1),
'%address' => isset($location['formatted_address']) ? $location['formatted_address'] : ''),
WATCHDOG_NOTICE);
}
}
else {
ip_geoloc_debug(t('Smart IP and GeoIP API fallbacks NOT enabled.'));
}
_ip_geoloc_set_session_value('position_pending_since', NULL);
}
if ($use_google_to_reverse_geocode && !variable_get('maintenance_mode', 0) /* avoid HTTP 503 */) {
// Insert some javascript to first retrieve the user's lat/long coords,
// HTML5 style (requiring the user to accept a browser prompt) and then
// use Google Maps API to reverse-geocode these coords into a street address.
// This is all done via client-side calls, so the Drupal server
// will not rake up any calls against its Google-imposed quotum, ie the
// OVER_QUERY_LIMIT.
// When done the javascript calls us back on the supplied menu callback,
// '/ip-geoloc-current-location', which receives the geolocation data
// from the Google Maps call via the $_POST variable and stores it in
// the session.
// Naturally all of this will only work if the browser is connected to
// the internet and has javascript enabled.
ip_geoloc_debug(t('IP Geolocation: initiating services to locate current position and reverse-geocode to address...'));
ip_geoloc_get_current_location('ip-geoloc-current-location');
_ip_geoloc_set_session_value('position_pending_since', microtime(TRUE));
}
_ip_geoloc_set_session_value('last_position_check', time());
};
// ip_geoloc_store_location() does nothing, if supplied IP address is empty.
if (ip_geoloc_store_location($location) !== FALSE) {
$location['ip_address'] = NULL; // if successfully stored, don't store again
}
_ip_geoloc_set_session_value('location', $location);
}
/**
* Data recipient for javascript function getLocation().
*
* Comes in via menu callback /ip-geoloc-current-location, see function
* ip_geoloc_menu() above.
* Receives latitude, longitude, accuracy and address via the global $_POST
* variable from function getLocation() in ip_geoloc_current_location.js, which
* posts these through an AJAX call.
* @see ip_geoloc_current_location.js
*/
function ip_geoloc_current_location_ajax_recipient() {
$location = array('provider' => 'google', 'ip_address' => ip_address());
if (isset($_POST['error'])) {
// Device/browser does not support getCurrentPosition(), timeout, or Google reverse-geocode error.
$error = check_plain($_POST['error']) . ' -- ';
if (ip_geoloc_use_smart_ip_if_enabled($location) || ip_geoloc_use_geoip_api_if_enabled($location)) {
// In case of HTML5 error fill out street address details based on the IP.
// These reflect the internet provider location, not the browser location.
// Server-side call, so subject to a Google-imposed limit of 2500/day
// coming from the same IP address.
// Note: don't have to check for applicable roles here, as the AJAX call
// to which we respond wouldn't have been instigated in the first place.
if (!empty($location['latitude']) && !empty($location['longitude'])) {
if ($google_address = ip_geoloc_reverse_geocode($location['latitude'], $location['longitude'])) {
ip_geoloc_flatten_google_address($google_address, $location);
}
}
$error .= t('Fallback: %address', array(
'%address' => isset($location['formatted_address']) ? $location['formatted_address'] : ''));
}
else {
$error .= t('No fallback. Neither Smart IP nor GeoIP API are enabled.');
}
watchdog('IP Geolocation', $error, NULL, WATCHDOG_NOTICE);
ip_geoloc_debug('IP Geolocation, ' . $location['ip_address'] . ': ' . $error);
}
else {
// Flesh out $location with the returned street address components.
foreach ($_POST as $key => $value) {
$location[check_plain($key)] = check_plain($value);
}
$since = _ip_geoloc_get_session_value('position_pending_since');
$time_elapsed = $since ? number_format(microtime(TRUE) - $since, 1) : t('many, many');
watchdog('IP Geolocation', 'Browser @ %address, received after %sec seconds.', array('%address' => $location['formatted_address'], '%sec' => $time_elapsed), WATCHDOG_INFO);
ip_geoloc_debug(t('IP Geolocaton: global position and reverse-geocoding callback received after %sec seconds: !location',
array('%sec' => $time_elapsed, '!location' => ip_geoloc_pretty_print($location))));
}
_ip_geoloc_set_session_value('position_pending_since', NULL);
if (ip_geoloc_store_location($location) !== FALSE) {
$location['ip_address'] = NULL; // if successfully stored, don't store again
}
_ip_geoloc_set_session_value('location', $location);
}
/**
* Use Smart IP (if enabled) to retrieve lat/long and address info.
*
* Note that smart_ip_get_location() will invoke
* hook_smart_ip_get_location_alter($location), which we use to format the
* address.
*
* @param
* $location, if $location['ip_address'] isn't filled out the current user's
* IP address will be used
*/
function ip_geoloc_use_smart_ip_if_enabled(&$location) {
if (variable_get('ip_geoloc_smart_ip_as_backup', TRUE)) {
$location['provider'] = 'smart_ip';
if (module_exists('smart_ip')) {
if (empty($location['ip_address'])) {
$location['ip_address'] = ip_address();
}
$location = smart_ip_get_location($location['ip_address']); // see also: ip_geoloc_smart_ip_get_location_alter()
return TRUE;
}
ip_geoloc_debug(t('IP Geolocation: Smart IP configured as a backup, but is not enabled.'));
}
// $location['formatted_address'] = '';
return FALSE;
}
/**
* Module GeoIP API does not expose a hook, but it does expose an API.
*/
function ip_geoloc_use_geoip_api_if_enabled(&$location) {
if (!module_exists('geoip')) {
return FALSE;
}
$location['provider'] = 'geoip';
if (empty($location['ip_address'])) {
$location['ip_address'] = ip_address();
}
$geoip_location = (array) geoip_city($location['ip_address']);
if (!empty($geoip_location)) {
// Where different convert GeoIP names to our equivalents
$geoip_location['country'] = isset($geoip_location['country_name']) ? $geoip_location['country_name'] : '';
unset($geoip_location['country_name']);
$location = array_merge($geoip_location, $location);
ip_geoloc_format_address($location);
}
ip_geoloc_debug(t('IP Geolocation: GeoIP API retrieved: !location', array('!location' => ip_geoloc_pretty_print($location))));
return TRUE;
}
/**
* Return whether a the visitor's location is due for an update.
*
* Updates are only performed on selected configured pages.
* An update is due when more than a configurable number of seconds have
* elapsed. If that number is set to zero, then the user's location will be
* requested until at least the location's country is known, which is
* normally immediately at the start of the session.
*/
function _ip_geoloc_check_location($location = NULL) {
$path_alias = drupal_get_path_alias();
$include_pages = variable_get('ip_geoloc_include_pages', '*');
if (!drupal_match_path($path_alias, $include_pages)) {
return FALSE;
}
$exclude_pages = variable_get('ip_geoloc_exclude_pages', IP_GEOLOC_DEFAULT_PAGE_EXCLUSIONS);
if (drupal_match_path($path_alias, $exclude_pages)) {
return FALSE;
}
$interval = (int)variable_get('ip_geoloc_location_check_interval', IP_GEOLOC_LOCATION_CHECK_INTERVAL);
if ($interval == 0) {
return !isset($location['country']);
}
$last_position_check = _ip_geoloc_get_session_value('last_position_check');
if (isset($last_position_check)) {
$time_elapsed = time() - $last_position_check;
if ($time_elapsed < $interval) {
ip_geoloc_debug(t('IP Geolocation: next update in %seconds seconds (if not on excluded page).', array('%seconds' => $interval - $time_elapsed)));
return FALSE;
}
}
return TRUE;
}
/**
* Handle timeout of the Google Maps reverse-geocode callback, if enabled.
*
* This is based on $position_pending_since being set to the current time when
* the service was initiated.
*/
function ip_geoloc_reverse_geocode_timeout() {
$pending_since = _ip_geoloc_get_session_value('position_pending_since');
if (isset($pending_since)) {
$time_elapsed = microtime(TRUE) - $pending_since;
ip_geoloc_debug(t('IP Geolocation: location info now pending for %sec s.', array('%sec' => number_format($time_elapsed, 1))));
if ($time_elapsed > IP_GEOLOC_CALLBACK_TIMEOUT) {
return $time_elapsed;
}
}
return FALSE;
}
/**
* Poor man's address formatter.
*
* It doesn't take local format conventions into account. Luckily this is only
* called as a fallback when lat/long could not be established or the Google
* reverse-geocode function returned an error.
*
* @param
* location object
*/
function ip_geoloc_format_address(&$location) {
$location['formatted_address'] = isset($location['city']) ? $location['city'] : '';
if (!empty($location['region'])) {
$location['formatted_address'] .= ' ' . $location['region'];
}
if (!empty($location['postal_code']) && $location['postal_code'] != '-') {
$location['formatted_address'] .= ' ' . $location['postal_code'] . ',';
}
$location['formatted_address'] .= ' ' . $location['country'];
$location['formatted_address'] = trim($location['formatted_address']);
}
/**
* Fleshes out the $ip_geoloc_address array.
*
* This is based on the additional data provided in the $google_address array.
* This may involve tweaking of the 'latitude' and 'longitude' entries so that
* they remain consistent with the street address components.
*
* @param
* google_address
* @param
* ip_geoloc_address
* @return
* TRUE, unless google_address or ip_geoloc_address are empty
*/
function ip_geoloc_flatten_google_address($google_address, &$ip_geoloc_address) {
if (is_array($google_address) && is_array($google_address['address_components']) && is_array($ip_geoloc_address)) {
$ip_geoloc_address['provider'] = 'google';
foreach ($google_address['address_components'] as $component) {
$long_name = $component['long_name'];
if (!empty($long_name)) {
$type = $component['types'][0];
$ip_geoloc_address[$type] = $long_name;
if ($type == 'country' && !empty($component['short_name'])) {
$ip_geoloc_address['country_code'] = $component['short_name'];
}
}
}
$ip_geoloc_address['formatted_address'] = $google_address['formatted_address'];
// The following may be slightly different from the original lat,long passed
// into ip_geoloc_reverse_geocode().
$ip_geoloc_address['latitude'] = $google_address['geometry']['location']['lat'];
$ip_geoloc_address['longitude'] = $google_address['geometry']['location']['lng'];
return TRUE;
}
return FALSE;
}
function ip_geoloc_pretty_print($location) {
$t = '';
foreach ($location as $label => $value) {
if (!empty($value)) {
$t .= check_plain($label) . ": " . check_plain($value) . " ";
}
}
return empty($t) ? t('nothing') : $t;
}
/**
* Return available marker colors for use in a select drop-down.
*
* List is compiled based on available .png files in ip_geoloc/markers dir.
*
* @return array of color names indexed by machine names
*/
function ip_geoloc_marker_colors() {
$color_list = &drupal_static(__FUNCTION__);
if (!isset($color_list)) {
$color_list = array('' => '<' . t('default') . '>');
$marker_directory = variable_get('ip_geoloc_marker_directory', drupal_get_path('module', 'ip_geoloc') . '/markers');
if ($directory_handle = opendir($marker_directory)) {
while (($filename = readdir($directory_handle)) !== FALSE) {
if ($ext_pos = strrpos($filename, '.png')) {
$color = drupal_substr($filename, 0, $ext_pos);
$color_list[$color] = t($color); // ok... relies on translations done elsewhere
}
}
closedir($directory_handle);
}
asort($color_list);
}
return $color_list;
}
/**
* Return available OpenLayers marker layers for use in a select drop-down.
*
* @return array indexed by marker layer number (1..n)
*/
function ip_geoloc_openlayers_marker_layers() {
$num_location_marker_layers = variable_get('ip_geoloc_num_location_marker_layers', IP_GEOLOC_DEF_NUM_MARKER_LAYERS);
$marker_layers = array();
for ($layer = 1; $layer <= $num_location_marker_layers; $layer++) {
$marker_layers[$layer] = t('Marker layer') . " #$layer";
}
return $marker_layers;
}
function ip_geoloc_form_alter(&$form, &$form_state) {
// Append our own handler to deal with saving of the differentiator table
if (isset($form['#form_id']) && $form['#form_id'] == 'views_ui_edit_display_form' && isset($form['options']['style_options']['differentiator'])) {
$form['buttons']['submit']['#submit'][] = 'ip_geoloc_plugin_style_differentiator_color_associations_submit';
}
}
/**
* Implements hook_smart_ip_get_location_alter().
*
* Called from the bottom of smart_ip_get_location() when it has fleshed out
* the $location array as much as it can. Used here to format the address.
*/
function ip_geoloc_smart_ip_get_location_alter(&$location) {
if (empty($location['postal_code'])) {
$location['postal_code'] = $location['zip'];
}
ip_geoloc_format_address($location);
ip_geoloc_debug(t('IP Geolocation: Smart IP retrieved: !location', array('!location' => ip_geoloc_pretty_print($location))));
}
/**
* Implements hook_device_geolocation_detector_ajax_alter().
*
* This is called from device_geolocation_detector_ajax(), the AJAX callback
* that receives in the $_POST array the address data from Google geocoding.
* @obsolete
*/
function ip_geoloc_device_geolocation_detector_ajax_alter(&$location) {
ip_geoloc_debug(t('IP Geolocation: Device Geolocation retrieved: !location', array('!location' => ip_geoloc_pretty_print($location))));
}
/**
* Implements hook_leaflet_map_info_alter().
*/
function ip_geoloc_leaflet_map_info_alter(&$map_info) {
return;
}
/**
* Determines if a value is within the supplied numeric or alphabetical range.
*
* String comparison is based on the ASCII/UTF8 order, so is case-sensitive.
*
* @param string $value
* @param string $range, of the form '1.5--4.5' (range is inclusive of end points)
* @return bool
*/
function ip_geoloc_is_in_range($value, $range) {
if (!isset($value) || !isset($range)) {
return FALSE;
}
if (is_array($range)) { // defensive programming to make sure we have a string
$range = reset($range);
}
$from_to = explode(IP_GEOLOC_RANGE_SEPARATOR1, $range);
if (count($from_to) < 2) {
$from_to = explode(IP_GEOLOC_RANGE_SEPARATOR2, $range);
}
if (count($from_to) == 1) { // single value
return trim($value) == trim($range);
}
$from = trim($from_to[0]);
$to = trim($from_to[1]);
if ($from == '' && $to == '') { // range separator without values
return TRUE;
}
if ($from != '' && $to != '') {
return ($value >= $from) && ($value <= $to);
}
if ($from != '') {
return $value >= $from;
}
return $value <= $to;
}
/**
* FAPI validation of a range element.
*
* We want to cover both numeric and alphabetic ranges, but do not know the
* whether we're dealing with numbers or strings. So this validation is as
* strict as it can be, not knowing that information.
*/
function ip_geoloc_range_widget_validate($element, &$form_state) {
$range = $element['#value'];
$from_to = explode(IP_GEOLOC_RANGE_SEPARATOR1, $range);
if (count($from_to) < 2) {
$from_to = explode(IP_GEOLOC_RANGE_SEPARATOR2, $range);
}
if (count($from_to) < 2) {
// Not a range but a single value. This is ok. If we knew we were checking
// for a number we would pass the input through is_numeric(), but we don't.
/*
if (!is_numeric(trim($range))) {
form_error($element, t('Invalid number or range.'));
}
*/
}
else {
$from = trim($from_to[0]);
$to = trim($from_to[1]);
$ok = TRUE;
// If either $from or $to is numeric then assume numeric range and apply
// validation accordingly.
if (is_numeric($from) || is_numeric($to)) {
// If one end is numeric, then the other must also be, or be empty.
$ok =
(empty($from) && empty($to)) ||
(empty($from) && is_numeric($to)) || (empty($to) && is_numeric($from)) ||
(is_numeric($from) && is_numeric($to) && $from <= $to);
}
elseif (!empty($from) && !empty($to)) { // alphabetic range validation
$ok = ($from <= $to);
}
if (!$ok) {
form_error($element, t('Invalid range.'));
}
}
}
function ip_geoloc_debug($message, $type = 'status') {
global $user;
$user_names = explode(',', check_plain(variable_get('ip_geoloc_debug')));
foreach ($user_names as $user_name) {
$user_name = drupal_strtolower(trim($user_name));
$match = isset($user->name) ? $user_name == drupal_strtolower(trim($user->name)) : ($user_name == 'anon' || $user_name == 'anonymous');
if ($match) {
drupal_set_message($message, $type);
return;
}
}
}
/**
* Implements hook_ctools_plugin_directory().
*/
function ip_geoloc_ctools_plugin_directory($module, $plugin) {
if ($module == 'ctools' || $module == 'panels') {
return 'plugins/' . $plugin;
}
}
/**
* Implements hook_views_api().
*/
function ip_geoloc_views_api() {
return array(
'api' => views_api_version(),
'path' => drupal_get_path('module', 'ip_geoloc') . '/views'
);
}
/**
* Implements hook_statistics_api() from Better Statistics module.
*/
function ip_geoloc_statistics_api() {
return array(
'version' => 1,
'path' => drupal_get_path('module', 'ip_geoloc') . '/plugins',
'file' => 'ip_geoloc.statistics.inc',
);
}