├── LICENSE ├── README.md ├── composer.json ├── config └── geoip.php ├── phpcs.xml ├── phpstan.neon ├── resources └── geoip.mmdb └── src ├── Cache.php ├── Console ├── Clear.php └── Update.php ├── Contracts └── ServiceInterface.php ├── Facades └── GeoIP.php ├── GeoIP.php ├── GeoIPServiceProvider.php ├── Location.php ├── Services ├── AbstractService.php ├── IPApi.php ├── IPData.php ├── IPFinder.php ├── IPGeoLocation.php ├── MaxMindDatabase.php └── MaxMindWebService.php ├── Support ├── Currencies.php └── HttpClient.php └── helpers.php /LICENSE: -------------------------------------------------------------------------------- 1 | The BSD 2-Clause License 2 | Copyright (c) 2013-2020, Daniel Stainback 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GeoIP for Laravel 2 | 3 | [![Latest Stable Version](https://poser.pugx.org/torann/geoip/v/stable)](https://packagist.org/packages/torann/geoip) 4 | [![Total Downloads](https://poser.pugx.org/torann/geoip/downloads)](https://packagist.org/packages/torann/geoip) 5 | [![Patreon donate button](https://img.shields.io/badge/patreon-donate-yellow.svg)](https://www.patreon.com/torann) 6 | [![Donate to this project using Paypal](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=4CJA2A97NPYVU) 7 | 8 | Determine the geographical location and currency of website visitors based on their IP addresses. 9 | 10 | - [GeoIP for Laravel on Packagist](https://packagist.org/packages/torann/geoip) 11 | - [GeoIP for Laravel on GitHub](https://github.com/Torann/laravel-geoip) 12 | - [Upgrade Guides](http://lyften.com/projects/laravel-geoip/doc/upgrade.html) 13 | 14 | ## v3.0.8 Possible Breaking Change 15 | 16 | Out of the box the system no longer comes with a service pre-configured. Which will cause an error for those who did **NOT** previously publish the configurtion file. Follow the [getting started guide](https://lyften.com/projects/laravel-geoip/doc/) on how to publish the config and how to set up a service. 17 | 18 | Sorry for this inconvenience, but MaxMind made me remove the free service that was the default. If the config file was published prior to this change, you should not been affected. 19 | 20 | ## Official Documentation 21 | 22 | Documentation for the package can be found on [Lyften.com](http://lyften.com/projects/laravel-geoip/). 23 | 24 | ## Contributions 25 | 26 | Many people have contributed to project since its inception. 27 | 28 | Thanks to: 29 | 30 | - [Dwight Watson](https://github.com/dwightwatson) 31 | - [nikkiii](https://github.com/nikkiii) 32 | - [jeffhennis](https://github.com/jeffhennis) 33 | - [max-kovpak](https://github.com/max-kovpak) 34 | - [dotpack](https://github.com/dotpack) 35 | - [Jess Archer](https://github.com/jessarcher) 36 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "torann/geoip", 3 | "description": "Support for multiple Geographical Location services.", 4 | "keywords": [ 5 | "laravel", 6 | "geoip", 7 | "geographical", 8 | "location", 9 | "geolocation", 10 | "IP API", 11 | "infoDB" 12 | ], 13 | "license": "BSD-2-Clause", 14 | "authors": [ 15 | { 16 | "name": "Daniel Stainback", 17 | "email": "torann@gmail.com" 18 | } 19 | ], 20 | "require": { 21 | "php": "^8.0|^8.1|^8.2|^8.3", 22 | "illuminate/cache": "^8.0|^9.0|^10.0|^11.0|^12.0", 23 | "illuminate/console": "^8.0|^9.0|^10.0|^11.0|^12.0", 24 | "illuminate/support": "^8.0|^9.0|^10.0|^11.0|^12.0" 25 | }, 26 | "suggest": { 27 | "geoip2/geoip2": "Required to use the MaxMind database or web service with GeoIP (~2.1).", 28 | "monolog/monolog": "Allows for storing location not found errors to the log" 29 | }, 30 | "require-dev": { 31 | "phpunit/phpunit": "^8.0|^9.0|^10.0|^11.0", 32 | "mockery/mockery": "^1.3", 33 | "geoip2/geoip2": "~2.1|~3.0", 34 | "vlucas/phpdotenv": "^5.0", 35 | "phpstan/phpstan": "^0.12.14|^1.9", 36 | "squizlabs/php_codesniffer": "^3.5" 37 | }, 38 | "autoload": { 39 | "files": [ 40 | "src/helpers.php" 41 | ], 42 | "psr-4": { 43 | "Torann\\GeoIP\\": "src/" 44 | } 45 | }, 46 | "autoload-dev": { 47 | "files": [ 48 | "tests/TestFunctions.php" 49 | ], 50 | "psr-4": { 51 | "Torann\\GeoIP\\Tests\\": "tests/" 52 | } 53 | }, 54 | "extra": { 55 | "branch-alias": { 56 | "dev-master": "1.0-dev" 57 | }, 58 | "laravel": { 59 | "providers": [ 60 | "Torann\\GeoIP\\GeoIPServiceProvider" 61 | ], 62 | "aliases": { 63 | "GeoIP": "Torann\\GeoIP\\Facades\\GeoIP" 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /config/geoip.php: -------------------------------------------------------------------------------- 1 | true, 16 | 17 | /* 18 | |-------------------------------------------------------------------------- 19 | | Include Currency in Results 20 | |-------------------------------------------------------------------------- 21 | | 22 | | When enabled the system will do it's best in deciding the user's currency 23 | | by matching their ISO code to a preset list of currencies. 24 | | 25 | */ 26 | 27 | 'include_currency' => true, 28 | 29 | /* 30 | |-------------------------------------------------------------------------- 31 | | Default Service 32 | |-------------------------------------------------------------------------- 33 | | 34 | | Here you may specify the default storage driver that should be used 35 | | by the framework using the services listed below. 36 | | 37 | */ 38 | 39 | 'service' => null, 40 | 41 | /* 42 | |-------------------------------------------------------------------------- 43 | | Storage Specific Configuration 44 | |-------------------------------------------------------------------------- 45 | | 46 | | Here you may configure as many storage drivers as you wish. 47 | | 48 | */ 49 | 50 | 'services' => [ 51 | 52 | 'maxmind_database' => [ 53 | 'class' => \Torann\GeoIP\Services\MaxMindDatabase::class, 54 | 'database_path' => storage_path('app/geoip.mmdb'), 55 | 'update_url' => sprintf('https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=%s&suffix=tar.gz', env('MAXMIND_LICENSE_KEY')), 56 | 'locales' => ['en'], 57 | ], 58 | 59 | 'maxmind_api' => [ 60 | 'class' => \Torann\GeoIP\Services\MaxMindWebService::class, 61 | 'user_id' => env('MAXMIND_USER_ID'), 62 | 'license_key' => env('MAXMIND_LICENSE_KEY'), 63 | 'locales' => ['en'], 64 | ], 65 | 66 | 'ipgeolocation' => [ 67 | 'class' => \Torann\GeoIP\Services\IPGeoLocation::class, 68 | 'secure' => true, 69 | 'key' => env('IPGEOLOCATION_KEY'), 70 | 'continent_path' => storage_path('app/continents.json'), 71 | 'lang' => 'en', 72 | ], 73 | 74 | 'ipdata' => [ 75 | 'class' => \Torann\GeoIP\Services\IPData::class, 76 | 'key' => env('IPDATA_API_KEY'), 77 | 'secure' => true, 78 | ], 79 | 80 | 'ipfinder' => [ 81 | 'class' => \Torann\GeoIP\Services\IPFinder::class, 82 | 'key' => env('IPFINDER_API_KEY'), 83 | 'secure' => true, 84 | 'locales' => ['en'], 85 | ], 86 | 87 | ], 88 | 89 | /* 90 | |-------------------------------------------------------------------------- 91 | | Default Cache Driver 92 | |-------------------------------------------------------------------------- 93 | | 94 | | Here you may specify the type of caching that should be used 95 | | by the package. 96 | | 97 | | Options: 98 | | 99 | | all - All location are cached 100 | | some - Cache only the requesting user 101 | | none - Disable cached 102 | | 103 | */ 104 | 105 | 'cache' => 'all', 106 | 107 | /* 108 | |-------------------------------------------------------------------------- 109 | | Cache Tags 110 | |-------------------------------------------------------------------------- 111 | | 112 | | Cache tags are not supported when using the file or database cache 113 | | drivers in Laravel. This is done so that only locations can be cleared. 114 | | 115 | */ 116 | 117 | 'cache_tags' => ['torann-geoip-location'], 118 | 119 | /* 120 | |-------------------------------------------------------------------------- 121 | | Cache Expiration 122 | |-------------------------------------------------------------------------- 123 | | 124 | | Define how long cached location are valid. 125 | | 126 | */ 127 | 128 | 'cache_expires' => 30, 129 | 130 | /* 131 | |-------------------------------------------------------------------------- 132 | | Default Location 133 | |-------------------------------------------------------------------------- 134 | | 135 | | Return when a location is not found. 136 | | 137 | */ 138 | 139 | 'default_location' => [ 140 | 'ip' => '127.0.0.0', 141 | 'iso_code' => 'US', 142 | 'country' => 'United States', 143 | 'city' => 'New Haven', 144 | 'state' => 'CT', 145 | 'state_name' => 'Connecticut', 146 | 'postal_code' => '06510', 147 | 'lat' => 41.31, 148 | 'lon' => -72.92, 149 | 'timezone' => 'America/New_York', 150 | 'continent' => 'NA', 151 | 'default' => true, 152 | 'currency' => 'USD', 153 | ], 154 | 155 | ]; 156 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | The PSR2 standard 4 | 5 | 6 | /resources/ 7 | /vendor/ 8 | /_[a-zA-Z0-9\._]+\.php 9 | /\.[a-zA-Z0-9\._]+\.php 10 | \.git 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | excludes_analyse: 3 | - vendor 4 | - resources 5 | ignoreErrors: 6 | - '#Function config_path not found#' 7 | - '#Function config not found#' 8 | - '#Instantiated class Monolog\\Logger not found#' 9 | - '#Instantiated class Monolog\\Handler\\StreamHandler not found#' 10 | - '#Access to constant ERROR on an unknown class Monolog\\Logger#' 11 | -------------------------------------------------------------------------------- /resources/geoip.mmdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Torann/laravel-geoip/1ea60c7e1a1608de3885e8cd76389cfe5c8424de/resources/geoip.mmdb -------------------------------------------------------------------------------- /src/Cache.php: -------------------------------------------------------------------------------- 1 | cache = $tags ? $cache->tags($tags) : $cache; 33 | $this->expires = $expires; 34 | } 35 | 36 | /** 37 | * Get an item from the cache. 38 | * 39 | * @param string $name 40 | * 41 | * @return Location|null 42 | */ 43 | public function get($name) 44 | { 45 | $value = $this->cache->get($name); 46 | 47 | return is_array($value) 48 | ? new Location($value) 49 | : null; 50 | } 51 | 52 | /** 53 | * Store an item in cache. 54 | * 55 | * @param string $name 56 | * @param Location $location 57 | * 58 | * @return bool 59 | */ 60 | public function set($name, Location $location) 61 | { 62 | return $this->cache->put($name, $location->toArray(), $this->expires); 63 | } 64 | 65 | /** 66 | * Flush cache for tags. 67 | * 68 | * @return bool 69 | */ 70 | public function flush() 71 | { 72 | return $this->cache->flush(); 73 | } 74 | } -------------------------------------------------------------------------------- /src/Console/Clear.php: -------------------------------------------------------------------------------- 1 | fire(); 31 | } 32 | 33 | /** 34 | * Execute the console command. 35 | * 36 | * @return void 37 | */ 38 | public function fire() 39 | { 40 | if ($this->isSupported() === false) { 41 | return $this->output->error('Default cache system does not support tags'); 42 | } 43 | 44 | $this->performFlush(); 45 | } 46 | 47 | /** 48 | * Is cache flushing supported. 49 | * 50 | * @return bool 51 | */ 52 | protected function isSupported() 53 | { 54 | return empty(app('geoip')->config('cache_tags')) === false 55 | && in_array(config('cache.default'), ['file', 'database']) === false; 56 | } 57 | 58 | /** 59 | * Flush the cache. 60 | * 61 | * @return void 62 | */ 63 | protected function performFlush() 64 | { 65 | $this->output->write("Clearing cache..."); 66 | 67 | app('geoip')->getCache()->flush(); 68 | 69 | $this->output->writeln("complete"); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Console/Update.php: -------------------------------------------------------------------------------- 1 | fire(); 31 | } 32 | 33 | /** 34 | * Execute the console command. 35 | * 36 | * @return void 37 | */ 38 | public function fire() 39 | { 40 | // Get default service 41 | $service = app('geoip')->getService(); 42 | 43 | // Ensure the selected service supports updating 44 | if (method_exists($service, 'update') === false) { 45 | $this->info('The current service "' . get_class($service) . '" does not support updating.'); 46 | 47 | return; 48 | } 49 | 50 | $this->comment('Updating...'); 51 | 52 | // Perform update 53 | if ($result = $service->update()) { 54 | $this->info($result); 55 | } 56 | else { 57 | $this->error('Update failed!'); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Contracts/ServiceInterface.php: -------------------------------------------------------------------------------- 1 | '127.0.0.0', 62 | 'iso_code' => 'US', 63 | 'country' => 'United States', 64 | 'city' => 'New Haven', 65 | 'state' => 'CT', 66 | 'state_name' => 'Connecticut', 67 | 'postal_code' => '06510', 68 | 'lat' => 41.31, 69 | 'lon' => -72.92, 70 | 'timezone' => 'America/New_York', 71 | 'continent' => 'NA', 72 | 'currency' => 'USD', 73 | 'default' => true, 74 | 'cached' => false, 75 | ]; 76 | 77 | /** 78 | * Create a new GeoIP instance. 79 | * 80 | * @param array $config 81 | * @param CacheManager $cache 82 | */ 83 | public function __construct(array $config, CacheManager $cache) 84 | { 85 | $this->config = $config; 86 | 87 | // Create caching instance 88 | $this->cache = new Cache( 89 | $cache, 90 | $this->config('cache_tags'), 91 | $this->config('cache_expires', 30) 92 | ); 93 | 94 | // Set custom default location 95 | $this->default_location = array_merge( 96 | $this->default_location, 97 | $this->config('default_location', []) 98 | ); 99 | 100 | // Set IP 101 | $this->remote_ip = $this->default_location['ip'] = $this->getClientIP(); 102 | } 103 | 104 | /** 105 | * Get the location from the provided IP. 106 | * 107 | * @param string $ip 108 | * 109 | * @return \Torann\GeoIP\Location 110 | * @throws \Exception 111 | */ 112 | public function getLocation($ip = null) 113 | { 114 | // Get location data 115 | $this->location = $this->find($ip); 116 | 117 | // Should cache location 118 | if ($this->shouldCache($this->location, $ip)) { 119 | $this->getCache()->set($ip, $this->location); 120 | } 121 | 122 | return $this->location; 123 | } 124 | 125 | /** 126 | * Find location from IP. 127 | * 128 | * @param string $ip 129 | * 130 | * @return \Torann\GeoIP\Location 131 | * @throws \Exception 132 | */ 133 | private function find($ip = null) 134 | { 135 | // If IP not set, user remote IP 136 | $ip = $ip ?: $this->remote_ip; 137 | 138 | // Check cache for location 139 | if ($this->config('cache', 'none') !== 'none' && $location = $this->getCache()->get($ip)) { 140 | $location->cached = true; 141 | 142 | return $location; 143 | } 144 | 145 | // Check if the ip is not local or empty 146 | if ($this->isValid($ip)) { 147 | try { 148 | // Find location 149 | $location = $this->getService()->locate($ip); 150 | 151 | // Set currency if not already set by the service 152 | if (! $location->currency) { 153 | $location->currency = $this->getCurrency($location->iso_code); 154 | } 155 | 156 | // Set default 157 | $location->default = false; 158 | 159 | return $location; 160 | } catch (\Exception $e) { 161 | if ($this->config('log_failures', true) === true) { 162 | $log = new Logger('geoip'); 163 | $log->pushHandler(new StreamHandler(storage_path('logs/geoip.log'), Logger::ERROR)); 164 | $log->error($e); 165 | } 166 | } 167 | } 168 | 169 | return $this->getService()->hydrate($this->default_location); 170 | } 171 | 172 | /** 173 | * Get the currency code from ISO. 174 | * 175 | * @param string $iso 176 | * 177 | * @return string 178 | */ 179 | public function getCurrency($iso) 180 | { 181 | if ($this->currencies === null && $this->config('include_currency', false)) { 182 | $this->currencies = include(__DIR__ . '/Support/Currencies.php'); 183 | } 184 | 185 | return Arr::get($this->currencies, $iso); 186 | } 187 | 188 | /** 189 | * Get service instance. 190 | * 191 | * @return \Torann\GeoIP\Contracts\ServiceInterface 192 | * @throws Exception 193 | */ 194 | public function getService() 195 | { 196 | if ($this->service === null) { 197 | // Get service configuration 198 | $config = $this->config('services.' . $this->config('service'), []); 199 | 200 | // Get service class 201 | $class = Arr::pull($config, 'class'); 202 | 203 | // Sanity check 204 | if ($class === null) { 205 | throw new Exception('No GeoIP service is configured.'); 206 | } 207 | 208 | // Create service instance 209 | $this->service = new $class($config); 210 | } 211 | 212 | return $this->service; 213 | } 214 | 215 | /** 216 | * Get cache instance. 217 | * 218 | * @return \Torann\GeoIP\Cache 219 | */ 220 | public function getCache() 221 | { 222 | return $this->cache; 223 | } 224 | 225 | /** 226 | * Get the client IP address. 227 | * 228 | * @return string 229 | */ 230 | public function getClientIP() 231 | { 232 | $remotes_keys = [ 233 | 'HTTP_X_FORWARDED_FOR', 234 | 'HTTP_CLIENT_IP', 235 | 'HTTP_X_REAL_IP', 236 | 'HTTP_X_FORWARDED', 237 | 'HTTP_FORWARDED_FOR', 238 | 'HTTP_FORWARDED', 239 | 'REMOTE_ADDR', 240 | 'HTTP_X_FORWARDED_IP', 241 | 'HTTP_X_CLUSTER_CLIENT_IP', 242 | ]; 243 | 244 | foreach ($remotes_keys as $key) { 245 | if ($address = getenv($key)) { 246 | foreach (explode(',', $address) as $ip) { 247 | if ($this->isValid($ip)) { 248 | return $ip; 249 | } 250 | } 251 | } 252 | } 253 | 254 | return '127.0.0.0'; 255 | } 256 | 257 | /** 258 | * Checks if the ip is valid. 259 | * 260 | * @param string $ip 261 | * 262 | * @return bool 263 | */ 264 | private function isValid($ip) 265 | { 266 | if (! filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) 267 | && ! filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 | FILTER_FLAG_NO_PRIV_RANGE) 268 | ) { 269 | return false; 270 | } 271 | 272 | return true; 273 | } 274 | 275 | /** 276 | * Determine if the location should be cached. 277 | * 278 | * @param Location $location 279 | * @param string|null $ip 280 | * 281 | * @return bool 282 | */ 283 | private function shouldCache(Location $location, $ip = null) 284 | { 285 | if ($location->default === true || $location->cached === true) { 286 | return false; 287 | } 288 | 289 | switch ($this->config('cache', 'none')) { 290 | case 'all': 291 | case 'some' && $ip === null: 292 | return true; 293 | } 294 | 295 | return false; 296 | } 297 | 298 | /** 299 | * Get configuration value. 300 | * 301 | * @param string $key 302 | * @param mixed $default 303 | * 304 | * @return mixed 305 | */ 306 | public function config($key, $default = null) 307 | { 308 | return Arr::get($this->config, $key, $default); 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /src/GeoIPServiceProvider.php: -------------------------------------------------------------------------------- 1 | registerGeoIpService(); 18 | 19 | if ($this->app->runningInConsole()) { 20 | $this->registerResources(); 21 | $this->registerGeoIpCommands(); 22 | } 23 | 24 | if ($this->isLumen() === false) { 25 | $this->mergeConfigFrom(__DIR__ . '/../config/geoip.php', 'geoip'); 26 | } 27 | } 28 | 29 | /** 30 | * Register currency provider. 31 | * 32 | * @return void 33 | */ 34 | public function registerGeoIpService() 35 | { 36 | $this->app->singleton('geoip', function ($app) { 37 | return new GeoIP( 38 | $app->config->get('geoip', []), 39 | $app['cache'] 40 | ); 41 | }); 42 | } 43 | 44 | /** 45 | * Register resources. 46 | * 47 | * @return void 48 | */ 49 | public function registerResources() 50 | { 51 | if ($this->isLumen() === false) { 52 | $this->publishes([ 53 | __DIR__ . '/../config/geoip.php' => config_path('geoip.php'), 54 | ], 'config'); 55 | } 56 | } 57 | 58 | /** 59 | * Register commands. 60 | * 61 | * @return void 62 | */ 63 | public function registerGeoIpCommands() 64 | { 65 | $this->commands([ 66 | Console\Update::class, 67 | Console\Clear::class, 68 | ]); 69 | } 70 | 71 | /** 72 | * Check if package is running under Lumen app 73 | * 74 | * @return bool 75 | */ 76 | protected function isLumen() 77 | { 78 | return Str::contains($this->app->version(), 'Lumen') === true; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Location.php: -------------------------------------------------------------------------------- 1 | attributes = $attributes; 46 | } 47 | 48 | /** 49 | * Determine if the location is for the same IP address. 50 | * 51 | * @param string $ip 52 | * 53 | * @return bool 54 | */ 55 | public function same($ip) 56 | { 57 | return $this->getAttribute('ip') == $ip; 58 | } 59 | 60 | /** 61 | * Set a given attribute on the location. 62 | * 63 | * @param string $key 64 | * @param mixed $value 65 | * 66 | * @return $this 67 | */ 68 | public function setAttribute($key, $value) 69 | { 70 | $this->attributes[$key] = $value; 71 | 72 | return $this; 73 | } 74 | 75 | /** 76 | * Get an attribute from the $attributes array. 77 | * 78 | * @param string $key 79 | * 80 | * @return mixed 81 | */ 82 | public function getAttribute($key) 83 | { 84 | $value = Arr::get($this->attributes, $key); 85 | 86 | $method = 'get' . Str::studly($key) . 'Attribute'; 87 | 88 | // First we will check for the presence of a mutator for the set operation 89 | // which simply lets the developers tweak the attribute as it is set. 90 | if (method_exists($this, $method)) { 91 | return $this->{$method}($value); 92 | } 93 | 94 | return $value; 95 | } 96 | 97 | /** 98 | * Return the display name of the location. 99 | * 100 | * @return string 101 | */ 102 | public function getDisplayNameAttribute() 103 | { 104 | return preg_replace('/^,\s/', '', "{$this->city}, {$this->state}"); 105 | } 106 | 107 | /** 108 | * Is the location the default. 109 | * 110 | * @return bool 111 | */ 112 | public function getDefaultAttribute($value) 113 | { 114 | return is_null($value) ? false : $value; 115 | } 116 | 117 | /** 118 | * Get the instance as an array. 119 | * 120 | * @return array 121 | */ 122 | public function toArray() 123 | { 124 | return $this->attributes; 125 | } 126 | 127 | /** 128 | * Get the location's attribute 129 | * 130 | * @param string $key 131 | * 132 | * @return mixed 133 | */ 134 | public function __get($key) 135 | { 136 | return $this->getAttribute($key); 137 | } 138 | 139 | /** 140 | * Set the location's attribute 141 | * 142 | * @param string $key 143 | * @param mixed $value 144 | */ 145 | public function __set($key, $value) 146 | { 147 | $this->setAttribute($key, $value); 148 | } 149 | 150 | /** 151 | * Determine if the given attribute exists. 152 | * 153 | * @param mixed $offset 154 | * 155 | * @return bool 156 | */ 157 | public function offsetExists(mixed $offset): bool 158 | { 159 | return isset($this->$offset); 160 | } 161 | 162 | /** 163 | * Get the value for a given offset. 164 | * 165 | * @param mixed $offset 166 | * 167 | * @return mixed 168 | */ 169 | public function offsetGet(mixed $offset): mixed 170 | { 171 | return $this->$offset; 172 | } 173 | 174 | /** 175 | * Set the value for a given offset. 176 | * 177 | * @param mixed $offset 178 | * @param mixed $value 179 | * 180 | * @return void 181 | */ 182 | public function offsetSet(mixed $offset, mixed $value): void 183 | { 184 | $this->$offset = $value; 185 | } 186 | 187 | /** 188 | * Unset the value for a given offset. 189 | * 190 | * @param mixed $offset 191 | * 192 | * @return void 193 | */ 194 | public function offsetUnset(mixed $offset): void 195 | { 196 | unset($this->$offset); 197 | } 198 | 199 | /** 200 | * Check if the location's attribute is set 201 | * 202 | * @param $key 203 | * 204 | * @return bool 205 | */ 206 | public function __isset($key) 207 | { 208 | return array_key_exists($key, $this->attributes); 209 | } 210 | 211 | /** 212 | * Unset an attribute on the location. 213 | * 214 | * @param string $key 215 | * 216 | * @return void 217 | */ 218 | public function __unset($key) 219 | { 220 | unset($this->attributes[$key]); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/Services/AbstractService.php: -------------------------------------------------------------------------------- 1 | config = $config; 26 | 27 | $this->boot(); 28 | } 29 | 30 | /** 31 | * The "booting" method of the service. 32 | * 33 | * @return void 34 | */ 35 | public function boot() 36 | { 37 | // 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | public function hydrate(array $attributes = []) 44 | { 45 | return new Location($attributes); 46 | } 47 | 48 | /** 49 | * Get configuration value. 50 | * 51 | * @param string $key 52 | * @param mixed $default 53 | * 54 | * @return mixed 55 | */ 56 | public function config($key, $default = null) 57 | { 58 | return Arr::get($this->config, $key, $default); 59 | } 60 | } -------------------------------------------------------------------------------- /src/Services/IPApi.php: -------------------------------------------------------------------------------- 1 | 'http://ip-api.com/', 34 | 'headers' => [ 35 | 'User-Agent' => 'Laravel-GeoIP', 36 | ], 37 | 'query' => [ 38 | 'fields' => 49663, 39 | 'lang' => $this->config('lang', ['en']), 40 | ], 41 | ]; 42 | 43 | // Using the Pro service 44 | if ($this->config('key')) { 45 | $base['base_uri'] = ($this->config('secure') ? 'https' : 'http') . '://pro.ip-api.com/'; 46 | $base['query']['key'] = $this->config('key'); 47 | } 48 | 49 | $this->client = new HttpClient($base); 50 | 51 | // Set continents 52 | if (file_exists($this->config('continent_path'))) { 53 | $this->continents = json_decode(file_get_contents($this->config('continent_path')), true); 54 | } 55 | } 56 | 57 | /** 58 | * {@inheritdoc} 59 | */ 60 | public function locate($ip) 61 | { 62 | // Get data from client 63 | $data = $this->client->get('json/' . $ip); 64 | 65 | // Verify server response 66 | if ($this->client->getErrors() !== null) { 67 | throw new Exception('Request failed (' . $this->client->getErrors() . ')'); 68 | } 69 | 70 | // Parse body content 71 | $json = json_decode($data[0]); 72 | 73 | // Verify response status 74 | if ($json->status !== 'success') { 75 | throw new Exception('Request failed (' . $json->message . ')'); 76 | } 77 | 78 | return $this->hydrate([ 79 | 'ip' => $ip, 80 | 'iso_code' => $json->countryCode, 81 | 'country' => $json->country, 82 | 'city' => $json->city, 83 | 'state' => $json->region, 84 | 'state_name' => $json->regionName, 85 | 'postal_code' => $json->zip, 86 | 'lat' => $json->lat, 87 | 'lon' => $json->lon, 88 | 'timezone' => $json->timezone, 89 | 'continent' => $this->getContinent($json->countryCode), 90 | ]); 91 | } 92 | 93 | /** 94 | * Update function for service. 95 | * 96 | * @return string 97 | * @throws Exception 98 | */ 99 | public function update() 100 | { 101 | $data = $this->client->get('https://dev.maxmind.com/static/csv/codes/country_continent.csv'); 102 | 103 | // Verify server response 104 | if ($this->client->getErrors() !== null) { 105 | throw new Exception($this->client->getErrors()); 106 | } 107 | 108 | $lines = explode("\n", $data[0]); 109 | 110 | array_shift($lines); 111 | 112 | $output = []; 113 | 114 | foreach ($lines as $line) { 115 | $arr = str_getcsv($line); 116 | 117 | if (count($arr) < 2) { 118 | continue; 119 | } 120 | 121 | $output[$arr[0]] = $arr[1]; 122 | } 123 | 124 | // Get path 125 | $path = $this->config('continent_path'); 126 | 127 | file_put_contents($path, json_encode($output)); 128 | 129 | return "Continent file ({$path}) updated."; 130 | } 131 | 132 | /** 133 | * Get continent based on country code. 134 | * 135 | * @param string $code 136 | * 137 | * @return string 138 | */ 139 | private function getContinent($code) 140 | { 141 | return Arr::get($this->continents, $code, 'Unknown'); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/Services/IPData.php: -------------------------------------------------------------------------------- 1 | client = new HttpClient([ 31 | 'base_uri' => 'https://api.ipdata.co/', 32 | 'query' => [ 33 | 'api-key' => $this->config('key'), 34 | ], 35 | ]); 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | * 41 | * @throws Exception 42 | */ 43 | public function locate($ip) 44 | { 45 | // Get data from client 46 | $data = $this->client->get($ip); 47 | 48 | // Verify server response 49 | if ($this->client->getErrors() !== null || empty($data[0])) { 50 | throw new Exception('Request failed (' . $this->client->getErrors() . ')'); 51 | } 52 | 53 | $json = json_decode($data[0], true); 54 | 55 | return $this->hydrate([ 56 | 'ip' => $ip, 57 | 'iso_code' => $json['country_code'], 58 | 'country' => $json['country_name'], 59 | 'city' => $json['city'], 60 | 'state' => $json['region_code'], 61 | 'state_name' => $json['region'], 62 | 'postal_code' => $json['postal'], 63 | 'lat' => $json['latitude'], 64 | 'lon' => $json['longitude'], 65 | 'timezone' => Arr::get($json, 'time_zone.name'), 66 | 'continent' => Arr::get($json, 'continent_code'), 67 | 'currency' => Arr::get($json, 'currency.code'), 68 | ]); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Services/IPFinder.php: -------------------------------------------------------------------------------- 1 | client = new HttpClient([ 31 | 'base_uri' => 'https://api.ipfinder.io/v1/', 32 | 'headers' => [ 33 | 'User-Agent' => 'Laravel-GeoIP-Torann', 34 | ], 35 | 'query' => [ 36 | 'token' => $this->config('key'), 37 | ], 38 | ]); 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | * 44 | * @throws Exception 45 | */ 46 | public function locate($ip) 47 | { 48 | // Get data from client 49 | $data = $this->client->get($ip); 50 | 51 | // Verify server response 52 | if ($this->client->getErrors() !== null || empty($data[0])) { 53 | throw new Exception('Request failed (' . $this->client->getErrors() . ')'); 54 | } 55 | 56 | $json = json_decode($data[0], true); 57 | 58 | return $this->hydrate($json); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Services/IPGeoLocation.php: -------------------------------------------------------------------------------- 1 | 'https://api.ipgeolocation.io/', 27 | ]; 28 | 29 | if ($this->config('key')) { 30 | $base['base_uri'] = "{$base['base_uri']}ipgeo?apiKey=" . $this->config('key'); 31 | } 32 | 33 | $this->client = new HttpClient($base); 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | 40 | public function locate($ip) 41 | { 42 | // Get data from client 43 | $data = $this->client->get('&ip=' . $ip); 44 | 45 | // Verify server response 46 | if ($this->client->getErrors() !== null) { 47 | throw new Exception('Request failed (' . $this->client->getErrors() . ')'); 48 | } 49 | 50 | // Parse body content 51 | $json = json_decode($data[0], true); 52 | 53 | return $this->hydrate($json); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Services/MaxMindDatabase.php: -------------------------------------------------------------------------------- 1 | config('database_path'); 28 | 29 | // Copy test database for now 30 | if (is_file($path) === false) { 31 | @mkdir(dirname($path)); 32 | 33 | copy(__DIR__ . '/../../resources/geoip.mmdb', $path); 34 | } 35 | 36 | $this->reader = new Reader( 37 | $path, $this->config('locales', ['en']) 38 | ); 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | public function locate($ip) 45 | { 46 | $record = $this->reader->city($ip); 47 | 48 | return $this->hydrate([ 49 | 'ip' => $ip, 50 | 'iso_code' => $record->country->isoCode, 51 | 'country' => $record->country->name, 52 | 'city' => $record->city->name, 53 | 'state' => $record->mostSpecificSubdivision->isoCode, 54 | 'state_name' => $record->mostSpecificSubdivision->name, 55 | 'postal_code' => $record->postal->code, 56 | 'lat' => $record->location->latitude, 57 | 'lon' => $record->location->longitude, 58 | 'timezone' => $record->location->timeZone, 59 | 'continent' => $record->continent->code, 60 | 'localizations' => $this->getLocalizations($record), 61 | ]); 62 | } 63 | 64 | /** 65 | * Update function for service. 66 | * 67 | * @return string 68 | * @throws Exception 69 | */ 70 | public function update() 71 | { 72 | if ($this->config('database_path', false) === false) { 73 | throw new Exception('Database path not set in config file.'); 74 | } 75 | 76 | $this->withTemporaryDirectory(function ($directory) { 77 | $tarFile = sprintf('%s/maxmind.tar.gz', $directory); 78 | 79 | file_put_contents($tarFile, fopen($this->config('update_url'), 'r')); 80 | 81 | $archive = new PharData($tarFile); 82 | 83 | $file = $this->findDatabaseFile($archive); 84 | 85 | $relativePath = "{$archive->getFilename()}/{$file->getFilename()}"; 86 | 87 | $archive->extractTo($directory, $relativePath); 88 | 89 | file_put_contents($this->config('database_path'), fopen("{$directory}/{$relativePath}", 'r')); 90 | }); 91 | 92 | return "Database file ({$this->config('database_path')}) updated."; 93 | } 94 | 95 | /** 96 | * Provide a temporary directory to perform operations in and and ensure 97 | * it is removed afterwards. 98 | * 99 | * @param callable $callback 100 | * 101 | * @return void 102 | */ 103 | protected function withTemporaryDirectory(callable $callback) 104 | { 105 | $directory = tempnam(sys_get_temp_dir(), 'maxmind'); 106 | 107 | if (file_exists($directory)) { 108 | unlink($directory); 109 | } 110 | 111 | mkdir($directory); 112 | 113 | try { 114 | $callback($directory); 115 | } finally { 116 | $this->deleteDirectory($directory); 117 | } 118 | } 119 | 120 | /** 121 | * Recursively search the given archive to find the .mmdb file. 122 | * 123 | * @param \PharData $archive 124 | * 125 | * @return mixed 126 | * @throws \Exception 127 | */ 128 | protected function findDatabaseFile($archive) 129 | { 130 | foreach ($archive as $file) { 131 | if ($file->isDir()) { 132 | return $this->findDatabaseFile(new PharData($file->getPathName())); 133 | } 134 | 135 | if (pathinfo($file, PATHINFO_EXTENSION) === 'mmdb') { 136 | return $file; 137 | } 138 | } 139 | 140 | throw new Exception('Database file could not be found within archive.'); 141 | } 142 | 143 | /** 144 | * Recursively delete the given directory. 145 | * 146 | * @param string $directory 147 | * 148 | * @return mixed 149 | */ 150 | protected function deleteDirectory(string $directory) 151 | { 152 | if (! file_exists($directory)) { 153 | return true; 154 | } 155 | 156 | if (! is_dir($directory)) { 157 | return unlink($directory); 158 | } 159 | 160 | foreach (scandir($directory) as $item) { 161 | if ($item == '.' || $item == '..') { 162 | continue; 163 | } 164 | 165 | if (! $this->deleteDirectory($directory . DIRECTORY_SEPARATOR . $item)) { 166 | return false; 167 | } 168 | } 169 | 170 | return rmdir($directory); 171 | } 172 | 173 | /** 174 | * Get localized country name, state name and city name based on config languages 175 | * 176 | * @param City $record 177 | * @return array 178 | */ 179 | private function getLocalizations(City $record): array 180 | { 181 | $localizations = []; 182 | 183 | foreach ($this->config('locales', ['en']) as $lang) { 184 | $localizations[$lang]['country'] = Arr::get($record->country->names, $lang); 185 | $localizations[$lang]['state_name'] = Arr::get($record->mostSpecificSubdivision->names, $lang); 186 | $localizations[$lang]['city'] = Arr::get($record->city->names, $lang); 187 | } 188 | 189 | return $localizations; 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/Services/MaxMindWebService.php: -------------------------------------------------------------------------------- 1 | client = new Client( 26 | $this->config('user_id'), 27 | $this->config('license_key'), 28 | $this->config('locales', ['en']) 29 | ); 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public function locate($ip) 36 | { 37 | $record = $this->client->city($ip); 38 | 39 | return $this->hydrate([ 40 | 'ip' => $ip, 41 | 'iso_code' => $record->country->isoCode, 42 | 'country' => $record->country->name, 43 | 'city' => $record->city->name, 44 | 'state' => $record->mostSpecificSubdivision->isoCode, 45 | 'state_name' => $record->mostSpecificSubdivision->name, 46 | 'postal_code' => $record->postal->code, 47 | 'lat' => $record->location->latitude, 48 | 'lon' => $record->location->longitude, 49 | 'timezone' => $record->location->timeZone, 50 | 'continent' => $record->continent->code, 51 | 'localizations' => $this->getLocalizations($record), 52 | ]); 53 | } 54 | 55 | /** 56 | * Get localized country name, state name and city name based on config languages 57 | * 58 | * @param City $record 59 | * 60 | * @return array 61 | */ 62 | private function getLocalizations(City $record): array 63 | { 64 | $localizations = []; 65 | 66 | foreach ($this->config('locales', ['en']) as $lang) { 67 | $localizations[$lang]['country'] = Arr::get($record->country->names, $lang); 68 | $localizations[$lang]['state_name'] = Arr::get($record->mostSpecificSubdivision->names, $lang); 69 | $localizations[$lang]['city'] = Arr::get($record->city->names, $lang); 70 | } 71 | 72 | return $localizations; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Support/Currencies.php: -------------------------------------------------------------------------------- 1 | 'EUR', 5 | 'AE' => 'AED', 6 | 'AF' => 'AFN', 7 | 'AG' => 'XCD', 8 | 'AI' => 'XCD', 9 | 'AL' => 'ALL', 10 | 'AM' => 'AMD', 11 | 'AN' => 'ANG', 12 | 'AO' => 'AOA', 13 | 'AQ' => '', 14 | 'AR' => 'ARS', 15 | 'AS' => 'USD', 16 | 'AT' => 'EUR', 17 | 'AU' => 'AUD', 18 | 'AW' => 'AWG', 19 | 'AZ' => 'AZN', 20 | 'BA' => 'BAM', 21 | 'BB' => 'BBD', 22 | 'BD' => 'BDT', 23 | 'BE' => 'EUR', 24 | 'BF' => 'XOF', 25 | 'BG' => 'BGN', 26 | 'BH' => 'BHD', 27 | 'BI' => 'BIF', 28 | 'BJ' => 'XOF', 29 | 'BL' => 'EUR', 30 | 'BM' => 'BMD', 31 | 'BN' => 'BND', 32 | 'BO' => 'BOB', 33 | 'BR' => 'BRL', 34 | 'BS' => 'BSD', 35 | 'BT' => 'BTN', 36 | 'BV' => 'NOK', 37 | 'BW' => 'BWP', 38 | 'BY' => 'BYN', 39 | 'BZ' => 'BZD', 40 | 'CA' => 'CAD', 41 | 'CC' => 'AUD', 42 | 'CD' => 'CDF', 43 | 'CF' => 'XAF', 44 | 'CG' => 'CDF', 45 | 'CH' => 'CHF', 46 | 'CI' => 'XOF', 47 | 'CK' => 'NZD', 48 | 'CL' => 'CLP', 49 | 'CM' => 'XAF', 50 | 'CN' => 'CNY', 51 | 'CO' => 'COP', 52 | 'CR' => 'CRC', 53 | 'CU' => 'CUP', 54 | 'CV' => 'CVE', 55 | 'CW' => 'ANG', 56 | 'CX' => 'AUD', 57 | 'CY' => 'EUR', 58 | 'CZ' => 'CZK', 59 | 'DE' => 'EUR', 60 | 'DJ' => 'DJF', 61 | 'DK' => 'DKK', 62 | 'DM' => 'XCD', 63 | 'DO' => 'DOP', 64 | 'DZ' => 'DZD', 65 | 'EC' => 'USD', 66 | 'EE' => 'EUR', 67 | 'EG' => 'EGP', 68 | 'EH' => 'MAD', 69 | 'ER' => 'ERN', 70 | 'ES' => 'EUR', 71 | 'ET' => 'ETB', 72 | 'FI' => 'EUR', 73 | 'FJ' => 'FJD', 74 | 'FK' => 'FKP', 75 | 'FM' => 'USD', 76 | 'FO' => 'DKK', 77 | 'FR' => 'EUR', 78 | 'GA' => 'XAF', 79 | 'GB' => 'GBP', 80 | 'GD' => 'XCD', 81 | 'GE' => 'GEL', 82 | 'GF' => 'EUR', 83 | 'GG' => 'GGP', 84 | 'GH' => 'GHS', 85 | 'GI' => 'GIP', 86 | 'GL' => 'DKK', 87 | 'GM' => 'GMD', 88 | 'GN' => 'GNF', 89 | 'GP' => 'EUR', 90 | 'GQ' => 'XAF', 91 | 'GR' => 'EUR', 92 | 'GS' => 'GBP', 93 | 'GT' => 'GTQ', 94 | 'GU' => 'USD', 95 | 'GW' => 'XOF', 96 | 'GY' => 'GYD', 97 | 'HK' => 'HKD', 98 | 'HM' => 'AUD', 99 | 'HN' => 'HNL', 100 | 'HR' => 'HRK', 101 | 'HT' => 'HTG', 102 | 'HU' => 'HUF', 103 | 'ID' => 'IDR', 104 | 'IE' => 'EUR', 105 | 'IL' => 'ILS', 106 | 'IM' => 'IMP', 107 | 'IN' => 'INR', 108 | 'IO' => 'USD', 109 | 'IQ' => 'IQD', 110 | 'IR' => 'IRR', 111 | 'IS' => 'ISK', 112 | 'IT' => 'EUR', 113 | 'JE' => 'JEP', 114 | 'JM' => 'JMD', 115 | 'JO' => 'JOD', 116 | 'JP' => 'JPY', 117 | 'KE' => 'KES', 118 | 'KG' => 'KGS', 119 | 'KH' => 'KHR', 120 | 'KI' => 'AUD', 121 | 'KM' => 'KMF', 122 | 'KN' => 'XCD', 123 | 'KP' => 'KPW', 124 | 'KR' => 'KRW', 125 | 'KW' => 'KWD', 126 | 'KY' => 'KYD', 127 | 'KZ' => 'KZT', 128 | 'LA' => 'LAK', 129 | 'LB' => 'LBP', 130 | 'LC' => 'XCD', 131 | 'LI' => 'CHF', 132 | 'LK' => 'LKR', 133 | 'LR' => 'LRD', 134 | 'LS' => 'LSL', 135 | 'LT' => 'EUR', 136 | 'LU' => 'EUR', 137 | 'LV' => 'EUR', 138 | 'LY' => 'LYD', 139 | 'MA' => 'MAD', 140 | 'MC' => 'EUR', 141 | 'MD' => 'MDL', 142 | 'ME' => 'EUR', 143 | 'MG' => 'MGA', 144 | 'MH' => 'USD', 145 | 'MK' => 'MKD', 146 | 'ML' => 'XOF', 147 | 'MM' => 'MMK', 148 | 'MN' => 'MNT', 149 | 'MO' => 'MOP', 150 | 'MP' => 'USD', 151 | 'MQ' => 'EUR', 152 | 'MR' => 'MRO', 153 | 'MS' => 'XCD', 154 | 'MT' => 'EUR', 155 | 'MU' => 'MUR', 156 | 'MV' => 'MVR', 157 | 'MW' => 'MWK', 158 | 'MX' => 'MXN', 159 | 'MY' => 'MYR', 160 | 'MZ' => 'MZN', 161 | 'NA' => 'NAD', 162 | 'NC' => 'XPF', 163 | 'NE' => 'XOF', 164 | 'NF' => 'AUD', 165 | 'NG' => 'NGN', 166 | 'NI' => 'NIO', 167 | 'NL' => 'EUR', 168 | 'NO' => 'NOK', 169 | 'NP' => 'NPR', 170 | 'NR' => 'AUD', 171 | 'NU' => 'NZD', 172 | 'NZ' => 'NZD', 173 | 'OM' => 'OMR', 174 | 'PA' => 'PAB', 175 | 'PE' => 'PEN', 176 | 'PF' => 'XPF', 177 | 'PG' => 'PGK', 178 | 'PH' => 'PHP', 179 | 'PK' => 'PKR', 180 | 'PL' => 'PLN', 181 | 'PM' => 'EUR', 182 | 'PN' => 'GBP', 183 | 'PR' => 'USD', 184 | 'PS' => 'ILS', 185 | 'PT' => 'EUR', 186 | 'PW' => 'USD', 187 | 'PY' => 'PYG', 188 | 'QA' => 'QAR', 189 | 'RE' => 'EUR', 190 | 'RO' => 'RON', 191 | 'RS' => 'RSD', 192 | 'RU' => 'RUB', 193 | 'RW' => 'RWF', 194 | 'SA' => 'SAR', 195 | 'SB' => 'SBD', 196 | 'SC' => 'SCR', 197 | 'SD' => 'SDG', 198 | 'SE' => 'SEK', 199 | 'SG' => 'SGD', 200 | 'SH' => 'SHP', 201 | 'SI' => 'EUR', 202 | 'SJ' => 'NOK', 203 | 'SK' => 'EUR', 204 | 'SL' => 'SLL', 205 | 'SM' => 'EUR', 206 | 'SN' => 'XOF', 207 | 'SO' => 'SOS', 208 | 'SR' => 'SRD', 209 | 'SS' => 'SSP', 210 | 'ST' => 'STD', 211 | 'SV' => 'USD', 212 | 'SX' => 'ANG', 213 | 'SY' => 'SYP', 214 | 'SZ' => 'SZL', 215 | 'TC' => 'USD', 216 | 'TD' => 'XAF', 217 | 'TF' => 'EUR', 218 | 'TG' => 'XOF', 219 | 'TH' => 'THB', 220 | 'TJ' => 'TJS', 221 | 'TK' => 'NZD', 222 | 'TL' => 'USD', 223 | 'TM' => 'TMT', 224 | 'TN' => 'TND', 225 | 'TO' => 'TOP', 226 | 'TR' => 'TRY', 227 | 'TT' => 'TTD', 228 | 'TV' => 'AUD', 229 | 'TW' => 'TWD', 230 | 'TZ' => 'TZS', 231 | 'UA' => 'UAH', 232 | 'UG' => 'UGX', 233 | 'US' => 'USD', 234 | 'UY' => 'UYU', 235 | 'UZ' => 'UZS', 236 | 'VA' => 'EUR', 237 | 'VC' => 'XCD', 238 | 'VE' => 'VEF', 239 | 'VG' => 'USD', 240 | 'VI' => 'USD', 241 | 'VN' => 'VND', 242 | 'VU' => 'VUV', 243 | 'WF' => 'XPF', 244 | 'WS' => 'WST', 245 | 'XK' => 'EUR', 246 | 'YE' => 'YER', 247 | 'YT' => 'EUR', 248 | 'ZA' => 'ZAR', 249 | 'ZM' => 'ZMK', 250 | 'ZW' => 'ZWL', 251 | ]; 252 | -------------------------------------------------------------------------------- /src/Support/HttpClient.php: -------------------------------------------------------------------------------- 1 | '', 16 | 'headers' => [], 17 | 'query' => [], 18 | ]; 19 | 20 | /** 21 | * Last request http status. 22 | * 23 | * @var int 24 | **/ 25 | protected $http_code = 200; 26 | 27 | /** 28 | * Last request error string. 29 | * 30 | * @var string 31 | **/ 32 | protected $errors = null; 33 | 34 | /** 35 | * Array containing headers from last performed request. 36 | * 37 | * @var array 38 | */ 39 | private $headers = []; 40 | 41 | /** 42 | * HttpClient constructor. 43 | * 44 | * @param array $config 45 | */ 46 | public function __construct(array $config = []) 47 | { 48 | $this->config = $config; 49 | } 50 | 51 | /** 52 | * Perform a get request. 53 | * 54 | * @param string $url 55 | * @param array $query 56 | * @param array $headers 57 | * 58 | * @return array 59 | */ 60 | public function get($url, array $query = [], array $headers = []) 61 | { 62 | return $this->execute('GET', $this->buildGetUrl($url, $query), [], $headers); 63 | } 64 | 65 | /** 66 | * Execute the curl request 67 | * 68 | * @param string $method 69 | * @param string $url 70 | * @param array $query 71 | * @param array $headers 72 | * 73 | * @return array 74 | */ 75 | public function execute($method, $url, array $query = [], array $headers = []) 76 | { 77 | // Merge global and request headers 78 | $headers = array_merge( 79 | Arr::get($this->config, 'headers', []), 80 | $headers 81 | ); 82 | 83 | // Merge global and request queries 84 | $query = array_merge( 85 | Arr::get($this->config, 'query', []), 86 | $query 87 | ); 88 | 89 | $this->errors = null; 90 | 91 | $curl = curl_init(); 92 | 93 | // Set options 94 | curl_setopt_array($curl, [ 95 | CURLOPT_URL => $this->getUrl($url), 96 | CURLOPT_HTTPHEADER => $headers, 97 | CURLOPT_CONNECTTIMEOUT => 20, 98 | CURLOPT_TIMEOUT => 90, 99 | CURLOPT_RETURNTRANSFER => 1, 100 | CURLOPT_SSL_VERIFYPEER => 0, 101 | CURLOPT_SSL_VERIFYHOST => 0, 102 | CURLOPT_HEADER => 1, 103 | CURLINFO_HEADER_OUT => 1, 104 | CURLOPT_VERBOSE => 1, 105 | ]); 106 | 107 | // Setup method specific options 108 | switch ($method) { 109 | case 'PUT': 110 | case 'PATCH': 111 | case 'POST': 112 | curl_setopt_array($curl, [ 113 | CURLOPT_CUSTOMREQUEST => $method, 114 | CURLOPT_POST => true, 115 | CURLOPT_POSTFIELDS => $query, 116 | ]); 117 | break; 118 | 119 | case 'DELETE': 120 | curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'DELETE'); 121 | break; 122 | 123 | default: 124 | curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'GET'); 125 | break; 126 | } 127 | 128 | // Make request 129 | curl_setopt($curl, CURLOPT_HEADER, true); 130 | $response = curl_exec($curl); 131 | 132 | // Set HTTP response code 133 | $this->http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE); 134 | 135 | // Set errors if there are any 136 | if (curl_errno($curl)) { 137 | $this->errors = curl_error($curl); 138 | } 139 | 140 | // Parse body 141 | $header_size = curl_getinfo($curl, CURLINFO_HEADER_SIZE); 142 | $header = substr($response, 0, $header_size); 143 | $body = substr($response, $header_size); 144 | 145 | curl_close($curl); 146 | 147 | return [$body, $this->parseHeaders($header)]; 148 | } 149 | 150 | /** 151 | * Check if the curl request ended up with errors 152 | * 153 | * @return bool 154 | */ 155 | public function hasErrors() 156 | { 157 | return is_null($this->errors) === false; 158 | } 159 | 160 | /** 161 | * Get curl errors 162 | * 163 | * @return string 164 | */ 165 | public function getErrors() 166 | { 167 | return $this->errors; 168 | } 169 | 170 | /** 171 | * Get last curl HTTP code. 172 | * 173 | * @return int 174 | */ 175 | public function getHttpCode() 176 | { 177 | return $this->http_code; 178 | } 179 | 180 | /** 181 | * Parse string headers into array 182 | * 183 | * @param string $headers 184 | * 185 | * @return array 186 | */ 187 | private function parseHeaders($headers) 188 | { 189 | $result = []; 190 | 191 | foreach (preg_split("/\\r\\n|\\r|\\n/", $headers) as $row) { 192 | $header = explode(':', $row, 2); 193 | 194 | if (count($header) == 2) { 195 | $result[$header[0]] = trim($header[1]); 196 | } 197 | else { 198 | $result[] = $header[0]; 199 | } 200 | } 201 | 202 | return $result; 203 | } 204 | 205 | /** 206 | * Get request URL. 207 | * 208 | * @param string $url 209 | * 210 | * @return string 211 | */ 212 | private function getUrl($url) 213 | { 214 | // Check for URL scheme 215 | if (parse_url($url, PHP_URL_SCHEME) === null) { 216 | $url = Arr::get($this->config, 'base_uri') . $url; 217 | } 218 | 219 | return $url; 220 | } 221 | 222 | /** 223 | * Build a GET request string. 224 | * 225 | * @param string $url 226 | * @param array $query 227 | * 228 | * @return string 229 | */ 230 | private function buildGetUrl($url, array $query = []) 231 | { 232 | // Merge global and request queries 233 | $query = array_merge( 234 | Arr::get($this->config, 'query', []), 235 | $query 236 | ); 237 | 238 | // Append query 239 | if ($query = http_build_query($query)) { 240 | $url .= strpos($url, '?') ? $query : "?{$query}"; 241 | } 242 | 243 | return $url; 244 | } 245 | } -------------------------------------------------------------------------------- /src/helpers.php: -------------------------------------------------------------------------------- 1 | getLocation($ip); 18 | } 19 | } --------------------------------------------------------------------------------