array('GeoLiteCity.dat'), * 'isp' => array('GeoIP.dat', 'GeoIPISP.dat') * 'org' => array('GeoIPOrg.dat') * ) * If a key is missing (or the parameter not supplied), then the * default database names are used. */ public function __construct($customDbNames = false) { $this->customDbNames = parent::$dbNames; if ($customDbNames !== false) { foreach ($this->customDbNames as $key => $names) { if (isset($customDbNames[$key])) { $this->customDbNames[$key] = $customDbNames[$key]; } } } } /** * Closes all open geoip instances. */ public function __destruct() { foreach ($this->geoIpCache as $instance) { geoip_close($instance); } } /** * Uses a GeoIP database to get a visitor's location based on their IP address. * * This function will return different results based on the data used. If a city * database is used, it may return the country code, region code, city name, area * code, latitude, longitude and postal code of the visitor. * * Alternatively, if used with a country database, only the country code will be * returned. * * @param array $info Must have an 'ip' field. * @return array */ public function getLocation($info) { $ip = $this->getIpFromInfo($info); $isIPv6 = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6); $result = array(); $locationGeoIp = $this->getGeoIpInstance($key = 'loc'); if ($locationGeoIp) { switch ($locationGeoIp->databaseType) { case GEOIP_CITY_EDITION_REV0: // city database type case GEOIP_CITY_EDITION_REV1: case GEOIP_CITYCOMBINED_EDITION: if ($isIPv6) { $location = geoip_record_by_addr_v6($locationGeoIp, $ip); } else { $location = geoip_record_by_addr($locationGeoIp, $ip); } if (!empty($location)) { $result[self::COUNTRY_CODE_KEY] = $location->country_code; $result[self::REGION_CODE_KEY] = $location->region; $result[self::CITY_NAME_KEY] = utf8_encode($location->city); $result[self::AREA_CODE_KEY] = $location->area_code; $result[self::LATITUDE_KEY] = $location->latitude; $result[self::LONGITUDE_KEY] = $location->longitude; $result[self::POSTAL_CODE_KEY] = $location->postal_code; } break; case GEOIP_REGION_EDITION_REV0: // region database type case GEOIP_REGION_EDITION_REV1: if ($isIPv6) { // NOTE: geoip_region_by_addr_v6 does not exist (yet?), so we // return the country code and an empty region code $location = array(geoip_country_code_by_addr_v6($locationGeoIp, $ip), ''); } else { $location = geoip_region_by_addr($locationGeoIp, $ip); } if (!empty($location)) { $result[self::COUNTRY_CODE_KEY] = $location[0]; $result[self::REGION_CODE_KEY] = $location[1]; } break; case GEOIP_COUNTRY_EDITION: // country database type if ($isIPv6) { $result[self::COUNTRY_CODE_KEY] = geoip_country_code_by_addr_v6($locationGeoIp, $ip); } else { $result[self::COUNTRY_CODE_KEY] = geoip_country_code_by_addr($locationGeoIp, $ip); } break; default: // unknown database type, log warning and fallback to country edition Log::warning("Found unrecognized database type: %s", $locationGeoIp->databaseType); if ($isIPv6) { $result[self::COUNTRY_CODE_KEY] = geoip_country_code_by_addr_v6($locationGeoIp, $ip); } else { $result[self::COUNTRY_CODE_KEY] = geoip_country_code_by_addr($locationGeoIp, $ip); } break; } } // NOTE: ISP & ORG require commercial dbs to test. The code has been tested manually, // but not by system tests. $ispGeoIp = $this->getGeoIpInstance($key = 'isp'); if ($ispGeoIp) { if ($isIPv6) { $isp = geoip_name_by_addr_v6($ispGeoIp, $ip); } else { $isp = geoip_org_by_addr($ispGeoIp, $ip); } if (!empty($isp)) { $result[self::ISP_KEY] = utf8_encode($isp); } } $orgGeoIp = $this->getGeoIpInstance($key = 'org'); if ($orgGeoIp) { if ($isIPv6) { $org = geoip_name_by_addr_v6($orgGeoIp, $ip); } else { $org = geoip_org_by_addr($orgGeoIp, $ip); } if (!empty($org)) { $result[self::ORG_KEY] = utf8_encode($org); } } if (empty($result)) { return false; } $this->completeLocationResult($result); return $result; } /** * Returns true if this location provider is available. Piwik ships w/ the MaxMind * PHP library, so this provider is available if a location GeoIP database can be found. * * @return bool */ public function isAvailable() { $path = self::getPathToGeoIpDatabase($this->customDbNames['loc']); return $path !== false; } /** * Returns true if this provider has been setup correctly, the error message if * otherwise. * * @return bool|string */ public function isWorking() { if (!function_exists('mb_internal_encoding')) { return Piwik::translate('UserCountry_GeoIPCannotFindMbstringExtension', array('mb_internal_encoding', 'mbstring')); } $geoIpError = false; $catchGeoIpError = function ($errno, $errstr, $errfile, $errline) use (&$geoIpError) { $filename = basename($errfile); if ($filename == 'geoip.inc' || $filename == 'geoipcity.inc' ) { $geoIpError = array($errno, $errstr, $errfile, $errline); } else { throw new \Exception("Error in PHP GeoIP provider: $errstr on line $errline of $errfile"); // unexpected } }; // catch GeoIP errors set_error_handler($catchGeoIpError); $result = parent::isWorking(); restore_error_handler(); if ($geoIpError) { list($errno, $errstr, $errfile, $errline) = $geoIpError; Log::warning("Got GeoIP error when testing PHP GeoIP location provider: %s(%s): %s", $errfile, $errline, $errstr); return Piwik::translate('UserCountry_GeoIPIncorrectDatabaseFormat'); } return $result; } /** * Returns an array describing the types of location information this provider will * return. * * The location info this provider supports depends on what GeoIP databases it can * find. * * This provider will always support country & continent information. * * If a region database is found, then region code & name information will be * supported. * * If a city database is found, then region code, region name, city name, * area code, latitude, longitude & postal code are all supported. * * If an organization database is found, organization information is * supported. * * If an ISP database is found, ISP information is supported. * * @return array */ public function getSupportedLocationInfo() { $result = array(); // country & continent info always available $result[self::CONTINENT_CODE_KEY] = true; $result[self::CONTINENT_NAME_KEY] = true; $result[self::COUNTRY_CODE_KEY] = true; $result[self::COUNTRY_NAME_KEY] = true; $locationGeoIp = $this->getGeoIpInstance($key = 'loc'); if ($locationGeoIp) { switch ($locationGeoIp->databaseType) { case GEOIP_CITY_EDITION_REV0: // city database type case GEOIP_CITY_EDITION_REV1: case GEOIP_CITYCOMBINED_EDITION: $result[self::REGION_CODE_KEY] = true; $result[self::REGION_NAME_KEY] = true; $result[self::CITY_NAME_KEY] = true; $result[self::AREA_CODE_KEY] = true; $result[self::LATITUDE_KEY] = true; $result[self::LONGITUDE_KEY] = true; $result[self::POSTAL_CODE_KEY] = true; break; case GEOIP_REGION_EDITION_REV0: // region database type case GEOIP_REGION_EDITION_REV1: $result[self::REGION_CODE_KEY] = true; $result[self::REGION_NAME_KEY] = true; break; default: // country or unknown database type break; } } // check if isp info is available if ($this->getGeoIpInstance($key = 'isp')) { $result[self::ISP_KEY] = true; } // check of org info is available if ($this->getGeoIpInstance($key = 'org')) { $result[self::ORG_KEY] = true; } return $result; } /** * Returns information about this location provider. Contains an id, title & description: * * array( * 'id' => 'geoip_php', * 'title' => '...', * 'description' => '...' * ); * * @return array */ public function getInfo() { $desc = Piwik::translate('UserCountry_GeoIpLocationProviderDesc_Php1') . '

' . Piwik::translate('UserCountry_GeoIpLocationProviderDesc_Php2', array('', '', '', '')); $installDocs = '' . Piwik::translate('UserCountry_HowToInstallGeoIPDatabases') . ''; $availableDatabaseTypes = array(); if (self::getPathToGeoIpDatabase(array('GeoIPCity.dat', 'GeoLiteCity.dat')) !== false) { $availableDatabaseTypes[] = Piwik::translate('UserCountry_City'); } if (self::getPathToGeoIpDatabase(array('GeoIPRegion.dat')) !== false) { $availableDatabaseTypes[] = Piwik::translate('UserCountry_Region'); } if (self::getPathToGeoIpDatabase(array('GeoIPCountry.dat')) !== false) { $availableDatabaseTypes[] = Piwik::translate('UserCountry_Country'); } if (self::getPathToGeoIpDatabase(array('GeoIPISP.dat')) !== false) { $availableDatabaseTypes[] = 'ISP'; } if (self::getPathToGeoIpDatabase(array('GeoIPOrg.dat')) !== false) { $availableDatabaseTypes[] = Piwik::translate('UserCountry_Organization'); } if (!empty($availableDatabaseTypes)) { $extraMessage = '' . Piwik::translate('General_Note') . ': ' . Piwik::translate('UserCountry_GeoIPImplHasAccessTo') . ': ' . implode(', ', $availableDatabaseTypes) . '.'; } else { $extraMessage = '' . Piwik::translate('General_Note') . ': ' . Piwik::translate('UserCountry_GeoIPNoDatabaseFound') . ''; } return array('id' => self::ID, 'title' => self::TITLE, 'description' => $desc, 'install_docs' => $installDocs, 'extra_message' => $extraMessage, 'order' => 12); } /** * Returns a GeoIP instance. Creates it if necessary. * * @param string $key 'loc', 'isp' or 'org'. Determines the type of GeoIP database * to load. * @return object|false */ private function getGeoIpInstance($key) { if (empty($this->geoIpCache[$key])) { // make sure region names are loaded & saved first parent::getRegionNames(); require_once PIWIK_INCLUDE_PATH . '/libs/MaxMindGeoIP/geoipcity.inc'; $pathToDb = self::getPathToGeoIpDatabase($this->customDbNames[$key]); if ($pathToDb !== false) { $this->geoIpCache[$key] = geoip_open($pathToDb, GEOIP_STANDARD); // TODO support shared memory } } return empty($this->geoIpCache[$key]) ? false : $this->geoIpCache[$key]; } }