1<?php 2/** 3 * Matomo - free/libre analytics platform 4 * 5 * @link https://matomo.org 6 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later 7 * 8 */ 9namespace Piwik\Plugins\GeoIp2; 10 11use Exception; 12use GeoIp2\Database\Reader; 13use Piwik\Common; 14use Piwik\Config; 15use Piwik\Container\StaticContainer; 16use Piwik\Date; 17use Piwik\Http; 18use Piwik\Log; 19use Piwik\Option; 20use Piwik\Piwik; 21use Piwik\Plugins\GeoIp2\LocationProvider\GeoIp2 AS LocationProviderGeoIp2; 22use Piwik\Plugins\GeoIp2\LocationProvider\GeoIp2\Php; 23use Piwik\Plugins\UserCountry\LocationProvider; 24use Piwik\Scheduler\Schedule\Hourly; 25use Piwik\Scheduler\Scheduler; 26use Piwik\Scheduler\Task; 27use Piwik\Scheduler\Timetable; 28use Piwik\Scheduler\Schedule\Monthly; 29use Piwik\Scheduler\Schedule\Weekly; 30use Piwik\SettingsPiwik; 31use Piwik\Unzip; 32use Psr\Log\LoggerInterface; 33 34/** 35 * Used to automatically update installed GeoIP 2 databases, and manages the updater's 36 * scheduled task. 37 */ 38class GeoIP2AutoUpdater extends Task 39{ 40 const SCHEDULE_PERIOD_MONTHLY = 'month'; 41 const SCHEDULE_PERIOD_WEEKLY = 'week'; 42 43 const SCHEDULE_PERIOD_OPTION_NAME = 'geoip2.updater_period'; 44 45 const LOC_URL_OPTION_NAME = 'geoip2.loc_db_url'; 46 const ISP_URL_OPTION_NAME = 'geoip2.isp_db_url'; 47 48 const LAST_RUN_TIME_OPTION_NAME = 'geoip2.updater_last_run_time'; 49 50 const AUTO_SETUP_OPTION_NAME = 'geoip2.autosetup'; 51 52 private static $urlOptions = array( 53 'loc' => self::LOC_URL_OPTION_NAME, 54 'isp' => self::ISP_URL_OPTION_NAME, 55 ); 56 57 /** 58 * Constructor. 59 */ 60 public function __construct() 61 { 62 $logger = StaticContainer::get(LoggerInterface::class); 63 64 if (!SettingsPiwik::isInternetEnabled()) { 65 // no automatic updates possible if no internet available 66 $logger->info("Internet is disabled in INI config, cannot update GeoIP database."); 67 return; 68 } 69 70 $schedulePeriodStr = self::getSchedulePeriod(); 71 72 // created the scheduledtime instance, also, since GeoIP 2 updates are done on tuesdays, 73 // get new DBs on Wednesday. For db-ip, the databases are updated daily, so it doesn't matter exactly 74 // when we download a new one. 75 switch ($schedulePeriodStr) { 76 case self::SCHEDULE_PERIOD_WEEKLY: 77 $schedulePeriod = new Weekly(); 78 $schedulePeriod->setDay(3); 79 break; 80 case self::SCHEDULE_PERIOD_MONTHLY: 81 default: 82 $schedulePeriod = new Monthly(); 83 $schedulePeriod->setDayOfWeek(3, 0); 84 break; 85 } 86 87 if (Option::get(self::AUTO_SETUP_OPTION_NAME)) { 88 $schedulePeriod = new Hourly(); 89 } 90 91 parent::__construct($this, 'update', null, $schedulePeriod, Task::LOWEST_PRIORITY); 92 } 93 94 /** 95 * Attempts to download new location & ISP GeoIP databases and 96 * replace the existing ones w/ them. 97 */ 98 public function update() 99 { 100 try { 101 Option::set(self::LAST_RUN_TIME_OPTION_NAME, Date::factory('today')->getTimestamp()); 102 103 $locUrl = Option::get(self::LOC_URL_OPTION_NAME); 104 if (!empty($locUrl)) { 105 $this->downloadFile('loc', $locUrl); 106 $this->updateDbIpUrlOption(self::LOC_URL_OPTION_NAME); 107 } 108 109 $ispUrl = Option::get(self::ISP_URL_OPTION_NAME); 110 if (!empty($ispUrl)) { 111 $this->downloadFile('isp', $ispUrl); 112 $this->updateDbIpUrlOption(self::ISP_URL_OPTION_NAME); 113 } 114 } catch (Exception $ex) { 115 // message will already be prefixed w/ 'GeoIP2AutoUpdater: ' 116 Log::error($ex); 117 $this->performRedundantDbChecks(); 118 throw $ex; 119 } 120 121 $this->performRedundantDbChecks(); 122 123 if (Option::get(self::AUTO_SETUP_OPTION_NAME)) { 124 Option::delete(self::AUTO_SETUP_OPTION_NAME); 125 LocationProvider::setCurrentProvider(Php::ID); 126 /** @var Scheduler $scheduler */ 127 $scheduler = StaticContainer::getContainer()->get('Piwik\Scheduler\Scheduler'); 128 // reschedule to ensure it's not run again in an hour 129 $scheduler->rescheduleTask(new GeoIP2AutoUpdater()); 130 } 131 } 132 133 /** 134 * Downloads a GeoIP 2 database archive, extracts the .mmdb file and overwrites the existing 135 * old database. 136 * 137 * If something happens that causes the download to fail, no exception is thrown, but 138 * an error is logged. 139 * 140 * @param string $dbType 141 * @param string $url URL to the database to download. The type of database is determined 142 * from this URL. 143 * @throws Exception 144 */ 145 protected function downloadFile($dbType, $url) 146 { 147 $logger = StaticContainer::get(LoggerInterface::class); 148 149 $url = trim($url); 150 151 if (self::isPaidDbIpUrl($url)) { 152 $url = $this->fetchPaidDbIpUrl($url); 153 } else if (self::isDbIpUrl($url)) { 154 $url = $this->getDbIpUrlWithLatestDate($url); 155 } 156 157 $ext = GeoIP2AutoUpdater::getGeoIPUrlExtension($url); 158 159 // NOTE: using the first item in $dbNames[$dbType] makes sure GeoLiteCity will be renamed to GeoIPCity 160 $zippedFilename = $this->getZippedFilenameToDownloadTo($url, $dbType, $ext); 161 162 $zippedOutputPath = self::getTemporaryFolder($zippedFilename, true); 163 164 $url = self::removeDateFromUrl($url); 165 166 // download zipped file to misc dir 167 try { 168 $logger->info("Downloading {url} to {output}.", [ 169 'url' => $url, 170 'output' => $zippedOutputPath, 171 ]); 172 173 $success = Http::sendHttpRequest($url, $timeout = 3600, $userAgent = null, $zippedOutputPath); 174 } catch (Exception $ex) { 175 throw new Exception("GeoIP2AutoUpdater: failed to download '$url' to " 176 . "'$zippedOutputPath': " . $ex->getMessage()); 177 } 178 179 if ($success !== true) { 180 throw new Exception("GeoIP2AutoUpdater: failed to download '$url' to " 181 . "'$zippedOutputPath'! (Unknown error)"); 182 } 183 184 Log::info("GeoIP2AutoUpdater: successfully downloaded '%s'", $url); 185 186 try { 187 self::unzipDownloadedFile($zippedOutputPath, $dbType, $url, $unlink = true); 188 } catch (Exception $ex) { 189 throw new Exception("GeoIP2AutoUpdater: failed to unzip '$zippedOutputPath' after " 190 . "downloading " . "'$url': " . $ex->getMessage()); 191 } 192 193 Log::info("GeoIP2AutoUpdater: successfully updated GeoIP 2 database '%s'", $url); 194 } 195 196 public static function getTemporaryFolder($file, $isDownload = false) 197 { 198 return \Piwik\Container\StaticContainer::get('path.tmp') . '/latest/' . $file . ($isDownload ? '.download' : ''); 199 } 200 201 /** 202 * Unzips a downloaded GeoIP 2 database. Only unzips .gz & .tar.gz files. 203 * 204 * @param string $path Path to zipped file. 205 * @param bool $unlink Whether to unlink archive or not. 206 * @throws Exception 207 */ 208 public static function unzipDownloadedFile($path, $dbType, $url, $unlink = false) 209 { 210 $isDbIp = self::isDbIpUrl($url); 211 212 $filename = $path; 213 214 if (substr($filename, -9, 9) === '.download') { 215 $filename = substr($filename, 0, -9); 216 } 217 218 $isDbIpUnknownDbType = $isDbIp && substr($filename, -5, 5) == '.mmdb'; 219 220 // extract file 221 if (substr($filename, -7, 7) == '.tar.gz') { 222 // find the .dat file in the tar archive 223 $unzip = Unzip::factory('tar.gz', $path); 224 $content = $unzip->listContent(); 225 226 if (empty($content)) { 227 throw new Exception(Piwik::translate('GeoIp2_CannotListContent', 228 array("'$path'", $unzip->errorInfo()))); 229 } 230 231 $fileToExtract = null; 232 foreach ($content as $info) { 233 $archivedPath = $info['filename']; 234 foreach (LocationProviderGeoIp2::$dbNames[$dbType] as $dbName) { 235 if (basename($archivedPath) === $dbName 236 || preg_match('/' . $dbName . '/', basename($archivedPath)) 237 ) { 238 $fileToExtract = $archivedPath; 239 } 240 } 241 } 242 243 if ($fileToExtract === null) { 244 throw new Exception(Piwik::translate('GeoIp2_CannotFindGeoIPDatabaseInArchive', 245 array("'$path'"))); 246 } 247 248 // extract JUST the .dat file 249 $unzipped = $unzip->extractInString($fileToExtract); 250 251 if (empty($unzipped)) { 252 throw new Exception(Piwik::translate('GeoIp2_CannotUnzipGeoIPFile', 253 array("'$path'", $unzip->errorInfo()))); 254 } 255 256 $dbFilename = basename($fileToExtract); 257 $tempFilename = $dbFilename . '.new'; 258 $outputPath = self::getTemporaryFolder($tempFilename); 259 260 // write unzipped to file 261 $fd = fopen($outputPath, 'wb'); 262 fwrite($fd, $unzipped); 263 fclose($fd); 264 } else if (substr($filename, -3, 3) == '.gz' 265 || $isDbIpUnknownDbType 266 ) { 267 $unzip = Unzip::factory('gz', $path); 268 269 if ($isDbIpUnknownDbType) { 270 $tempFilename = 'unzipped-temp-dbip-file.mmdb'; 271 } else { 272 $dbFilename = substr(basename($filename), 0, -3); 273 $tempFilename = $dbFilename . '.new'; 274 } 275 276 $outputPath = self::getTemporaryFolder($tempFilename); 277 278 $success = $unzip->extract($outputPath); 279 if ($success !== true) { 280 throw new Exception(Piwik::translate('General_CannotUnzipFile', 281 array("'$path'", $unzip->errorInfo()))); 282 } 283 284 if ($isDbIpUnknownDbType) { 285 $php = new Php([$dbType => [$outputPath]]); 286 $dbFilename = $php->detectDatabaseType($dbType) . '.mmdb'; 287 unset($php); 288 } 289 } else { 290 $parts = explode(basename($filename), '.', 2); 291 $ext = end($parts); 292 throw new Exception(Piwik::translate('GeoIp2_UnsupportedArchiveType', "'$ext'")); 293 } 294 295 try { 296 // test that the new archive is a valid GeoIP 2 database 297 if (empty($dbFilename) || false === LocationProviderGeoIp2::getGeoIPDatabaseTypeFromFilename($dbFilename)) { 298 throw new Exception("Unexpected GeoIP 2 archive file name '$path'."); 299 } 300 301 $customDbNames = array( 302 'loc' => array(), 303 'isp' => array() 304 ); 305 $customDbNames[$dbType] = array($outputPath); 306 307 $phpProvider = new Php($customDbNames); 308 309 try { 310 $location = $phpProvider->getLocation(array('ip' => LocationProviderGeoIp2::TEST_IP)); 311 unset($phpProvider); 312 } catch (\Exception $e) { 313 Log::info("GeoIP2AutoUpdater: Encountered exception when testing newly downloaded" . 314 " GeoIP 2 database: %s", $e->getMessage()); 315 316 throw new Exception(Piwik::translate('GeoIp2_ThisUrlIsNotAValidGeoIPDB')); 317 } 318 319 if (empty($location)) { 320 throw new Exception(Piwik::translate('GeoIp2_ThisUrlIsNotAValidGeoIPDB')); 321 } 322 323 // ensure the cached location providers do no longer block any files on windows 324 foreach (LocationProvider::getAllProviders() as $provider) { 325 if ($provider instanceof Php) { 326 $provider->clearCachedInstances(); 327 } 328 } 329 330 // delete the existing GeoIP database (if any) and rename the downloaded file 331 $oldDbFile = LocationProviderGeoIp2::getPathForGeoIpDatabase($dbFilename); 332 if (file_exists($oldDbFile)) { 333 @unlink($oldDbFile); 334 } 335 336 $tempFile = self::getTemporaryFolder($tempFilename); 337 if (@rename($tempFile, $oldDbFile) !== true) { 338 //In case the $tempfile cannot be renamed, we copy the file. 339 copy($tempFile, $oldDbFile); 340 unlink($tempFile); 341 } 342 343 // delete original archive 344 if ($unlink) { 345 unlink($path); 346 } 347 348 self::renameAnyExtraGeolocationDatabases($dbFilename, $dbType); 349 } catch (Exception $ex) { 350 // remove downloaded files 351 if (file_exists($outputPath)) { 352 unlink($outputPath); 353 } 354 unlink($path); 355 356 throw $ex; 357 } 358 } 359 360 private static function renameAnyExtraGeolocationDatabases($dbFilename, $dbType) 361 { 362 if (!in_array($dbFilename, LocationProviderGeoIp2::$dbNames[$dbType])) { 363 return; 364 } 365 366 $logger = StaticContainer::get(LoggerInterface::class); 367 foreach (LocationProviderGeoIp2::$dbNames[$dbType] as $possibleName) { 368 if ($dbFilename == $possibleName) { 369 break; 370 } 371 372 $pathToExistingFile = LocationProviderGeoIp2::getPathForGeoIpDatabase($possibleName); 373 if (file_exists($pathToExistingFile)) { 374 $newFilename = $pathToExistingFile . '.' . time() . '.old'; 375 $logger->info("Renaming old geolocation database file {old} to {rename} so new downloaded file {new} will be used.", [ 376 'old' => $possibleName, 377 'rename' => $newFilename, 378 'new' => $dbFilename, 379 ]); 380 381 rename($pathToExistingFile, $newFilename); // adding timestamp to avoid any potential race conditions 382 } 383 } 384 } 385 386 /** 387 * Sets the options used by this class based on query parameter values. 388 * 389 * See setUpdaterOptions for query params used. 390 */ 391 public static function setUpdaterOptionsFromUrl() 392 { 393 $options = array( 394 'loc' => Common::getRequestVar('loc_db', false, 'string'), 395 'isp' => Common::getRequestVar('isp_db', false, 'string'), 396 'period' => Common::getRequestVar('period', false, 'string'), 397 ); 398 399 foreach (self::$urlOptions as $optionKey => $optionName) { 400 $options[$optionKey] = Common::unsanitizeInputValue($options[$optionKey]); // URLs should not be sanitized 401 } 402 403 self::setUpdaterOptions($options); 404 } 405 406 /** 407 * Sets the options used by this class based on the elements in $options. 408 * 409 * The following elements of $options are used: 410 * 'loc' - URL for location database. 411 * 'isp' - URL for ISP database. 412 * 'org' - URL for Organization database. 413 * 'period' - 'weekly' or 'monthly'. When to run the updates. 414 * 415 * @param array $options 416 * @throws Exception 417 */ 418 public static function setUpdaterOptions($options) 419 { 420 // set url options 421 foreach (self::$urlOptions as $optionKey => $optionName) { 422 if (!isset($options[$optionKey])) { 423 continue; 424 } 425 426 $url = $options[$optionKey]; 427 $url = self::removeDateFromUrl($url); 428 429 self::checkGeoIPUpdateUrl($url); 430 431 Option::set($optionName, $url); 432 } 433 434 // set period option 435 if (!empty($options['period'])) { 436 $period = $options['period']; 437 438 if ($period != self::SCHEDULE_PERIOD_MONTHLY 439 && $period != self::SCHEDULE_PERIOD_WEEKLY 440 ) { 441 throw new Exception(Piwik::translate( 442 'GeoIp2_InvalidGeoIPUpdatePeriod', 443 array("'$period'", "'" . self::SCHEDULE_PERIOD_MONTHLY . "', '" . self::SCHEDULE_PERIOD_WEEKLY . "'") 444 )); 445 } 446 447 Option::set(self::SCHEDULE_PERIOD_OPTION_NAME, $period); 448 449 /** @var Scheduler $scheduler */ 450 $scheduler = StaticContainer::getContainer()->get('Piwik\Scheduler\Scheduler'); 451 452 $scheduler->rescheduleTaskAndRunTomorrow(new GeoIP2AutoUpdater()); 453 } 454 } 455 456 protected static function checkGeoIPUpdateUrl($url) 457 { 458 if (empty($url)) { 459 return; 460 } 461 462 $parsedUrl = @parse_url($url); 463 $schema = $parsedUrl['scheme'] ?? ''; 464 $host = $parsedUrl['host'] ?? ''; 465 466 if (empty($schema) || empty($host) || !in_array(mb_strtolower($schema), ['http', 'https'])) { 467 throw new Exception(Piwik::translate('GeoIp2_MalFormedUpdateUrl', '<i>'.Common::sanitizeInputValue($url).'</i>')); 468 } 469 470 $validHosts = Config::getInstance()->General['geolocation_download_from_trusted_hosts']; 471 $isValidHost = false; 472 473 foreach ($validHosts as $validHost) { 474 if (preg_match('/(^|\.)' . preg_quote($validHost) . '$/i', $host)) { 475 $isValidHost = true; 476 break; 477 } 478 } 479 480 if (true !== $isValidHost) { 481 throw new Exception(Piwik::translate('GeoIp2_InvalidGeoIPUpdateHost', [ 482 '<i>'.$url.'</i>', '<i>'.implode(', ', $validHosts).'</i>', '<i>geolocation_download_from_trusted_hosts</i>' 483 ])); 484 } 485 } 486 487 /** 488 * Returns true if the auto-updater is setup to update at least one type of 489 * database. False if otherwise. 490 * 491 * @return bool 492 */ 493 public static function isUpdaterSetup() 494 { 495 if (Option::get(self::LOC_URL_OPTION_NAME) !== false 496 || Option::get(self::ISP_URL_OPTION_NAME) !== false 497 ) { 498 return true; 499 } 500 501 return false; 502 } 503 504 /** 505 * Retrieves the URLs used to update various GeoIP 2 database files. 506 * 507 * @return array 508 */ 509 public static function getConfiguredUrls() 510 { 511 $result = array(); 512 foreach (self::$urlOptions as $key => $optionName) { 513 $result[$key] = Option::get($optionName); 514 } 515 return $result; 516 } 517 518 /** 519 * Returns the confiured URL (if any) for a type of database. 520 * 521 * @param string $key 'loc', 'isp' or 'org' 522 * @throws Exception 523 * @return string|false 524 */ 525 public static function getConfiguredUrl($key) 526 { 527 if (empty(self::$urlOptions[$key])) { 528 throw new Exception("Invalid key $key"); 529 } 530 $url = Option::get(self::$urlOptions[$key]); 531 return $url; 532 } 533 534 /** 535 * Performs a GeoIP 2 database update. 536 */ 537 public static function performUpdate() 538 { 539 $instance = new GeoIP2AutoUpdater(); 540 $instance->update(); 541 } 542 543 /** 544 * Returns the configured update period, either 'week' or 'month'. Defaults to 545 * 'month'. 546 * 547 * @return string 548 */ 549 public static function getSchedulePeriod() 550 { 551 $period = Option::get(self::SCHEDULE_PERIOD_OPTION_NAME); 552 if ($period === false) { 553 $period = self::SCHEDULE_PERIOD_MONTHLY; 554 } 555 return $period; 556 } 557 558 /** 559 * Returns an array of strings for GeoIP 2 databases that have update URLs configured, but 560 * are not present in the misc directory. Each string is a key describing the type of 561 * database (ie, 'loc', 'isp' or 'org'). 562 * 563 * @return array 564 */ 565 public static function getMissingDatabases() 566 { 567 $result = array(); 568 foreach (self::getConfiguredUrls() as $key => $url) { 569 if (!empty($url)) { 570 // if a database of the type does not exist, but there's a url to update, then 571 // a database is missing 572 $path = LocationProviderGeoIp2::getPathToGeoIpDatabase( 573 LocationProviderGeoIp2::$dbNames[$key]); 574 if ($path === false) { 575 $result[] = $key; 576 } 577 } 578 } 579 return $result; 580 } 581 582 /** 583 * Returns the extension of a URL used to update a GeoIP 2 database, if it can be found. 584 */ 585 public static function getGeoIPUrlExtension($url) 586 { 587 // check for &suffix= query param that is special to MaxMind URLs 588 if (preg_match('/suffix=([^&]+)/', $url, $matches)) { 589 $ext = $matches[1]; 590 } else { 591 // use basename of url 592 $filenameParts = explode('.', basename($url), 2); 593 if (count($filenameParts) > 1) { 594 $ext = end($filenameParts); 595 } else { 596 $ext = reset($filenameParts); 597 } 598 } 599 600 if ('mmdb.gz' === $ext) { 601 $ext = 'gz'; 602 } 603 604 self::checkForSupportedArchiveType($url, $ext); 605 606 return $ext; 607 } 608 609 /** 610 * Avoid downloading archive types we don't support. No point in downloading it, 611 * if we can't unzip it... 612 * 613 * @param string $ext The URL file's extension. 614 * @throws \Exception 615 */ 616 private static function checkForSupportedArchiveType($url, $ext) 617 { 618 if ($ext === 'mmdb' && self::isDbIpUrl($url)) { 619 return; 620 } 621 622 if ($ext != 'tar.gz' 623 && $ext != 'gz' 624 && $ext != 'mmdb.gz' 625 ) { 626 throw new \Exception(Piwik::translate('GeoIp2_UnsupportedArchiveType', "'$ext'")); 627 } 628 } 629 630 /** 631 * Utility function that checks if geolocation works with each installed database, 632 * and if one or more doesn't, they are renamed to make sure tracking will work. 633 * This is a safety measure used to make sure tracking isn't affected if strange 634 * update errors occur. 635 * 636 * Databases are renamed to ${original}.broken . 637 * 638 * Note: method is protected for testability. 639 * 640 * @param $logErrors - only used to hide error logs during tests 641 */ 642 protected function performRedundantDbChecks($logErrors = true) 643 { 644 $databaseTypes = array_keys(LocationProviderGeoIp2::$dbNames); 645 646 foreach ($databaseTypes as $type) { 647 $customNames = array( 648 'loc' => array(), 649 'isp' => array(), 650 'org' => array() 651 ); 652 $customNames[$type] = LocationProviderGeoIp2::$dbNames[$type]; 653 654 // create provider that only uses the DB type we're testing 655 $provider = new Php($customNames); 656 657 // test the provider. on error, we rename the broken DB. 658 try { 659 // check database directly, as location provider ignores invalid database errors 660 $pathToDb = LocationProviderGeoIp2::getPathToGeoIpDatabase($customNames[$type]); 661 662 if (empty($pathToDb)) { 663 continue; // skip, as no database for this type is available 664 } 665 666 $reader = new Reader($pathToDb); 667 668 $location = $provider->getLocation(array('ip' => LocationProviderGeoIp2::TEST_IP)); 669 unset($provider, $reader); 670 } catch (\Exception $e) { 671 if ($logErrors) { 672 Log::error("GeoIP2AutoUpdater: Encountered exception when performing redundant tests on GeoIP2 " 673 . "%s database: %s", $type, $e->getMessage()); 674 } 675 676 // get the current filename for the DB and an available new one to rename it to 677 [$oldPath, $newPath] = $this->getOldAndNewPathsForBrokenDb($customNames[$type]); 678 679 // rename the DB so tracking will not fail 680 if ($oldPath !== false 681 && $newPath !== false 682 ) { 683 if (file_exists($newPath)) { 684 unlink($newPath); 685 } 686 687 rename($oldPath, $newPath); 688 } 689 } 690 } 691 } 692 693 /** 694 * Returns the path to a GeoIP 2 database and a path to rename it to if it's broken. 695 * 696 * @param array $possibleDbNames The possible names of the database. 697 * @return array Array with two elements, the path to the existing database, and 698 * the path to rename it to if it is broken. The second will end 699 * with something like .broken . 700 */ 701 private function getOldAndNewPathsForBrokenDb($possibleDbNames) 702 { 703 $pathToDb = LocationProviderGeoIp2::getPathToGeoIpDatabase($possibleDbNames); 704 $newPath = false; 705 706 if ($pathToDb !== false) { 707 $newPath = $pathToDb . ".broken"; 708 } 709 710 return array($pathToDb, $newPath); 711 } 712 713 /** 714 * Returns the time the auto updater was last run. 715 * 716 * @return Date|false 717 */ 718 public static function getLastRunTime() 719 { 720 $timestamp = Option::get(self::LAST_RUN_TIME_OPTION_NAME); 721 return $timestamp === false ? false : Date::factory((int)$timestamp); 722 } 723 724 /** 725 * Removes the &date=... query parameter if present in the URL. This query parameter 726 * is in MaxMind URLs by default and will force the download of an old database. 727 * 728 * @param string $url 729 * @return string 730 */ 731 private static function removeDateFromUrl($url) 732 { 733 return preg_replace("/&date=[^&#]*/", '', $url); 734 } 735 736 /** 737 * Returns the next scheduled time for the auto updater. 738 * 739 * @return Date|false 740 */ 741 public static function getNextRunTime() 742 { 743 $task = new GeoIP2AutoUpdater(); 744 745 $timetable = new Timetable(); 746 return $timetable->getScheduledTaskTime($task->getName()); 747 } 748 749 /** 750 * See {@link \Piwik\Scheduler\Schedule\Schedule::getRescheduledTime()}. 751 */ 752 public function getRescheduledTime() 753 { 754 $nextScheduledTime = parent::getRescheduledTime(); 755 756 // if a geoip 2 database is out of date, run the updater as soon as possible 757 if ($this->isAtLeastOneGeoIpDbOutOfDate($nextScheduledTime)) { 758 return time(); 759 } 760 761 return $nextScheduledTime; 762 } 763 764 private function isAtLeastOneGeoIpDbOutOfDate($rescheduledTime) 765 { 766 $previousScheduledRuntime = $this->getPreviousScheduledTime($rescheduledTime)->setTime("00:00:00")->getTimestamp(); 767 768 foreach (LocationProviderGeoIp2::$dbNames as $type => $dbNames) { 769 $dbUrl = Option::get(self::$urlOptions[$type]); 770 $dbPath = LocationProviderGeoIp2::getPathToGeoIpDatabase($dbNames); 771 772 // if there is a URL for this DB type and the GeoIP 2 DB file's last modified time is before 773 // the time the updater should have been previously run, then **the file is out of date** 774 if (!empty($dbUrl) 775 && filemtime($dbPath) < $previousScheduledRuntime 776 ) { 777 return true; 778 } 779 } 780 781 return false; 782 } 783 784 private function getPreviousScheduledTime($rescheduledTime) 785 { 786 $updaterPeriod = self::getSchedulePeriod(); 787 788 if ($updaterPeriod == self::SCHEDULE_PERIOD_WEEKLY) { 789 return Date::factory($rescheduledTime)->subWeek(1); 790 } else if ($updaterPeriod == self::SCHEDULE_PERIOD_MONTHLY) { 791 return Date::factory($rescheduledTime)->subMonth(1); 792 } 793 throw new Exception("Unknown GeoIP 2 updater period found in database: %s", $updaterPeriod); 794 } 795 796 public static function getZippedFilenameToDownloadTo($url, $dbType, $ext) 797 { 798 if (self::isDbIpUrl($url)) { 799 if (preg_match('/(dbip-[a-zA-Z0-9_]+)(?:-lite)?-\d{4}-\d{2}/', $url, $matches)) { 800 $parts = explode('-', $matches[1]); 801 $dbName = $parts[1] === 'asn' ? 'ASN' : ucfirst($parts[1]); 802 return strtoupper($parts[0]) . '-' . $dbName . '.mmdb.' . $ext; 803 } else { 804 return basename($url); 805 } 806 } 807 808 return LocationProviderGeoIp2::$dbNames[$dbType][0] . '.' . $ext; 809 } 810 811 protected function getDbIpUrlWithLatestDate($url) 812 { 813 $today = Date::today(); 814 return preg_replace('/-\d{4}-\d{2}\./', '-' . $today->toString('Y-m') . '.', $url); 815 } 816 817 public static function isDbIpUrl($url) 818 { 819 return !! preg_match('/^http[s]?:\/\/([a-z0-9-]+\.)?db-ip\.com/', $url); 820 } 821 822 protected static function isPaidDbIpUrl($url) 823 { 824 return !! preg_match('/^http[s]?:\/\/([a-z0-9-]+\.)?db-ip\.com\/account\/[0-9a-z]+\/db/', $url); 825 } 826 827 protected function fetchPaidDbIpUrl($url) 828 { 829 $content = trim($this->fetchUrl($url)); 830 831 if (0 === strpos($content, 'http')) { 832 return $content; 833 } 834 835 $content = json_decode($content, true); 836 837 if (!empty($content['mmdb']['url'])) { 838 return $content['mmdb']['url']; 839 } 840 841 if (!empty($content['url'])) { 842 return $content['url']; 843 } 844 845 throw new Exception('Unable to determine download url'); 846 } 847 848 protected function fetchUrl($url) 849 { 850 return Http::fetchRemoteFile($url); 851 } 852 853 /** 854 * Updates the DB-IP URL option value so that users see 855 * the updated link in the "Download URL" field on the plugin page 856 * instead of the one that was set when Matomo was installed months 857 * or even years ago. 858 * 859 * @param string $option The option to check and update: either 860 * self::LOC_URL_OPTION_NAME or self::ISP_URL_OPTION_NAME 861 */ 862 protected function updateDbIpUrlOption(string $option): void 863 { 864 if ($option !== self::LOC_URL_OPTION_NAME && $option !== self::ISP_URL_OPTION_NAME) 865 { 866 return; 867 } 868 869 $url = trim(Option::get($option)); 870 871 if (self::isDbIpUrl($url)) { 872 $latestUrl = $this->getDbIpUrlWithLatestDate($url); 873 874 if($url !== $latestUrl) { 875 Option::set($option, $latestUrl); 876 } 877 } 878 } 879} 880