├── .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 | [](https://packagist.org/packages/brightnucleus/geolite2-country)
4 | [](https://packagist.org/packages/brightnucleus/geolite2-country)
5 | [](https://packagist.org/packages/brightnucleus/geolite2-country)
6 | [](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 |
--------------------------------------------------------------------------------