├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json └── src ├── Database.php └── DatabaseUpdater.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /data/ 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | This project adheres to [Semantic Versioning](http://semver.org/). 4 | 5 | ## [0.2.3] - 2017-02-10 6 | ### Changed 7 | - Additional notification improvements. Props [Gary Jones] 8 | 9 | ## [0.2.2] - 2017-02-09 10 | ### Changed 11 | - Further improve notifications. Props [Gary Jones] 12 | 13 | ### Fixed 14 | - Let user know when the database is already up to date. Props [Gary Jones](https://github.com/GaryJones) 15 | - Fix typo. Props [Gary Jones](https://github.com/GaryJones) 16 | 17 | ## [0.2.1] - 2017-02-09 18 | ### Added 19 | - Added integrity check to make sure the downloaded database file matches the expected MD5 hash. 20 | - Added a mechanism to retry failed downloads three times before aborting. 21 | 22 | ### Changed 23 | - All update operations work on temporary files until the download is confirmed to be good, to avoid breaking already working code on updates. 24 | 25 | ## [0.2.0] - 2016-08-01 26 | ### Added 27 | - Added the path to the data to the Composer output. 28 | - Added `LICENSE` file. 29 | 30 | ### Changed 31 | - Changed license from GPL-v2.0+ to MIT. 32 | 33 | ## [0.1.6] - 2016-03-05 34 | ### Fixed 35 | - Changed two constants that were now referencing the wrong class. 36 | 37 | ## [0.1.5] - 2016-03-04 38 | ### Fixed 39 | - Split code into two different classes to avoid issues outside of Composer flow. 40 | 41 | ## [0.1.4] - 2016-03-04 42 | ### Fixed 43 | - Corrected the `README.md` to adapt it to the recent changes and added example code. 44 | 45 | ## [0.1.3] - 2016-03-04 46 | ### Added 47 | - Changed class into a Composer plugin to work around the fact that Composer does not call dependency scripts automatically. 48 | 49 | ## [0.1.2] - 2016-03-04 50 | ### Added 51 | - The zipped file is now remove after it was unzipped, to recover storage space. 52 | 53 | ## [0.1.1] - 2016-03-04 54 | ### Added 55 | - Added details about adding `scripts` hooks to `README.md`. 56 | 57 | ## [0.1.0] - 2016-03-03 58 | ### Added 59 | - Initial release to GitHub. 60 | 61 | [Gary Jones]: https://github.com/GaryJones 62 | 63 | [0.2.3]: https://github.com/brightnucleus/geolite2-country/compare/v0.2.2...v0.2.3 64 | [0.2.2]: https://github.com/brightnucleus/geolite2-country/compare/v0.2.1...v0.2.2 65 | [0.2.1]: https://github.com/brightnucleus/geolite2-country/compare/v0.2.0...v0.2.1 66 | [0.2.0]: https://github.com/brightnucleus/geolite2-country/compare/v0.1.6...v0.2.0 67 | [0.1.6]: https://github.com/brightnucleus/geolite2-country/compare/v0.1.5...v0.1.6 68 | [0.1.5]: https://github.com/brightnucleus/geolite2-country/compare/v0.1.4...v0.1.5 69 | [0.1.4]: https://github.com/brightnucleus/geolite2-country/compare/v0.1.3...v0.1.4 70 | [0.1.3]: https://github.com/brightnucleus/geolite2-country/compare/v0.1.2...v0.1.3 71 | [0.1.2]: https://github.com/brightnucleus/geolite2-country/compare/v0.1.1...v0.1.2 72 | [0.1.1]: https://github.com/brightnucleus/geolite2-country/compare/v0.1.0...v0.1.1 73 | [0.1.0]: https://github.com/brightnucleus/geolite2-country/compare/v0.0.0...v0.1.0 74 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 Alain Schlesser, Bright Nucleus 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 8 | of the Software, and to permit persons to whom the Software is furnished to do 9 | so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bright Nucleus GeoLite2 Country Database 2 | 3 | [![Latest Stable Version](https://poser.pugx.org/brightnucleus/geolite2-country/v/stable)](https://packagist.org/packages/brightnucleus/geolite2-country) 4 | [![Total Downloads](https://poser.pugx.org/brightnucleus/geolite2-country/downloads)](https://packagist.org/packages/brightnucleus/geolite2-country) 5 | [![Latest Unstable Version](https://poser.pugx.org/brightnucleus/geolite2-country/v/unstable)](https://packagist.org/packages/brightnucleus/geolite2-country) 6 | [![License](https://poser.pugx.org/brightnucleus/geolite2-country/license)](https://packagist.org/packages/brightnucleus/geolite2-country) 7 | 8 | This is a Composer plugin that provides an automated binary version of the free MaxMind GeoLite2 Country database. 9 | 10 | The main advantage is that the downloaded database will be checked for updates on each `composer install` and `composer update`. 11 | 12 | ## Table Of Contents 13 | 14 | * [Attribution](#attribution) 15 | * [Installation](#installation) 16 | * [Basic Usage](#basic-usage) 17 | * [Example](#example) 18 | * [Contributing](#contributing) 19 | * [License](#license) 20 | 21 | ## Attribution 22 | 23 | This product includes GeoLite2 data created by MaxMind, available from 24 | http://www.maxmind.com. 25 | 26 | ## Installation 27 | 28 | The only thing you need to do to make this work is adding this package as a dependency to your project: 29 | 30 | ```BASH 31 | composer require brightnucleus/geolite2-country 32 | ``` 33 | 34 | ## Basic Usage 35 | 36 | On each `composer install` or `composer update`, a check will be made to see whether there's a new version of the database available. If there is, that new version is downloaded. 37 | 38 | To retrieve the path to the binary database file from within your project, you can use the `Database::getLocation()` method: 39 | 40 | ```PHP 41 | country($ip); 65 | } 66 | ``` 67 | 68 | ## Contributing 69 | 70 | All feedback / bug reports / pull requests are welcome. 71 | 72 | ## License 73 | 74 | This code is released under the MIT license. 75 | 76 | For the full copyright and license information, please view the [`LICENSE`](LICENSE) file distributed with this source code. 77 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "brightnucleus/geolite2-country", 3 | "description": "Composer-packaged version of the free MaxMind GeoLite2 Country database.", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Alain Schlesser", 8 | "email": "alain.schlesser@gmail.com" 9 | } 10 | ], 11 | "type": "composer-plugin", 12 | "require": { 13 | "php": ">=5.4", 14 | "composer-plugin-api": "^1" 15 | }, 16 | "autoload": { 17 | "psr-4": { 18 | "BrightNucleus\\GeoLite2Country\\": "src/" 19 | } 20 | }, 21 | "extra": { 22 | "class": "BrightNucleus\\GeoLite2Country\\DatabaseUpdater" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Database.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link http://www.brightnucleus.com/ 9 | * @copyright 2016 Alain Schlesser, Bright Nucleus 10 | */ 11 | 12 | namespace BrightNucleus\GeoLite2Country; 13 | 14 | /** 15 | * Class Database. 16 | * 17 | * @since 0.1.0 18 | * 19 | * @package BrightNucleus\GeoLite2Country 20 | * @author Alain Schlesser 21 | */ 22 | class Database 23 | { 24 | 25 | const DB_FILENAME = 'GeoLite2-Country.mmdb'; 26 | const DB_FOLDER = 'data'; 27 | const DB_URL = 'http://geolite.maxmind.com/download/geoip/database/GeoLite2-Country.mmdb.gz'; 28 | const MD5_URL = 'http://geolite.maxmind.com/download/geoip/database/GeoLite2-Country.md5'; 29 | 30 | /** 31 | * Get the location of the database file. 32 | * 33 | * @since 0.1.0 34 | * 35 | * @param bool $array Optional. Whether to return the location as an array. Defaults to false. 36 | * @return string|array Either a string, containing the absolute path to the file, or an array with the location 37 | * split up into two keys named 'folder' and 'filename' 38 | */ 39 | public static function getLocation($array = false) 40 | { 41 | $folder = realpath(__DIR__ . '/../') . '/' . self::DB_FOLDER; 42 | $filepath = $folder . '/' . self::DB_FILENAME; 43 | if (! $array) { 44 | return $filepath; 45 | } 46 | 47 | return [ 48 | 'folder' => $folder, 49 | 'file' => self::DB_FILENAME, 50 | ]; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/DatabaseUpdater.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link http://www.brightnucleus.com/ 9 | * @copyright 2016 Alain Schlesser, Bright Nucleus 10 | */ 11 | 12 | namespace BrightNucleus\GeoLite2Country; 13 | 14 | use Composer\Composer; 15 | use Composer\EventDispatcher\EventSubscriberInterface; 16 | use Composer\IO\IOInterface; 17 | use Composer\Plugin\PluginInterface; 18 | use Composer\Script\Event; 19 | use Composer\Script\ScriptEvents; 20 | 21 | /** 22 | * Class DatabaseUpdater. 23 | * 24 | * @since 0.1.5 25 | * 26 | * @package BrightNucleus\GeoLite2Country 27 | * @author Alain Schlesser 28 | */ 29 | class DatabaseUpdater implements PluginInterface, EventSubscriberInterface 30 | { 31 | 32 | /** 33 | * Get the event subscriber configuration for this plugin. 34 | * 35 | * @return array The events to listen to, and their associated handlers. 36 | */ 37 | public static function getSubscribedEvents() 38 | { 39 | return array( 40 | ScriptEvents::POST_INSTALL_CMD => 'update', 41 | ScriptEvents::POST_UPDATE_CMD => 'update', 42 | ); 43 | } 44 | 45 | /** 46 | * Update the stored database. 47 | * 48 | * @since 0.1.0 49 | * 50 | * @param Event $event 51 | */ 52 | public static function update(Event $event) 53 | { 54 | $dbFilename = Database::getLocation(); 55 | 56 | $io = $event->getIO(); 57 | 58 | $io->write('Making sure the DB folder exists: ' . dirname($dbFilename), true, IOInterface::VERBOSE); 59 | self::maybeCreateDBFolder(dirname($dbFilename)); 60 | 61 | $oldMD5 = self::getContents($dbFilename . '.md5'); 62 | $io->write('MD5 of existing local DB file: ' . $oldMD5, true, IOInterface::VERBOSE); 63 | 64 | $io->write('Fetching remote MD5 hash...'); 65 | $io->write( 66 | sprintf( 67 | 'Downloading file: %1$s => %2$s', 68 | Database::MD5_URL, 69 | $dbFilename . '.md5.new' 70 | ), 71 | true, 72 | IOInterface::VERBOSE 73 | ); 74 | self::downloadFile($dbFilename . '.md5.new', Database::MD5_URL); 75 | 76 | $newMD5 = self::getContents($dbFilename . '.md5.new'); 77 | $io->write('MD5 of current remote DB file: ' . $newMD5, true, IOInterface::VERBOSE); 78 | if ($newMD5 === $oldMD5) { 79 | $io->write( 80 | sprintf( 81 | 'The local MaxMind GeoLite2 Country database is already up to date. (%1$s)', 82 | $dbFilename 83 | ), 84 | true 85 | ); 86 | 87 | return; 88 | } 89 | 90 | // If the download was corrupted, retry three times before aborting. 91 | // If the update is aborted, the currently active DB file stays in place, to not break a site on failed updates. 92 | $retry = 3; 93 | while ($retry > 0) { 94 | $io->write('Fetching new version of the MaxMind GeoLite2 Country database...', true); 95 | $io->write( 96 | sprintf( 97 | 'Downloading file: %1$s => %2$s', 98 | Database::DB_URL, 99 | $dbFilename . '.gz' 100 | ), 101 | true, 102 | IOInterface::VERBOSE 103 | ); 104 | self::downloadFile($dbFilename . '.gz', Database::DB_URL); 105 | 106 | // We unzip into a temporary file, so as not to destroy the DB that is known to be working. 107 | $io->write('Unzipping the database...', true); 108 | 109 | $io->write('Unzipping file: ' . $dbFilename . '.gz => ' . $dbFilename . '.tmp', true, IOInterface::VERBOSE); 110 | self::unzipFile($dbFilename . '.gz', $dbFilename . '.tmp'); 111 | 112 | $io->write('Removing file: ' . $dbFilename . '.gz', true, IOInterface::VERBOSE); 113 | self::removeFile($dbFilename . '.gz'); 114 | 115 | $io->write('Verifying integrity of the downloaded database file...', true); 116 | $downloadMD5 = self::calculateMD5($dbFilename . '.tmp'); 117 | $io->write('MD5 of downloaded DB file: ' . $downloadMD5, true, IOInterface::VERBOSE); 118 | 119 | // Download was successful, so now we replace the existing DB file with the freshly downloaded one. 120 | if ($downloadMD5 === $newMD5) { 121 | $io->write('All good, replacing previous version of the database with the downloaded one...', true); 122 | $retry = 0; 123 | 124 | $io->write('Removing file: ' . $dbFilename, true, IOInterface::VERBOSE); 125 | self::removeFile($dbFilename); 126 | 127 | $io->write('Removing file: ' . $dbFilename . '.md5', true, IOInterface::VERBOSE); 128 | self::removeFile($dbFilename . '.md5'); 129 | 130 | $io->write('Renaming file: ' . $dbFilename . '.tmp => ' . $dbFilename, true, IOInterface::VERBOSE); 131 | self::renameFile($dbFilename . '.tmp', $dbFilename); 132 | 133 | $io->write( 134 | 'Renaming file: ' . $dbFilename . '.md5.new => ' . $dbFilename . '.md5', 135 | true, 136 | IOInterface::VERBOSE 137 | ); 138 | self::renameFile($dbFilename . '.md5.new', $dbFilename . '.md5'); 139 | continue; 140 | } 141 | 142 | // The download was fishy, so we remove intermediate files and retry. 143 | $io->write('Downloaded file did not match expected MD5, retrying...', true); 144 | 145 | $io->write('Removing file: ' . $dbFilename . '.tmp', true, IOInterface::VERBOSE); 146 | self::removeFile($dbFilename . '.tmp'); 147 | 148 | $retry--; 149 | } 150 | 151 | // Even several retries did not produce a proper download, so we remove intermediate files and let the user know 152 | // about the issue. 153 | if (! isset($downloadMD5) 154 | || $downloadMD5 !== $newMD5 155 | ) { 156 | $io->write('Removing file: ' . $dbFilename . '.md5.new', true, IOInterface::VERBOSE); 157 | self::removeFile($dbFilename . '.md5.new'); 158 | 159 | $io->writeError('Failed to download the MaxMind GeoLite2 Country database! Aborting update.'); 160 | 161 | return; 162 | } 163 | 164 | $io->write( 165 | sprintf( 166 | 'The local MaxMind GeoLite2 Country database has been updated. (%1$s)', 167 | $dbFilename 168 | ), 169 | true 170 | ); 171 | } 172 | 173 | /** 174 | * Create the DB folder if it does not exist yet. 175 | * 176 | * @since 0.1.0 177 | * 178 | * @param string $folder Name of the DB folder. 179 | */ 180 | protected static function maybeCreateDBFolder($folder) 181 | { 182 | if (! is_dir($folder)) { 183 | mkdir($folder); 184 | } 185 | } 186 | 187 | /** 188 | * Get the content from within a file. 189 | * 190 | * @since 0.2.1 191 | * 192 | * @param string $filename Filename. 193 | * @return string File content. 194 | */ 195 | protected static function getContents($filename) 196 | { 197 | if (! is_file($filename)) { 198 | return ''; 199 | } 200 | 201 | return file_get_contents($filename); 202 | } 203 | 204 | /** 205 | * Calculate the MD5 hash of a file. 206 | * 207 | * @since 0.2.1 208 | * 209 | * @param string $filename Filename of the MD5 file. 210 | * @return string MD5 hash contained within the file. Empty string if not found. 211 | */ 212 | protected static function calculateMD5($filename) 213 | { 214 | return md5(self::getContents($filename)); 215 | } 216 | 217 | /** 218 | * Download a file from an URL. 219 | * 220 | * @since 0.1.0 221 | * 222 | * @param string $filename Filename of the file to download. 223 | * @param string $url URL of the file to download. 224 | */ 225 | protected static function downloadFile($filename, $url) 226 | { 227 | $fileHandle = fopen($filename, 'w'); 228 | $options = [ 229 | CURLOPT_FILE => $fileHandle, 230 | CURLOPT_TIMEOUT => 600, 231 | CURLOPT_URL => $url, 232 | ]; 233 | 234 | $curl = curl_init(); 235 | curl_setopt_array($curl, $options); 236 | curl_exec($curl); 237 | curl_close($curl); 238 | } 239 | 240 | /** 241 | * Unzip a gzipped file. 242 | * 243 | * @since 0.1.0 244 | * 245 | * @param string $source Source, zipped filename to unzip. 246 | * @param string $destination Destination filename to write the unzipped contents to. 247 | */ 248 | protected static function unzipFile($source, $destination) 249 | { 250 | $buffer_size = 4096; 251 | 252 | $zippedFile = gzopen($source, 'rb'); 253 | $unzippedFile = fopen($destination, 'wb'); 254 | 255 | while (! gzeof($zippedFile)) { 256 | fwrite($unzippedFile, gzread($zippedFile, $buffer_size)); 257 | } 258 | 259 | fclose($unzippedFile); 260 | gzclose($zippedFile); 261 | } 262 | 263 | /** 264 | * Delete a file. 265 | * 266 | * @since 0.1.2 267 | * 268 | * @param string $filename Filename of the file to delete. 269 | */ 270 | protected static function removeFile($filename) 271 | { 272 | if (is_file($filename)) { 273 | unlink($filename); 274 | } 275 | } 276 | 277 | /** 278 | * Rename a file. 279 | * 280 | * @since 0.1.2 281 | * 282 | * @param string $source Source filename of the file to rename. 283 | * @param string $destination Destination filename to rename the file to. 284 | */ 285 | protected static function renameFile($source, $destination) 286 | { 287 | if (is_file($source)) { 288 | rename($source, $destination); 289 | } 290 | } 291 | 292 | /** 293 | * Activate the plugin. 294 | * 295 | * @since 0.1.3 296 | * 297 | * @param Composer $composer The main Composer object. 298 | * @param IOInterface $io The i/o interface to use. 299 | */ 300 | public function activate(Composer $composer, IOInterface $io) 301 | { 302 | // no action required 303 | } 304 | } 305 | --------------------------------------------------------------------------------