PageRenderTime 35ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 0ms

/plugins/UserCountry/LocationProvider.php

https://github.com/CodeYellowBV/piwik
PHP | 459 lines | 223 code | 47 blank | 189 comment | 41 complexity | bcdf71380986055763dfe529ce6fbca9 MD5 | raw file
Possible License(s): LGPL-3.0, JSON, MIT, GPL-3.0, LGPL-2.1, GPL-2.0, AGPL-1.0, BSD-2-Clause, BSD-3-Clause
  1. <?php
  2. /**
  3. * Piwik - free/libre analytics platform
  4. *
  5. * @link http://piwik.org
  6. * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  7. *
  8. */
  9. namespace Piwik\Plugins\UserCountry;
  10. use Exception;
  11. use Piwik\Common;
  12. use Piwik\IP;
  13. use Piwik\Option;
  14. use Piwik\Piwik;
  15. use Piwik\Plugins\UserCountry\LocationProvider\DefaultProvider;
  16. use Piwik\Tracker\Cache;
  17. use ReflectionClass;
  18. /**
  19. * @see plugins/UserCountry/LocationProvider/Default.php
  20. */
  21. require_once PIWIK_INCLUDE_PATH . '/plugins/UserCountry/LocationProvider/Default.php';
  22. /**
  23. * @see plugins/UserCountry/LocationProvider/GeoIp.php
  24. */
  25. require_once PIWIK_INCLUDE_PATH . '/plugins/UserCountry/LocationProvider/GeoIp.php';
  26. /**
  27. * The base class of all LocationProviders.
  28. *
  29. * LocationProviders attempt to determine a visitor's location using
  30. * visit information. All LocationProviders require a visitor's IP address, some
  31. * require more, such as the browser language.
  32. */
  33. abstract class LocationProvider
  34. {
  35. const NOT_INSTALLED = 0;
  36. const INSTALLED = 1;
  37. const BROKEN = 2;
  38. const CURRENT_PROVIDER_OPTION_NAME = 'usercountry.location_provider';
  39. const GEOGRAPHIC_COORD_PRECISION = 3;
  40. const CONTINENT_CODE_KEY = 'continent_code';
  41. const CONTINENT_NAME_KEY = 'continent_name';
  42. const COUNTRY_CODE_KEY = 'country_code';
  43. const COUNTRY_NAME_KEY = 'country_name';
  44. const REGION_CODE_KEY = 'region_code';
  45. const REGION_NAME_KEY = 'region_name';
  46. const CITY_NAME_KEY = 'city_name';
  47. const AREA_CODE_KEY = 'area_code';
  48. const LATITUDE_KEY = 'lat';
  49. const LONGITUDE_KEY = 'long';
  50. const POSTAL_CODE_KEY = 'postal_code';
  51. const ISP_KEY = 'isp';
  52. const ORG_KEY = 'org';
  53. /**
  54. * An array of all provider instances. Access it through static methods.
  55. *
  56. * @var array
  57. */
  58. public static $providers = null;
  59. /**
  60. * Returns location information based on visitor information.
  61. *
  62. * The result of this function will be an array. The array can store some or all of
  63. * the following information:
  64. *
  65. * - Continent Code: The code of the visitor's continent.
  66. * (array key is self::CONTINENT_CODE_KEY)
  67. * - Continent Name: The name of the visitor's continent.
  68. * (array key is self::CONTINENT_NAME_KEY)
  69. * - Country Code: The code of the visitor's country.
  70. * (array key is self::COUNTRY_CODE_KEY)
  71. * - Country Name: The name of the visitor's country.
  72. * (array key is self::COUNTRY_NAME_KEY)
  73. * - Region Code: The code of the visitor's region.
  74. * (array key is self::REGION_CODE_KEY)
  75. * - Region Name: The name of the visitor's region.
  76. * (array key is self::REGION_NAME_KEY)
  77. * - City Name: The name of the visitor's city.
  78. * (array key is self::CITY_NAME_KEY)
  79. * - Area Code: The visitor's area code.
  80. * (array key is self::AREA_CODE_KEY)
  81. * - Latitude: The visitor's latitude.
  82. * (array key is self::LATITUDE_KEY)
  83. * - Longitude: The visitor's longitude.
  84. * (array key is self::LONGITUDE_KEY)
  85. * - Postal Code: The visitor's postal code.
  86. * (array key is self::POSTAL_CODE_KEY)
  87. * - ISP: The visitor's ISP.
  88. * (array key is self::ISP_KEY)
  89. * - Org: The company/organization of the visitor's IP.
  90. * (array key is self::ORG_KEY)
  91. *
  92. * All LocationProviders will attempt to return the country of the visitor.
  93. *
  94. * @param array $info What this must contain depends on the specific provider
  95. * implementation. All providers require an 'ip' key mapped
  96. * to the visitor's IP address.
  97. * @return array|false
  98. */
  99. abstract public function getLocation($info);
  100. /**
  101. * Returns true if this provider is available for use, false if otherwise.
  102. *
  103. * @return bool
  104. */
  105. abstract public function isAvailable();
  106. /**
  107. * Returns true if this provider is working, false if otherwise.
  108. *
  109. * @return bool
  110. */
  111. abstract public function isWorking();
  112. /**
  113. * Returns an array mapping location result keys w/ bool values indicating whether
  114. * that information is supported by this provider. If it is not supported, that means
  115. * this provider either cannot get this information, or is not configured to get it.
  116. *
  117. * @return array eg. array(self::CONTINENT_CODE_KEY => true,
  118. * self::CONTINENT_NAME_KEY => true,
  119. * self::ORG_KEY => false)
  120. * The result is not guaranteed to have keys for every type of location
  121. * info.
  122. */
  123. abstract public function getSupportedLocationInfo();
  124. /**
  125. * Returns every available provider instance.
  126. *
  127. * @return LocationProvider[]
  128. */
  129. public static function getAllProviders()
  130. {
  131. if (is_null(self::$providers)) {
  132. self::$providers = array();
  133. foreach (get_declared_classes() as $klass) {
  134. if (is_subclass_of($klass, 'Piwik\Plugins\UserCountry\LocationProvider')) {
  135. $klassInfo = new ReflectionClass($klass);
  136. if ($klassInfo->isAbstract()) {
  137. continue;
  138. }
  139. self::$providers[] = new $klass;
  140. }
  141. }
  142. }
  143. return self::$providers;
  144. }
  145. /**
  146. * Returns all provider instances that are 'available'. An 'available' provider
  147. * is one that is available for use. They may not necessarily be working.
  148. *
  149. * @return array
  150. */
  151. public static function getAvailableProviders()
  152. {
  153. $result = array();
  154. foreach (self::getAllProviders() as $provider) {
  155. if ($provider->isAvailable()) {
  156. $result[] = $provider;
  157. }
  158. }
  159. return $result;
  160. }
  161. /**
  162. * Returns an array mapping provider IDs w/ information about the provider,
  163. * for each location provider.
  164. *
  165. * The following information is provided for each provider:
  166. * 'id' - The provider's unique string ID.
  167. * 'title' - The provider's title.
  168. * 'description' - A description of how the location provider works.
  169. * 'status' - Either self::NOT_INSTALLED, self::INSTALLED or self::BROKEN.
  170. * 'statusMessage' - If the status is self::BROKEN, then the message describes why.
  171. * 'location' - A pretty formatted location of the current IP address
  172. * (IP::getIpFromHeader()).
  173. *
  174. * An example result:
  175. * array(
  176. * 'geoip_php' => array('id' => 'geoip_php',
  177. * 'title' => '...',
  178. * 'desc' => '...',
  179. * 'status' => GeoIp::BROKEN,
  180. * 'statusMessage' => '...',
  181. * 'location' => '...')
  182. * 'geoip_serverbased' => array(...)
  183. * )
  184. *
  185. * @param string $newline What to separate lines with in the pretty locations.
  186. * @param bool $includeExtra Whether to include ISP/Org info in formatted location.
  187. * @return array
  188. */
  189. public static function getAllProviderInfo($newline = "\n", $includeExtra = false)
  190. {
  191. $allInfo = array();
  192. foreach (self::getAllProviders() as $provider) {
  193. $info = $provider->getInfo();
  194. $status = self::INSTALLED;
  195. $location = false;
  196. $statusMessage = false;
  197. $availableOrMessage = $provider->isAvailable();
  198. if ($availableOrMessage !== true) {
  199. $status = self::NOT_INSTALLED;
  200. if (is_string($availableOrMessage)) {
  201. $statusMessage = $availableOrMessage;
  202. }
  203. } else {
  204. $workingOrError = $provider->isWorking();
  205. if ($workingOrError === true) // if the implementation is configured correctly, get the location
  206. {
  207. $locInfo = array('ip' => IP::getIpFromHeader(),
  208. 'lang' => Common::getBrowserLanguage(),
  209. 'disable_fallbacks' => true);
  210. $location = $provider->getLocation($locInfo);
  211. $location = self::prettyFormatLocation($location, $newline, $includeExtra);
  212. } else // otherwise set an error message describing why
  213. {
  214. $status = self::BROKEN;
  215. $statusMessage = $workingOrError;
  216. }
  217. }
  218. $info['status'] = $status;
  219. $info['statusMessage'] = $statusMessage;
  220. $info['location'] = $location;
  221. $allInfo[$info['order']] = $info;
  222. }
  223. ksort($allInfo);
  224. $result = array();
  225. foreach ($allInfo as $info) {
  226. $result[$info['id']] = $info;
  227. }
  228. return $result;
  229. }
  230. /**
  231. * Returns the ID of the currently used location provider.
  232. *
  233. * The used provider is stored in the 'usercountry.location_provider' option.
  234. *
  235. * This function should not be called by the Tracker.
  236. *
  237. * @return string
  238. */
  239. public static function getCurrentProviderId()
  240. {
  241. $optionValue = Option::get(self::CURRENT_PROVIDER_OPTION_NAME);
  242. return $optionValue === false ? DefaultProvider::ID : $optionValue;
  243. }
  244. /**
  245. * Returns the provider instance of the current location provider.
  246. *
  247. * This function should not be called by the Tracker.
  248. *
  249. * @return \Piwik\Plugins\UserCountry\LocationProvider
  250. */
  251. public static function getCurrentProvider()
  252. {
  253. return self::getProviderById(self::getCurrentProviderId());
  254. }
  255. /**
  256. * Sets the provider to use when tracking.
  257. *
  258. * @param string $providerId The ID of the provider to use.
  259. * @return \Piwik\Plugins\UserCountry\LocationProvider The new current provider.
  260. * @throws Exception If the provider ID is invalid.
  261. */
  262. public static function setCurrentProvider($providerId)
  263. {
  264. $provider = self::getProviderById($providerId);
  265. if ($provider === false) {
  266. throw new Exception(
  267. "Invalid provider ID '$providerId'. The provider either does not exist or is not available");
  268. }
  269. Option::set(self::CURRENT_PROVIDER_OPTION_NAME, $providerId);
  270. Cache::clearCacheGeneral();
  271. return $provider;
  272. }
  273. /**
  274. * Returns a provider instance by ID or false if the ID is invalid or unavailable.
  275. *
  276. * @param string $providerId
  277. * @return \Piwik\Plugins\UserCountry\LocationProvider|false
  278. */
  279. public static function getProviderById($providerId)
  280. {
  281. foreach (self::getAvailableProviders() as $provider) {
  282. $info = $provider->getInfo();
  283. if ($info['id'] == $providerId) {
  284. return $provider;
  285. }
  286. }
  287. return false;
  288. }
  289. /**
  290. * Tries to fill in any missing information in a location result.
  291. *
  292. * This method will try to set the continent code, continent name and country code
  293. * using other information.
  294. *
  295. * Note: This function must always be called by location providers in getLocation.
  296. *
  297. * @param array $location The location information to modify.
  298. */
  299. public function completeLocationResult(&$location)
  300. {
  301. // fill in continent code if country code is present
  302. if (empty($location[self::CONTINENT_CODE_KEY])
  303. && !empty($location[self::COUNTRY_CODE_KEY])
  304. ) {
  305. $countryCode = strtolower($location[self::COUNTRY_CODE_KEY]);
  306. $location[self::CONTINENT_CODE_KEY] = Common::getContinent($countryCode);
  307. }
  308. // fill in continent name if continent code is present
  309. if (empty($location[self::CONTINENT_NAME_KEY])
  310. && !empty($location[self::CONTINENT_CODE_KEY])
  311. ) {
  312. $continentCode = strtolower($location[self::CONTINENT_CODE_KEY]);
  313. $location[self::CONTINENT_NAME_KEY] = Piwik::translate('UserCountry_continent_' . $continentCode);
  314. }
  315. // fill in country name if country code is present
  316. if (empty($location[self::COUNTRY_NAME_KEY])
  317. && !empty($location[self::COUNTRY_CODE_KEY])
  318. ) {
  319. $countryCode = strtolower($location[self::COUNTRY_CODE_KEY]);
  320. $location[self::COUNTRY_NAME_KEY] = Piwik::translate('UserCountry_country_' . $countryCode);
  321. }
  322. // deal w/ improper latitude/longitude & round proper values
  323. if (!empty($location[self::LATITUDE_KEY])) {
  324. if (is_numeric($location[self::LATITUDE_KEY])) {
  325. $location[self::LATITUDE_KEY] = round($location[self::LATITUDE_KEY], self::GEOGRAPHIC_COORD_PRECISION);
  326. } else {
  327. unset($location[self::LATITUDE_KEY]);
  328. }
  329. }
  330. if (!empty($location[self::LONGITUDE_KEY])) {
  331. if (is_numeric($location[self::LONGITUDE_KEY])) {
  332. $location[self::LONGITUDE_KEY] = round($location[self::LONGITUDE_KEY], self::GEOGRAPHIC_COORD_PRECISION);
  333. } else {
  334. unset($location[self::LONGITUDE_KEY]);
  335. }
  336. }
  337. }
  338. /**
  339. * Returns a prettified location result.
  340. *
  341. * @param array|false $locationInfo
  342. * @param string $newline The line separator (ie, \n or <br/>).
  343. * @param bool $includeExtra Whether to include ISP/Organization info.
  344. * @return string
  345. */
  346. public static function prettyFormatLocation($locationInfo, $newline = "\n", $includeExtra = false)
  347. {
  348. if ($locationInfo === false) {
  349. return Piwik::translate('General_Unknown');
  350. }
  351. // add latitude/longitude line
  352. $lines = array();
  353. if (!empty($locationInfo[self::LATITUDE_KEY])
  354. && !empty($locationInfo[self::LONGITUDE_KEY])
  355. ) {
  356. $lines[] = '(' . $locationInfo[self::LATITUDE_KEY] . ', ' . $locationInfo[self::LONGITUDE_KEY] . ')';
  357. }
  358. // add city/state line
  359. $cityState = array();
  360. if (!empty($locationInfo[self::CITY_NAME_KEY])) {
  361. $cityState[] = $locationInfo[self::CITY_NAME_KEY];
  362. }
  363. if (!empty($locationInfo[self::REGION_CODE_KEY])) {
  364. $cityState[] = $locationInfo[self::REGION_CODE_KEY];
  365. } else if (!empty($locationInfo[self::REGION_NAME_KEY])) {
  366. $cityState[] = $locationInfo[self::REGION_NAME_KEY];
  367. }
  368. if (!empty($cityState)) {
  369. $lines[] = implode(', ', $cityState);
  370. }
  371. // add postal code line
  372. if (!empty($locationInfo[self::POSTAL_CODE_KEY])) {
  373. $lines[] = $locationInfo[self::POSTAL_CODE_KEY];
  374. }
  375. // add country line
  376. if (!empty($locationInfo[self::COUNTRY_NAME_KEY])) {
  377. $lines[] = $locationInfo[self::COUNTRY_NAME_KEY];
  378. } else if (!empty($locationInfo[self::COUNTRY_CODE_KEY])) {
  379. $lines[] = $locationInfo[self::COUNTRY_CODE_KEY];
  380. }
  381. // add extra information (ISP/Organization)
  382. if ($includeExtra) {
  383. $lines[] = '';
  384. $unknown = Piwik::translate('General_Unknown');
  385. $org = !empty($locationInfo[self::ORG_KEY]) ? $locationInfo[self::ORG_KEY] : $unknown;
  386. $lines[] = "Org: $org";
  387. $isp = !empty($locationInfo[self::ISP_KEY]) ? $locationInfo[self::ISP_KEY] : $unknown;
  388. $lines[] = "ISP: $isp";
  389. }
  390. return implode($newline, $lines);
  391. }
  392. /**
  393. * Returns an IP address from an array that was passed into getLocation. This
  394. * will return an IPv4 address or false if the address is IPv6 (IPv6 is not
  395. * supported yet).
  396. *
  397. * @param array $info Must have 'ip' key.
  398. * @return string|bool
  399. */
  400. protected function getIpFromInfo($info)
  401. {
  402. $ip = $info['ip'];
  403. if (IP::isMappedIPv4($ip)) {
  404. return IP::getIPv4FromMappedIPv6($ip);
  405. } else if (IP::isIPv6($ip)) // IPv6 is not supported (yet)
  406. {
  407. return false;
  408. } else {
  409. return $ip;
  410. }
  411. }
  412. }