diff --git a/composer.json b/composer.json index 3e0112c14ab..83dce1fda06 100644 --- a/composer.json +++ b/composer.json @@ -7,14 +7,15 @@ "prefer-stable": true, "minimum-stability": "dev", "require": { - "automattic/jetpack-autoloader": "^1.2.0", "php": ">=5.6|>=7.0", + "automattic/jetpack-autoloader": "^1.2.0", "composer/installers": "1.7.0", - "woocommerce/woocommerce-blocks": "2.5.7", - "woocommerce/woocommerce-rest-api": "1.0.5" + "maxmind-db/reader": "1.6.0", + "woocommerce/woocommerce-blocks": "2.5.10", + "woocommerce/woocommerce-rest-api": "1.0.6" }, "require-dev": { - "phpunit/phpunit": "7.5.18", + "phpunit/phpunit": "7.5.20", "woocommerce/woocommerce-sniffs": "0.0.9" }, "config": { diff --git a/composer.lock b/composer.lock index 8426538364e..1af8da387da 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "95355370e5e250e500f8114896d52f7a", + "content-hash": "316f960abc40df62fbc75b77930323fc", "packages": [ { "name": "automattic/jetpack-autoloader", @@ -165,17 +165,77 @@ "time": "2019-08-12T15:00:31+00:00" }, { - "name": "woocommerce/woocommerce-blocks", - "version": "v2.5.7", + "name": "maxmind-db/reader", + "version": "v1.6.0", "source": { "type": "git", - "url": "https://github.com/woocommerce/woocommerce-gutenberg-products-block.git", - "reference": "24b6552d38204fbbdd87ec5ba76f3ec391b042d0" + "url": "https://github.com/maxmind/MaxMind-DB-Reader-php.git", + "reference": "febd4920bf17c1da84cef58e56a8227dfb37fbe4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/woocommerce/woocommerce-gutenberg-products-block/zipball/24b6552d38204fbbdd87ec5ba76f3ec391b042d0", - "reference": "24b6552d38204fbbdd87ec5ba76f3ec391b042d0", + "url": "https://api.github.com/repos/maxmind/MaxMind-DB-Reader-php/zipball/febd4920bf17c1da84cef58e56a8227dfb37fbe4", + "reference": "febd4920bf17c1da84cef58e56a8227dfb37fbe4", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "conflict": { + "ext-maxminddb": "<1.6.0,>=2.0.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "2.*", + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpcov": "^3.0", + "phpunit/phpunit": "5.*", + "squizlabs/php_codesniffer": "3.*" + }, + "suggest": { + "ext-bcmath": "bcmath or gmp is required for decoding larger integers with the pure PHP decoder", + "ext-gmp": "bcmath or gmp is required for decoding larger integers with the pure PHP decoder", + "ext-maxminddb": "A C-based database decoder that provides significantly faster lookups" + }, + "type": "library", + "autoload": { + "psr-4": { + "MaxMind\\Db\\": "src/MaxMind/Db" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Gregory J. Oschwald", + "email": "goschwald@maxmind.com", + "homepage": "https://www.maxmind.com/" + } + ], + "description": "MaxMind DB Reader API", + "homepage": "https://github.com/maxmind/MaxMind-DB-Reader-php", + "keywords": [ + "database", + "geoip", + "geoip2", + "geolocation", + "maxmind" + ], + "time": "2019-12-19T22:59:03+00:00" + }, + { + "name": "woocommerce/woocommerce-blocks", + "version": "v2.5.10", + "source": { + "type": "git", + "url": "https://github.com/woocommerce/woocommerce-gutenberg-products-block.git", + "reference": "4a6d993c1df7ccd8581873ee56269efa00d49ddc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/woocommerce/woocommerce-gutenberg-products-block/zipball/4a6d993c1df7ccd8581873ee56269efa00d49ddc", + "reference": "4a6d993c1df7ccd8581873ee56269efa00d49ddc", "shasum": "" }, "require": { @@ -209,20 +269,20 @@ "gutenberg", "woocommerce" ], - "time": "2019-12-20T16:26:08+00:00" + "time": "2020-01-09T15:29:03+00:00" }, { "name": "woocommerce/woocommerce-rest-api", - "version": "1.0.5", + "version": "1.0.6", "source": { "type": "git", "url": "https://github.com/woocommerce/woocommerce-rest-api.git", - "reference": "3be425631faefa61ab8b81011ae8a422b9bfca35" + "reference": "78ccf4d4c6bafbc841182b68aa863e7b0caa37c8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/woocommerce/woocommerce-rest-api/zipball/3be425631faefa61ab8b81011ae8a422b9bfca35", - "reference": "3be425631faefa61ab8b81011ae8a422b9bfca35", + "url": "https://api.github.com/repos/woocommerce/woocommerce-rest-api/zipball/78ccf4d4c6bafbc841182b68aa863e7b0caa37c8", + "reference": "78ccf4d4c6bafbc841182b68aa863e7b0caa37c8", "shasum": "" }, "require": { @@ -249,7 +309,7 @@ ], "description": "The WooCommerce core REST API.", "homepage": "https://github.com/woocommerce/woocommerce-rest-api", - "time": "2019-12-18T22:20:59+00:00" + "time": "2020-01-15T23:29:39+00:00" } ], "packages-dev": [ @@ -527,16 +587,16 @@ }, { "name": "phpcompatibility/php-compatibility", - "version": "9.3.4", + "version": "9.3.5", "source": { "type": "git", "url": "https://github.com/PHPCompatibility/PHPCompatibility.git", - "reference": "1f37659196e4f3113ea506a7efba201c52303bf1" + "reference": "9fb324479acf6f39452e0655d2429cc0d3914243" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/1f37659196e4f3113ea506a7efba201c52303bf1", - "reference": "1f37659196e4f3113ea506a7efba201c52303bf1", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/9fb324479acf6f39452e0655d2429cc0d3914243", + "reference": "9fb324479acf6f39452e0655d2429cc0d3914243", "shasum": "" }, "require": { @@ -581,7 +641,7 @@ "phpcs", "standards" ], - "time": "2019-11-15T04:12:02+00:00" + "time": "2019-12-27T09:44:58+00:00" }, { "name": "phpcompatibility/phpcompatibility-paragonie", @@ -739,16 +799,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "4.3.2", + "version": "4.3.4", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "b83ff7cfcfee7827e1e78b637a5904fe6a96698e" + "reference": "da3fd972d6bafd628114f7e7e036f45944b62e9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/b83ff7cfcfee7827e1e78b637a5904fe6a96698e", - "reference": "b83ff7cfcfee7827e1e78b637a5904fe6a96698e", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/da3fd972d6bafd628114f7e7e036f45944b62e9c", + "reference": "da3fd972d6bafd628114f7e7e036f45944b62e9c", "shasum": "" }, "require": { @@ -760,6 +820,7 @@ "require-dev": { "doctrine/instantiator": "^1.0.5", "mockery/mockery": "^1.0", + "phpdocumentor/type-resolver": "0.4.*", "phpunit/phpunit": "^6.4" }, "type": "library", @@ -786,7 +847,7 @@ } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2019-09-12T14:27:41+00:00" + "time": "2019-12-28T18:55:12+00:00" }, { "name": "phpdocumentor/type-resolver", @@ -837,16 +898,16 @@ }, { "name": "phpspec/prophecy", - "version": "1.10.0", + "version": "1.10.1", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "d638ebbb58daba25a6a0dc7969e1358a0e3c6682" + "reference": "cbe1df668b3fe136bcc909126a0f529a78d4cbbc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/d638ebbb58daba25a6a0dc7969e1358a0e3c6682", - "reference": "d638ebbb58daba25a6a0dc7969e1358a0e3c6682", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/cbe1df668b3fe136bcc909126a0f529a78d4cbbc", + "reference": "cbe1df668b3fe136bcc909126a0f529a78d4cbbc", "shasum": "" }, "require": { @@ -896,7 +957,7 @@ "spy", "stub" ], - "time": "2019-12-17T16:54:23+00:00" + "time": "2019-12-22T21:05:45+00:00" }, { "name": "phpunit/php-code-coverage", @@ -1152,16 +1213,16 @@ }, { "name": "phpunit/phpunit", - "version": "7.5.18", + "version": "7.5.20", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "fcf6c4bfafaadc07785528b06385cce88935474d" + "reference": "9467db479d1b0487c99733bb1e7944d32deded2c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fcf6c4bfafaadc07785528b06385cce88935474d", - "reference": "fcf6c4bfafaadc07785528b06385cce88935474d", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9467db479d1b0487c99733bb1e7944d32deded2c", + "reference": "9467db479d1b0487c99733bb1e7944d32deded2c", "shasum": "" }, "require": { @@ -1232,7 +1293,7 @@ "testing", "xunit" ], - "time": "2019-12-06T05:14:37+00:00" + "time": "2020-01-08T08:45:45+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", diff --git a/includes/admin/class-wc-admin-notices.php b/includes/admin/class-wc-admin-notices.php index 13a5cc05658..76ba356bd6c 100644 --- a/includes/admin/class-wc-admin-notices.php +++ b/includes/admin/class-wc-admin-notices.php @@ -36,6 +36,7 @@ class WC_Admin_Notices { 'no_secure_connection' => 'secure_connection_notice', 'wc_admin' => 'wc_admin_feature_plugin_notice', WC_PHP_MIN_REQUIREMENTS_NOTICE => 'wp_php_min_requirements_notice', + 'maxmind_license_key' => 'maxmind_missing_license_key_notice', ); /** @@ -87,6 +88,7 @@ class WC_Admin_Notices { self::add_wc_admin_feature_plugin_notice(); self::add_notice( 'template_files' ); self::add_min_version_notice(); + self::add_maxmind_missing_license_key_notice(); } /** @@ -428,6 +430,41 @@ class WC_Admin_Notices { include dirname( __FILE__ ) . '/views/html-notice-wp-php-minimum-requirements.php'; } + /** + * Add MaxMind missing license key notice. + * + * @since 3.9.0 + */ + public static function add_maxmind_missing_license_key_notice() { + $default_address = get_option( 'woocommerce_default_customer_address' ); + + if ( ! in_array( $default_address, array( 'geolocation', 'geolocation_ajax' ), true ) ) { + return; + } + + $integration_options = get_option( 'woocommerce_maxmind_geolocation_settings' ); + if ( empty( $integration_options['license_key'] ) ) { + self::add_notice( 'maxmind_license_key' ); + } + } + + /** + * Display MaxMind missing license key notice. + * + * @since 3.9.0 + */ + public static function maxmind_missing_license_key_notice() { + $user_dismissed_notice = get_user_meta( get_current_user_id(), 'dismissed_maxmind_license_key_notice', true ); + $filter_dismissed_notice = ! apply_filters( 'woocommerce_maxmind_geolocation_display_notices', true ); + + if ( $user_dismissed_notice || $filter_dismissed_notice ) { + self::remove_notice( 'maxmind_license_key' ); + return; + } + + include dirname( __FILE__ ) . '/views/html-notice-maxmind-license-key.php'; + } + /** * Determine if the store is running SSL. * diff --git a/includes/admin/settings/class-wc-settings-general.php b/includes/admin/settings/class-wc-settings-general.php index 44c4dc7b51b..6e0126b00da 100644 --- a/includes/admin/settings/class-wc-settings-general.php +++ b/includes/admin/settings/class-wc-settings-general.php @@ -39,17 +39,6 @@ class WC_Settings_General extends WC_Settings_Page { $currency_code_options[ $code ] = $name . ' (' . get_woocommerce_currency_symbol( $code ) . ')'; } - $woocommerce_default_customer_address_options = array( - '' => __( 'No location by default', 'woocommerce' ), - 'base' => __( 'Shop base address', 'woocommerce' ), - 'geolocation' => __( 'Geolocate', 'woocommerce' ), - 'geolocation_ajax' => __( 'Geolocate (with page caching support)', 'woocommerce' ), - ); - - if ( version_compare( PHP_VERSION, '5.4', '<' ) ) { - unset( $woocommerce_default_customer_address_options['geolocation'], $woocommerce_default_customer_address_options['geolocation_ajax'] ); - } - $settings = apply_filters( 'woocommerce_general_settings', array( @@ -182,10 +171,15 @@ class WC_Settings_General extends WC_Settings_Page { 'title' => __( 'Default customer location', 'woocommerce' ), 'id' => 'woocommerce_default_customer_address', 'desc_tip' => __( 'This option determines a customers default location. The MaxMind GeoLite Database will be periodically downloaded to your wp-content directory if using geolocation.', 'woocommerce' ), - 'default' => 'geolocation', + 'default' => 'base', 'type' => 'select', 'class' => 'wc-enhanced-select', - 'options' => $woocommerce_default_customer_address_options, + 'options' => array( + '' => __( 'No location by default', 'woocommerce' ), + 'base' => __( 'Shop base address', 'woocommerce' ), + 'geolocation' => __( 'Geolocate', 'woocommerce' ), + 'geolocation_ajax' => __( 'Geolocate (with page caching support)', 'woocommerce' ), + ), ), array( diff --git a/includes/admin/views/html-admin-page-status-report.php b/includes/admin/views/html-admin-page-status-report.php index 337aca7cd45..729882eab2f 100644 --- a/includes/admin/views/html-admin-page-status-report.php +++ b/includes/admin/views/html-admin-page-status-report.php @@ -435,25 +435,6 @@ $untested_plugins = $plugin_updates->get_untested_plugins( WC()->version, 'min - - - : - - - ' . wp_kses_post( __( 'MaxMind GeoIP database requires at least PHP 5.4.', 'woocommerce' ) ) . ''; - } elseif ( file_exists( $database['maxmind_geoip_database'] ) ) { - echo ' ' . esc_html( $database['maxmind_geoip_database'] ) . ' '; - } else { - /* Translators: %1$s: Library url, %2$s: install path. */ - printf( ' ' . sprintf( esc_html__( 'The MaxMind GeoIP Database does not exist - Geolocation will not function. You can download and install it manually from %1$s to the path: %2$s. Scroll down to "Downloads" and download the "MaxMind DB binary, gzipped" file next to "GeoLite2 Country". Please remember to uncompress GeoLite2-Country_xxxxxxxx.tar.gz and upload the GeoLite2-Country.mmdb file only.', 'woocommerce' ), 'https://dev.maxmind.com/geoip/geoip2/geolite2/', '' . esc_html( $database['maxmind_geoip_database'] ) . '' ) . '', esc_html( WC_LOG_DIR ) ); - } - ?> - - - - @@ -584,7 +565,7 @@ $untested_plugins = $plugin_updates->get_untested_plugins( WC()->version, 'min get_untested_plugins( WC()->version, 'min ' . $plugin_name . ''; diff --git a/includes/admin/views/html-notice-maxmind-license-key.php b/includes/admin/views/html-notice-maxmind-license-key.php new file mode 100644 index 00000000000..1ae8c8132f1 --- /dev/null +++ b/includes/admin/views/html-notice-maxmind-license-key.php @@ -0,0 +1,31 @@ + + +
+ + +

+ +

+ +

+ MaxMind integration settings page in order to use the geolocation service. If you do not need geolocation for shipping or taxes, you should change the default customer location on the general settings page.', 'woocommerce' ), + admin_url( 'admin.php?page=wc-settings&tab=integration§ion=maxmind_geolocation' ), + admin_url( 'admin.php?page=wc-settings&tab=general' ) + ) + ); + ?> +

+
diff --git a/includes/class-wc-autoloader.php b/includes/class-wc-autoloader.php index dd287411089..502a5f63f18 100644 --- a/includes/class-wc-autoloader.php +++ b/includes/class-wc-autoloader.php @@ -88,6 +88,8 @@ class WC_Autoloader { $path = $this->include_path . 'payment-tokens/'; } elseif ( 0 === strpos( $class, 'wc_log_handler_' ) ) { $path = $this->include_path . 'log-handlers/'; + } elseif ( 0 === strpos( $class, 'wc_integration' ) ) { + $path = $this->include_path . 'integrations/' . substr( str_replace( '_', '-', $class ), 15 ) . '/'; } if ( empty( $path ) || ! $this->load_file( $path . $file ) ) { diff --git a/includes/class-wc-geolite-integration.php b/includes/class-wc-geolite-integration.php index a5a33b8eead..c661aa08d0c 100644 --- a/includes/class-wc-geolite-integration.php +++ b/includes/class-wc-geolite-integration.php @@ -8,12 +8,15 @@ * * @package WooCommerce\Classes * @since 3.4.0 + * @deprecated 3.9.0 */ defined( 'ABSPATH' ) || exit; /** * Geolite integration class. + * + * @deprecated 3.9.0 */ class WC_Geolite_Integration { @@ -38,10 +41,6 @@ class WC_Geolite_Integration { */ public function __construct( $database ) { $this->database = $database; - - if ( ! class_exists( 'MaxMind\\Db\\Reader', false ) ) { - $this->require_geolite_library(); - } } /** @@ -50,8 +49,11 @@ class WC_Geolite_Integration { * * @param string $ip_address User IP address. * @return string + * @deprecated 3.9.0 */ public function get_country_iso( $ip_address ) { + wc_deprecated_function( 'get_country_iso', '3.9.0' ); + $iso_code = ''; try { @@ -87,15 +89,4 @@ class WC_Geolite_Integration { $this->log->log( $level, $message, array( 'source' => 'geoip' ) ); } - - /** - * Require geolite library. - */ - private function require_geolite_library() { - require_once WC_ABSPATH . 'includes/libraries/geolite2/Reader/Decoder.php'; - require_once WC_ABSPATH . 'includes/libraries/geolite2/Reader/InvalidDatabaseException.php'; - require_once WC_ABSPATH . 'includes/libraries/geolite2/Reader/Metadata.php'; - require_once WC_ABSPATH . 'includes/libraries/geolite2/Reader/Util.php'; - require_once WC_ABSPATH . 'includes/libraries/geolite2/Reader.php'; - } } diff --git a/includes/class-wc-geolocation.php b/includes/class-wc-geolocation.php index efcc86c15c4..64cf628f956 100644 --- a/includes/class-wc-geolocation.php +++ b/includes/class-wc-geolocation.php @@ -7,7 +7,7 @@ * This product includes GeoLite data created by MaxMind, available from http://www.maxmind.com. * * @package WooCommerce/Classes - * @version 3.4.0 + * @version 3.9.0 */ defined( 'ABSPATH' ) || exit; @@ -35,6 +35,7 @@ class WC_Geolocation { * GeoLite2 DB. * * @since 3.4.0 + * @deprecated 3.9.0 */ const GEOLITE2_DB = 'http://geolite.maxmind.com/download/geoip/database/GeoLite2-Country.tar.gz'; @@ -60,16 +61,6 @@ class WC_Geolocation { 'ip-api.com' => 'http://ip-api.com/json/%s', ); - /** - * Check if server supports MaxMind GeoLite2 Reader. - * - * @since 3.4.0 - * @return bool - */ - private static function supports_geolite2() { - return version_compare( PHP_VERSION, '5.4.0', '>=' ); - } - /** * Check if geolocation is enabled. * @@ -81,67 +72,20 @@ class WC_Geolocation { return in_array( $current_settings, array( 'geolocation', 'geolocation_ajax' ), true ); } - /** - * Prevent geolocation via MaxMind when using legacy versions of php. - * - * @since 3.4.0 - * @param string $default_customer_address current value. - * @return string - */ - public static function disable_geolocation_on_legacy_php( $default_customer_address ) { - if ( self::is_geolocation_enabled( $default_customer_address ) ) { - $default_customer_address = 'base'; - } - - return $default_customer_address; - } - - /** - * Hook in geolocation functionality. - */ - public static function init() { - if ( self::supports_geolite2() ) { - // Only download the database from MaxMind if the geolocation function is enabled, or a plugin specifically requests it. - if ( self::is_geolocation_enabled( get_option( 'woocommerce_default_customer_address' ) ) || apply_filters( 'woocommerce_geolocation_update_database_periodically', false ) ) { - add_action( 'woocommerce_geoip_updater', array( __CLASS__, 'update_database' ) ); - } - - // Trigger database update when settings are changed to enable geolocation. - add_filter( 'pre_update_option_woocommerce_default_customer_address', array( __CLASS__, 'maybe_update_database' ), 10, 2 ); - } else { - add_filter( 'pre_option_woocommerce_default_customer_address', array( __CLASS__, 'disable_geolocation_on_legacy_php' ) ); - } - } - - /** - * Maybe trigger a DB update for the first time. - * - * @param string $new_value New value. - * @param string $old_value Old value. - * @return string - */ - public static function maybe_update_database( $new_value, $old_value ) { - if ( $new_value !== $old_value && self::is_geolocation_enabled( $new_value ) ) { - self::update_database(); - } - - return $new_value; - } - /** * Get current user IP Address. * * @return string */ public static function get_ip_address() { - if ( isset( $_SERVER['HTTP_X_REAL_IP'] ) ) { // WPCS: input var ok, CSRF ok. - return sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_REAL_IP'] ) ); // WPCS: input var ok, CSRF ok. - } elseif ( isset( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) { // WPCS: input var ok, CSRF ok. + if ( isset( $_SERVER['HTTP_X_REAL_IP'] ) ) { + return sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_REAL_IP'] ) ); + } elseif ( isset( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) { // Proxy servers can send through this header like this: X-Forwarded-For: client1, proxy1, proxy2 // Make sure we always only send through the first IP in the list which should always be the client IP. - return (string) rest_is_ip_address( trim( current( preg_split( '/,/', sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) ) ) ) ); // WPCS: input var ok, CSRF ok. - } elseif ( isset( $_SERVER['REMOTE_ADDR'] ) ) { // @codingStandardsIgnoreLine - return sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ); // @codingStandardsIgnoreLine + return (string) rest_is_ip_address( trim( current( preg_split( '/,/', sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) ) ) ) ); + } elseif ( isset( $_SERVER['REMOTE_ADDR'] ) ) { + return sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ); } return ''; } @@ -195,119 +139,114 @@ class WC_Geolocation { // Filter to allow custom geolocation of the IP address. $country_code = apply_filters( 'woocommerce_geolocate_ip', false, $ip_address, $fallback, $api_fallback ); - if ( false === $country_code ) { - // If GEOIP is enabled in CloudFlare, we can use that (Settings -> CloudFlare Settings -> Settings Overview). - if ( ! empty( $_SERVER['HTTP_CF_IPCOUNTRY'] ) ) { // WPCS: input var ok, CSRF ok. - $country_code = strtoupper( sanitize_text_field( wp_unslash( $_SERVER['HTTP_CF_IPCOUNTRY'] ) ) ); // WPCS: input var ok, CSRF ok. - } elseif ( ! empty( $_SERVER['GEOIP_COUNTRY_CODE'] ) ) { // WPCS: input var ok, CSRF ok. - // WP.com VIP has a variable available. - $country_code = strtoupper( sanitize_text_field( wp_unslash( $_SERVER['GEOIP_COUNTRY_CODE'] ) ) ); // WPCS: input var ok, CSRF ok. - } elseif ( ! empty( $_SERVER['HTTP_X_COUNTRY_CODE'] ) ) { // WPCS: input var ok, CSRF ok. - // VIP Go has a variable available also. - $country_code = strtoupper( sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_COUNTRY_CODE'] ) ) ); // WPCS: input var ok, CSRF ok. - } else { - $ip_address = $ip_address ? $ip_address : self::get_ip_address(); - $database = self::get_local_database_path(); + if ( false !== $country_code ) { + return array( + 'country' => $country_code, + 'state' => '', + 'city' => '', + 'postcode' => '', + ); + } - if ( self::supports_geolite2() && file_exists( $database ) ) { - $country_code = self::geolocate_via_db( $ip_address, $database ); - } elseif ( $api_fallback ) { - $country_code = self::geolocate_via_api( $ip_address ); - } else { - $country_code = ''; - } + if ( empty( $ip_address ) ) { + $ip_address = self::get_ip_address(); + } - if ( ! $country_code && $fallback ) { - // May be a local environment - find external IP. - return self::geolocate_ip( self::get_external_ip_address(), false, $api_fallback ); - } + $country_code = self::get_country_code_from_headers(); + + /** + * Get geolocation filter. + * + * @since 3.9.0 + * @param array $geolocation Geolocation data, including country, state, city, and postcode. + * @param string $ip_address IP Address. + */ + $geolocation = apply_filters( + 'woocommerce_get_geolocation', + array( + 'country' => $country_code, + 'state' => '', + 'city' => '', + 'postcode' => '', + ), + $ip_address + ); + + // If we still haven't found a country code, let's consider doing an API lookup. + if ( '' === $geolocation['country'] && $api_fallback ) { + $geolocation['country'] = self::geolocate_via_api( $ip_address ); + } + + // It's possible that we're in a local environment, in which case the geolocation needs to be done from the + // external address. + if ( '' === $geolocation['country'] && $fallback ) { + $external_ip_address = self::get_external_ip_address(); + + // Only bother with this if the external IP differs. + if ( '0.0.0.0' !== $external_ip_address && $external_ip_address !== $ip_address ) { + return self::geolocate_ip( $external_ip_address, false, $api_fallback ); } } return array( - 'country' => $country_code, - 'state' => '', + 'country' => $geolocation['country'], + 'state' => $geolocation['state'], + 'city' => $geolocation['city'], + 'postcode' => $geolocation['postcode'], ); } /** * Path to our local db. * + * @deprecated 3.9.0 * @param string $deprecated Deprecated since 3.4.0. * @return string */ public static function get_local_database_path( $deprecated = '2' ) { - return apply_filters( 'woocommerce_geolocation_local_database_path', WP_CONTENT_DIR . '/uploads/GeoLite2-Country.mmdb', $deprecated ); + wc_deprecated_function( 'WC_Geolocation::get_local_database_path', '3.9.0' ); + $integration = wc()->integrations->get_integration( 'maxmind_geolocation' ); + return $integration->get_database_service()->get_database_path(); } /** * Update geoip database. * + * @deprecated 3.9.0 * Extract files with PharData. Tool built into PHP since 5.3. */ public static function update_database() { - $logger = wc_get_logger(); - - if ( ! self::supports_geolite2() ) { - $logger->notice( 'Requires PHP 5.4 to be able to download MaxMind GeoLite2 database', array( 'source' => 'geolocation' ) ); - return; - } - - require_once ABSPATH . 'wp-admin/includes/file.php'; - - $database = 'GeoLite2-Country.mmdb'; - $target_database_path = self::get_local_database_path(); - $tmp_database_path = download_url( self::GEOLITE2_DB ); - - if ( ! is_wp_error( $tmp_database_path ) ) { - WP_Filesystem(); - - global $wp_filesystem; - - try { - // Make sure target dir exists. - $wp_filesystem->mkdir( dirname( $target_database_path ) ); - - // Extract files with PharData. Tool built into PHP since 5.3. - $file = new PharData( $tmp_database_path ); // phpcs:ignore PHPCompatibility.Classes.NewClasses.phardataFound - $file_path = trailingslashit( $file->current()->getFileName() ) . $database; - $file->extractTo( dirname( $tmp_database_path ), $file_path, true ); - - // Move file and delete temp. - $wp_filesystem->move( trailingslashit( dirname( $tmp_database_path ) ) . $file_path, $target_database_path, true ); - $wp_filesystem->delete( trailingslashit( dirname( $tmp_database_path ) ) . $file->current()->getFileName() ); - } catch ( Exception $e ) { - $logger->notice( $e->getMessage(), array( 'source' => 'geolocation' ) ); - - // Reschedule download of DB. - wp_clear_scheduled_hook( 'woocommerce_geoip_updater' ); - wp_schedule_event( strtotime( 'first tuesday of next month' ), 'monthly', 'woocommerce_geoip_updater' ); - } - // Delete temp file regardless of success. - $wp_filesystem->delete( $tmp_database_path ); - } else { - $logger->notice( - 'Unable to download GeoIP Database: ' . $tmp_database_path->get_error_message(), - array( 'source' => 'geolocation' ) - ); - } + wc_deprecated_function( 'WC_Geolocation::update_database', '3.9.0' ); + $integration = wc()->integrations->get_integration( 'maxmind_geolocation' ); + $integration->update_database(); } /** - * Use MAXMIND GeoLite database to geolocation the user. + * Fetches the country code from the request headers, if one is available. * - * @param string $ip_address IP address. - * @param string $database Database path. - * @return string + * @since 3.9.0 + * @return string The country code pulled from the headers, or empty string if one was not found. */ - private static function geolocate_via_db( $ip_address, $database ) { - if ( ! class_exists( 'WC_Geolite_Integration', false ) ) { - require_once WC_ABSPATH . 'includes/class-wc-geolite-integration.php'; + private static function get_country_code_from_headers() { + $country_code = ''; + + $headers = array( + 'MM_COUNTRY_CODE', + 'GEOIP_COUNTRY_CODE', + 'HTTP_CF_IPCOUNTRY', + 'HTTP_X_COUNTRY_CODE', + ); + + foreach ( $headers as $header ) { + if ( empty( $_SERVER[ $header ] ) ) { + continue; + } + + $country_code = strtoupper( sanitize_text_field( wp_unslash( $_SERVER[ $header ] ) ) ); + break; } - $geolite = new WC_Geolite_Integration( $database ); - - return $geolite->get_country_iso( $ip_address ); + return $country_code; } /** @@ -368,6 +307,50 @@ class WC_Geolocation { return $country_code; } -} -WC_Geolocation::init(); + /** + * Hook in geolocation functionality. + * + * @deprecated 3.9.0 + * @return null + */ + public static function init() { + wc_deprecated_function( 'WC_Geolocation::init', '3.9.0' ); + return null; + } + + /** + * Prevent geolocation via MaxMind when using legacy versions of php. + * + * @deprecated 3.9.0 + * @since 3.4.0 + * @param string $default_customer_address current value. + * @return string + */ + public static function disable_geolocation_on_legacy_php( $default_customer_address ) { + wc_deprecated_function( 'WC_Geolocation::disable_geolocation_on_legacy_php', '3.9.0' ); + + if ( self::is_geolocation_enabled( $default_customer_address ) ) { + $default_customer_address = 'base'; + } + + return $default_customer_address; + } + + /** + * Maybe trigger a DB update for the first time. + * + * @deprecated 3.9.0 + * @param string $new_value New value. + * @param string $old_value Old value. + * @return string + */ + public static function maybe_update_database( $new_value, $old_value ) { + wc_deprecated_function( 'WC_Geolocation::maybe_update_database', '3.9.0' ); + if ( $new_value !== $old_value && self::is_geolocation_enabled( $new_value ) ) { + self::update_database(); + } + + return $new_value; + } +} diff --git a/includes/class-wc-install.php b/includes/class-wc-install.php index 45b9e2b0055..fba3c6263e4 100644 --- a/includes/class-wc-install.php +++ b/includes/class-wc-install.php @@ -135,6 +135,11 @@ class WC_Install { 'wc_update_370_mro_std_currency', 'wc_update_370_db_version', ), + '3.9.0' => array( + 'wc_update_390_move_maxmind_database', + 'wc_update_390_change_geolocation_database_update_cron', + 'wc_update_390_db_version', + ), ); /** @@ -421,6 +426,10 @@ class WC_Install { 'interval' => 2635200, 'display' => __( 'Monthly', 'woocommerce' ), ); + $schedules['fifteendays'] = array( + 'interval' => 1296000, + 'display' => __( 'Every 15 Days', 'woocommerce' ), + ); return $schedules; } @@ -449,11 +458,8 @@ class WC_Install { wp_schedule_event( time(), 'daily', 'woocommerce_cleanup_personal_data' ); wp_schedule_event( time() + ( 3 * HOUR_IN_SECONDS ), 'daily', 'woocommerce_cleanup_logs' ); wp_schedule_event( time() + ( 6 * HOUR_IN_SECONDS ), 'twicedaily', 'woocommerce_cleanup_sessions' ); - wp_schedule_event( strtotime( 'first tuesday of next month' ), 'monthly', 'woocommerce_geoip_updater' ); + wp_schedule_event( time() + MINUTE_IN_SECONDS, 'fifteendays', 'woocommerce_geoip_updater' ); wp_schedule_event( time() + 10, apply_filters( 'woocommerce_tracker_event_recurrence', 'daily' ), 'woocommerce_tracker_send_event' ); - - // Trigger GeoLite2 database download after 1 minute. - wp_schedule_single_event( time() + ( MINUTE_IN_SECONDS * 1 ), 'woocommerce_geoip_updater' ); } /** @@ -970,7 +976,7 @@ CREATE TABLE {$wpdb->prefix}wc_tax_rate_classes ( $tables = self::get_tables(); foreach ( $tables as $table ) { - $wpdb->query( "DROP TABLE IF EXISTS {$table}" ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $wpdb->query( "DROP TABLE IF EXISTS {$table}" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared } } diff --git a/includes/class-wc-integrations.php b/includes/class-wc-integrations.php index 3e89f67a706..13d906060a2 100644 --- a/includes/class-wc-integrations.php +++ b/includes/class-wc-integrations.php @@ -4,7 +4,7 @@ * * Loads Integrations into WooCommerce. * - * @version 2.3.0 + * @version 3.9.0 * @package WooCommerce/Classes/Integrations */ @@ -29,7 +29,11 @@ class WC_Integrations { do_action( 'woocommerce_integrations_init' ); - $load_integrations = apply_filters( 'woocommerce_integrations', array() ); + $load_integrations = array( + 'WC_Integration_MaxMind_Geolocation', + ); + + $load_integrations = apply_filters( 'woocommerce_integrations', $load_integrations ); // Load integration classes. foreach ( $load_integrations as $integration ) { @@ -48,4 +52,19 @@ class WC_Integrations { public function get_integrations() { return $this->integrations; } + + /** + * Return a desired integration. + * + * @since 3.9.0 + * @param string $id The id of the integration to get. + * @return mixed|null The integration if one is found, otherwise null. + */ + public function get_integration( $id ) { + if ( isset( $this->integrations[ $id ] ) ) { + return $this->integrations[ $id ]; + } + + return null; + } } diff --git a/includes/class-wc-shipping.php b/includes/class-wc-shipping.php index de5cb859603..ef2bfe08ed3 100644 --- a/includes/class-wc-shipping.php +++ b/includes/class-wc-shipping.php @@ -294,12 +294,7 @@ class WC_Shipping { } $states = WC()->countries->get_states( $country ); - if ( is_array( $states ) && ! isset( $states[ $package['destination']['state'] ] ) ) { - return false; - } - - $postcode = wc_format_postcode( $package['destination']['postcode'], $country ); - if ( ! WC_Validation::is_postcode( $postcode, $country ) ) { + if ( is_array( $states ) && ! empty( $states ) && ! isset( $states[ $package['destination']['state'] ] ) ) { return false; } diff --git a/includes/emails/class-wc-email.php b/includes/emails/class-wc-email.php index bae6396f853..14201db7274 100644 --- a/includes/emails/class-wc-email.php +++ b/includes/emails/class-wc-email.php @@ -441,17 +441,23 @@ class WC_Email extends WC_Settings_API { /** * Get email content type. * + * @param string $default_content_type Default wp_mail() content type. * @return string */ - public function get_content_type() { + public function get_content_type( $default_content_type = '' ) { switch ( $this->get_email_type() ) { case 'html': - return 'text/html'; + $content_type = 'text/html'; + break; case 'multipart': - return 'multipart/alternative'; + $content_type = 'multipart/alternative'; + break; default: - return 'text/plain'; + $content_type = 'text/plain'; + break; } + + return apply_filters( 'woocommerce_email_content_type', $content_type, $this, $default_content_type ); } /** @@ -599,21 +605,23 @@ class WC_Email extends WC_Settings_API { /** * Get the from name for outgoing emails. * + * @param string $from_name Default wp_mail() name associated with the "from" email address. * @return string */ - public function get_from_name() { - $from_name = apply_filters( 'woocommerce_email_from_name', get_option( 'woocommerce_email_from_name' ), $this ); + public function get_from_name( $from_name = '' ) { + $from_name = apply_filters( 'woocommerce_email_from_name', get_option( 'woocommerce_email_from_name' ), $this, $from_name ); return wp_specialchars_decode( esc_html( $from_name ), ENT_QUOTES ); } /** * Get the from address for outgoing emails. * + * @param string $from_email Default wp_mail() email address to send from. * @return string */ - public function get_from_address() { - $from_address = apply_filters( 'woocommerce_email_from_address', get_option( 'woocommerce_email_from_address' ), $this ); - return sanitize_email( $from_address ); + public function get_from_address( $from_email = '' ) { + $from_email = apply_filters( 'woocommerce_email_from_address', get_option( 'woocommerce_email_from_address' ), $this, $from_email ); + return sanitize_email( $from_email ); } /** diff --git a/includes/integrations/maxmind-geolocation/class-wc-integration-maxmind-database-service.php b/includes/integrations/maxmind-geolocation/class-wc-integration-maxmind-database-service.php new file mode 100644 index 00000000000..e56730372fe --- /dev/null +++ b/includes/integrations/maxmind-geolocation/class-wc-integration-maxmind-database-service.php @@ -0,0 +1,167 @@ +database_prefix = $database_prefix; + } + + /** + * Fetches the path that the database should be stored. + * + * @return string The local database path. + */ + public function get_database_path() { + $uploads_dir = wp_upload_dir(); + + $database_path = trailingslashit( $uploads_dir['basedir'] ) . 'woocommerce_uploads/'; + if ( ! empty( $this->database_prefix ) ) { + $database_path .= $this->database_prefix . '-'; + } + $database_path .= self::DATABASE . self::DATABASE_EXTENSION; + + /** + * Filter the geolocation database storage path. + * + * @param string $database_path The path to the database. + * @param int $version Deprecated since 3.4.0. + * @deprecated 3.9.0 + */ + $database_path = apply_filters_deprecated( + 'woocommerce_geolocation_local_database_path', + array( $database_path, 2 ), + '3.9.0', + 'woocommerce_maxmind_geolocation_database_path' + ); + + /** + * Filter the geolocation database storage path. + * + * @since 3.9.0 + * @param string $database_path The path to the database. + */ + return apply_filters( 'woocommerce_maxmind_geolocation_database_path', $database_path ); + } + + /** + * Fetches the database from the MaxMind service. + * + * @param string $license_key The license key to be used when downloading the database. + * @return string|WP_Error The path to the database file or an error if invalid. + */ + public function download_database( $license_key ) { + $download_uri = add_query_arg( + array( + 'edition_id' => self::DATABASE, + 'license_key' => wc_clean( $license_key ), + 'suffix' => 'tar.gz', + ), + 'https://download.maxmind.com/app/geoip_download' + ); + + // Needed for the download_url call right below. + require_once ABSPATH . 'wp-admin/includes/file.php'; + + $tmp_archive_path = download_url( $download_uri ); + if ( is_wp_error( $tmp_archive_path ) ) { + // Transform the error into something more informative. + $error_data = $tmp_archive_path->get_error_data(); + if ( isset( $error_data['code'] ) ) { + switch ( $error_data['code'] ) { + case 401: + return new WP_Error( + 'woocommerce_maxmind_geolocation_database_license_key', + __( 'The MaxMind license key is invalid.', 'woocommerce' ) + ); + } + } + + return new WP_Error( 'woocommerce_maxmind_geolocation_database_download', __( 'Failed to download the MaxMind database.', 'woocommerce' ) ); + } + + // Extract the database from the archive. + try { + $file = new PharData( $tmp_archive_path ); + + $tmp_database_path = trailingslashit( dirname( $tmp_archive_path ) ) . trailingslashit( $file->current()->getFilename() ) . self::DATABASE . self::DATABASE_EXTENSION; + + $file->extractTo( + dirname( $tmp_archive_path ), + trailingslashit( $file->current()->getFilename() ) . self::DATABASE . self::DATABASE_EXTENSION, + true + ); + } catch ( Exception $exception ) { + return new WP_Error( 'woocommerce_maxmind_geolocation_database_archive', $exception->getMessage() ); + } finally { + // Remove the archive since we only care about a single file in it. + unlink( $tmp_archive_path ); + } + + return $tmp_database_path; + } + + /** + * Fetches the ISO country code associated with an IP address. + * + * @param string $ip_address The IP address to find the country code for. + * @return string|null The country code for the IP address, or null if none was found. + */ + public function get_iso_country_code_for_ip( $ip_address ) { + $country_code = null; + + if ( ! class_exists( 'MaxMind\Db\Reader' ) ) { + wc_get_logger()->notice( __( 'Missing MaxMind Reader library!', 'woocommerce' ), array( 'source' => 'maxmind-geolocation' ) ); + return $country_code; + } + + try { + $reader = new MaxMind\Db\Reader( $this->get_database_path() ); + $data = $reader->get( $ip_address ); + + if ( isset( $data['country']['iso_code'] ) ) { + $country_code = $data['country']['iso_code']; + } + + $reader->close(); + } catch ( Exception $e ) { + wc_get_logger()->notice( $e->getMessage(), array( 'source' => 'maxmind-geolocation' ) ); + } + + return $country_code; + } +} diff --git a/includes/integrations/maxmind-geolocation/class-wc-integration-maxmind-geolocation.php b/includes/integrations/maxmind-geolocation/class-wc-integration-maxmind-geolocation.php new file mode 100644 index 00000000000..51965fd64f2 --- /dev/null +++ b/includes/integrations/maxmind-geolocation/class-wc-integration-maxmind-geolocation.php @@ -0,0 +1,289 @@ +id = 'maxmind_geolocation'; + $this->method_title = __( 'WooCommerce MaxMind Geolocation', 'woocommerce' ); + $this->method_description = __( 'An integration for utilizing MaxMind to do Geolocation lookups. Please note that this integration will only do country lookups.', 'woocommerce' ); + + /** + * Supports overriding the database service to be used. + * + * @since 3.9.0 + * @return mixed|null The geolocation database service. + */ + $this->database_service = apply_filters( 'woocommerce_maxmind_geolocation_database_service', null ); + if ( null === $this->database_service ) { + $this->database_service = new WC_Integration_MaxMind_Database_Service( $this->get_database_prefix() ); + } + + $this->init_form_fields(); + $this->init_settings(); + + // Bind to the save action for the settings. + add_action( 'woocommerce_update_options_integration_' . $this->id, array( $this, 'process_admin_options' ) ); + + // Trigger notice if license key is missing. + add_action( 'update_option_woocommerce_default_customer_address', array( $this, 'display_missing_license_key_notice' ), 1000, 2 ); + + /** + * Allows for the automatic database update to be disabled. + * + * @deprecated 3.9.0 + * @return bool Whether or not the database should be updated periodically. + */ + $bind_updater = apply_filters_deprecated( + 'woocommerce_geolocation_update_database_periodically', + array( true ), + '3.9.0', + 'woocommerce_maxmind_geolocation_update_database_periodically' + ); + + /** + * Allows for the automatic database update to be disabled. + * Note that MaxMind's TOS requires that the databases be updated or removed periodically. + * + * @since 3.9.0 + * @param bool $bind_updater Whether or not the database should be updated periodically. + */ + $bind_updater = apply_filters( 'woocommerce_maxmind_geolocation_update_database_periodically', $bind_updater ); + + // Bind to the scheduled updater action. + if ( $bind_updater ) { + add_action( 'woocommerce_geoip_updater', array( $this, 'update_database' ) ); + } + + // Bind to the geolocation filter for MaxMind database lookups. + add_filter( 'woocommerce_get_geolocation', array( $this, 'get_geolocation' ), 10, 2 ); + } + + /** + * Override the normal options so we can print the database file path to the admin, + */ + public function admin_options() { + parent::admin_options(); + + include dirname( __FILE__ ) . '/views/html-admin-options.php'; + } + + /** + * Initializes the settings fields. + */ + public function init_form_fields() { + $this->form_fields = array( + 'license_key' => array( + 'title' => __( 'MaxMind License Key', 'woocommerce' ), + 'type' => 'password', + 'description' => sprintf( + /* translators: %1$s: Documentation URL */ + __( + 'The key that will be used when dealing with MaxMind Geolocation services. You can read how to generate one in MaxMind\'s License Key Documentation.', + 'woocommerce' + ), + 'https://support.maxmind.com/account-faq/account-related/how-do-i-generate-a-license-key/' + ), + 'desc_tip' => false, + 'default' => '', + ), + ); + } + + /** + * Get database service. + * + * @return WC_Integration_MaxMind_Database_Service|null + */ + public function get_database_service() { + return $this->database_service; + } + + /** + * Checks to make sure that the license key is valid. + * + * @param string $key The key of the field. + * @param mixed $value The value of the field. + * @return mixed + * @throws Exception When the license key is invalid. + */ + public function validate_license_key_field( $key, $value ) { + // Empty license keys have no need to validate the data. + if ( empty( $value ) ) { + return $value; + } + + // Check the license key by attempting to download the Geolocation database. + $tmp_database_path = $this->database_service->download_database( $value ); + if ( is_wp_error( $tmp_database_path ) ) { + WC_Admin_Settings::add_error( $tmp_database_path->get_error_message() ); + + // Throw an exception to keep from changing this value. This will prevent + // users from accidentally losing their license key, which cannot + // be viewed again after generating. + throw new Exception( $tmp_database_path->get_error_message() ); + } + + // We may as well put this archive to good use, now that we've downloaded one. + self::update_database( $tmp_database_path ); + + // Remove missing license key notice. + $this->remove_missing_license_key_notice(); + + return $value; + } + + /** + * Updates the database used for geolocation queries. + * + * @param string|null $new_database_path The path to the new database file. Null will fetch a new archive. + */ + public function update_database( $new_database_path = null ) { + // Allow us to easily interact with the filesystem. + require_once ABSPATH . 'wp-admin/includes/file.php'; + WP_Filesystem(); + global $wp_filesystem; + + // Remove any existing archives to comply with the MaxMind TOS. + $target_database_path = $this->database_service->get_database_path(); + + // If there's no database path, we can't store the database. + if ( empty( $target_database_path ) ) { + return; + } + + if ( $wp_filesystem->exists( $target_database_path ) ) { + $wp_filesystem->delete( $target_database_path ); + } + + if ( isset( $new_database_path ) ) { + $tmp_database_path = $new_database_path; + } else { + // We can't download a database if there's no license key configured. + $license_key = $this->get_option( 'license_key' ); + if ( empty( $license_key ) ) { + return; + } + + $tmp_database_path = $this->database_service->download_database( $license_key ); + if ( is_wp_error( $tmp_database_path ) ) { + wc_get_logger()->notice( $tmp_database_path->get_error_message(), array( 'source' => 'maxmind-geolocation' ) ); + return; + } + } + + // Move the new database into position. + $wp_filesystem->move( $tmp_database_path, $target_database_path, true ); + $wp_filesystem->delete( dirname( $tmp_database_path ) ); + } + + /** + * Performs a geolocation lookup against the MaxMind database for the given IP address. + * + * @param array $data Geolocation data. + * @param string $ip_address The IP address to geolocate. + * @return array Geolocation including country code, state, city and postcode based on an IP address. + */ + public function get_geolocation( $data, $ip_address ) { + // WooCommerce look for headers first, and at this moment could be just enough. + if ( ! empty( $data['country'] ) ) { + return $data; + } + + if ( empty( $ip_address ) ) { + return $data; + } + + $country_code = $this->database_service->get_iso_country_code_for_ip( $ip_address ); + + return array( + 'country' => $country_code ? $country_code : '', + 'state' => '', + 'city' => '', + 'postcode' => '', + ); + } + + /** + * Fetches the prefix for the MaxMind database file. + * + * @return string + */ + private function get_database_prefix() { + $prefix = $this->get_option( 'database_prefix' ); + if ( empty( $prefix ) ) { + $prefix = wp_generate_password( 32, false ); + $this->update_option( 'database_prefix', $prefix ); + } + + return $prefix; + } + + /** + * Add missing license key notice. + */ + private function add_missing_license_key_notice() { + if ( ! class_exists( 'WC_Admin_Notices' ) ) { + include_once WC_ABSPATH . 'includes/admin/class-wc-admin-notices.php'; + } + WC_Admin_Notices::add_notice( 'maxmind_license_key' ); + } + + /** + * Remove missing license key notice. + */ + private function remove_missing_license_key_notice() { + if ( ! class_exists( 'WC_Admin_Notices' ) ) { + include_once WC_ABSPATH . 'includes/admin/class-wc-admin-notices.php'; + } + WC_Admin_Notices::remove_notice( 'maxmind_license_key' ); + } + + /** + * Display notice if license key is missing. + * + * @param mixed $old_value Option old value. + * @param mixed $new_value Current value. + */ + public function display_missing_license_key_notice( $old_value, $new_value ) { + if ( ! apply_filters( 'woocommerce_maxmind_geolocation_display_notices', true ) ) { + return; + } + + if ( ! in_array( $new_value, array( 'geolocation', 'geolocation_ajax' ), true ) ) { + $this->remove_missing_license_key_notice(); + return; + } + + $license_key = $this->get_option( 'license_key' ); + if ( ! empty( $license_key ) ) { + return; + } + + $this->add_missing_license_key_notice(); + } +} diff --git a/includes/integrations/maxmind-geolocation/views/html-admin-options.php b/includes/integrations/maxmind-geolocation/views/html-admin-options.php new file mode 100644 index 00000000000..a027e0072a1 --- /dev/null +++ b/includes/integrations/maxmind-geolocation/views/html-admin-options.php @@ -0,0 +1,25 @@ + + + + + + + +
+ + +
+ + +

+
+
diff --git a/includes/libraries/geolite2/Reader.php b/includes/libraries/geolite2/Reader.php deleted file mode 100644 index 4ccab914466..00000000000 --- a/includes/libraries/geolite2/Reader.php +++ /dev/null @@ -1,309 +0,0 @@ -get method. - */ -class Reader -{ - private static $DATA_SECTION_SEPARATOR_SIZE = 16; - private static $METADATA_START_MARKER = "\xAB\xCD\xEFMaxMind.com"; - private static $METADATA_START_MARKER_LENGTH = 14; - private static $METADATA_MAX_SIZE = 131072; // 128 * 1024 = 128KB - - private $decoder; - private $fileHandle; - private $fileSize; - private $ipV4Start; - private $metadata; - - /** - * Constructs a Reader for the MaxMind DB format. The file passed to it must - * be a valid MaxMind DB file such as a GeoIp2 database file. - * - * @param string $database - * the MaxMind DB file to use - * - * @throws \InvalidArgumentException for invalid database path or unknown arguments - * @throws \MaxMind\Db\Reader\InvalidDatabaseException - * if the database is invalid or there is an error reading - * from it - */ - public function __construct($database) - { - if (func_num_args() !== 1) { - throw new \InvalidArgumentException( - 'The constructor takes exactly one argument.' - ); - } - - if (!is_readable($database)) { - throw new \InvalidArgumentException( - "The file \"$database\" does not exist or is not readable." - ); - } - $this->fileHandle = @fopen($database, 'rb'); - if ($this->fileHandle === false) { - throw new \InvalidArgumentException( - "Error opening \"$database\"." - ); - } - $this->fileSize = @filesize($database); - if ($this->fileSize === false) { - throw new \UnexpectedValueException( - "Error determining the size of \"$database\"." - ); - } - - $start = $this->findMetadataStart($database); - $metadataDecoder = new Decoder($this->fileHandle, $start); - list($metadataArray) = $metadataDecoder->decode($start); - $this->metadata = new Metadata($metadataArray); - $this->decoder = new Decoder( - $this->fileHandle, - $this->metadata->searchTreeSize + self::$DATA_SECTION_SEPARATOR_SIZE - ); - } - - /** - * Looks up the address in the MaxMind DB. - * - * @param string $ipAddress - * the IP address to look up - * - * @throws \BadMethodCallException if this method is called on a closed database - * @throws \InvalidArgumentException if something other than a single IP address is passed to the method - * @throws InvalidDatabaseException - * if the database is invalid or there is an error reading - * from it - * - * @return array the record for the IP address - */ - public function get($ipAddress) - { - if (func_num_args() !== 1) { - throw new \InvalidArgumentException( - 'Method takes exactly one argument.' - ); - } - - if (!is_resource($this->fileHandle)) { - throw new \BadMethodCallException( - 'Attempt to read from a closed MaxMind DB.' - ); - } - - if (!filter_var($ipAddress, FILTER_VALIDATE_IP)) { - throw new \InvalidArgumentException( - "The value \"$ipAddress\" is not a valid IP address." - ); - } - - if ($this->metadata->ipVersion === 4 && strrpos($ipAddress, ':')) { - throw new \InvalidArgumentException( - "Error looking up $ipAddress. You attempted to look up an" - . ' IPv6 address in an IPv4-only database.' - ); - } - $pointer = $this->findAddressInTree($ipAddress); - if ($pointer === 0) { - return null; - } - - return $this->resolveDataPointer($pointer); - } - - private function findAddressInTree($ipAddress) - { - // XXX - could simplify. Done as a byte array to ease porting - $rawAddress = array_merge(unpack('C*', inet_pton($ipAddress))); - - $bitCount = count($rawAddress) * 8; - - // The first node of the tree is always node 0, at the beginning of the - // value - $node = $this->startNode($bitCount); - - for ($i = 0; $i < $bitCount; $i++) { - if ($node >= $this->metadata->nodeCount) { - break; - } - $tempBit = 0xFF & $rawAddress[$i >> 3]; - $bit = 1 & ($tempBit >> 7 - ($i % 8)); - - $node = $this->readNode($node, $bit); - } - if ($node === $this->metadata->nodeCount) { - // Record is empty - return 0; - } elseif ($node > $this->metadata->nodeCount) { - // Record is a data pointer - return $node; - } - throw new InvalidDatabaseException('Something bad happened'); - } - - private function startNode($length) - { - // Check if we are looking up an IPv4 address in an IPv6 tree. If this - // is the case, we can skip over the first 96 nodes. - if ($this->metadata->ipVersion === 6 && $length === 32) { - return $this->ipV4StartNode(); - } - // The first node of the tree is always node 0, at the beginning of the - // value - return 0; - } - - private function ipV4StartNode() - { - // This is a defensive check. There is no reason to call this when you - // have an IPv4 tree. - if ($this->metadata->ipVersion === 4) { - return 0; - } - - if ($this->ipV4Start) { - return $this->ipV4Start; - } - $node = 0; - - for ($i = 0; $i < 96 && $node < $this->metadata->nodeCount; $i++) { - $node = $this->readNode($node, 0); - } - $this->ipV4Start = $node; - - return $node; - } - - private function readNode($nodeNumber, $index) - { - $baseOffset = $nodeNumber * $this->metadata->nodeByteSize; - - // XXX - probably could condense this. - switch ($this->metadata->recordSize) { - case 24: - $bytes = Util::read($this->fileHandle, $baseOffset + $index * 3, 3); - list(, $node) = unpack('N', "\x00" . $bytes); - - return $node; - case 28: - $middleByte = Util::read($this->fileHandle, $baseOffset + 3, 1); - list(, $middle) = unpack('C', $middleByte); - if ($index === 0) { - $middle = (0xF0 & $middle) >> 4; - } else { - $middle = 0x0F & $middle; - } - $bytes = Util::read($this->fileHandle, $baseOffset + $index * 4, 3); - list(, $node) = unpack('N', chr($middle) . $bytes); - - return $node; - case 32: - $bytes = Util::read($this->fileHandle, $baseOffset + $index * 4, 4); - list(, $node) = unpack('N', $bytes); - - return $node; - default: - throw new InvalidDatabaseException( - 'Unknown record size: ' - . $this->metadata->recordSize - ); - } - } - - private function resolveDataPointer($pointer) - { - $resolved = $pointer - $this->metadata->nodeCount - + $this->metadata->searchTreeSize; - if ($resolved > $this->fileSize) { - throw new InvalidDatabaseException( - "The MaxMind DB file's search tree is corrupt" - ); - } - - list($data) = $this->decoder->decode($resolved); - - return $data; - } - - /* - * This is an extremely naive but reasonably readable implementation. There - * are much faster algorithms (e.g., Boyer-Moore) for this if speed is ever - * an issue, but I suspect it won't be. - */ - private function findMetadataStart($filename) - { - $handle = $this->fileHandle; - $fstat = fstat($handle); - $fileSize = $fstat['size']; - $marker = self::$METADATA_START_MARKER; - $markerLength = self::$METADATA_START_MARKER_LENGTH; - $metadataMaxLengthExcludingMarker - = min(self::$METADATA_MAX_SIZE, $fileSize) - $markerLength; - - for ($i = 0; $i <= $metadataMaxLengthExcludingMarker; $i++) { - for ($j = 0; $j < $markerLength; $j++) { - fseek($handle, $fileSize - $i - $j - 1); - $matchBit = fgetc($handle); - if ($matchBit !== $marker[$markerLength - $j - 1]) { - continue 2; - } - } - - return $fileSize - $i; - } - throw new InvalidDatabaseException( - "Error opening database file ($filename). " . - 'Is this a valid MaxMind DB file?' - ); - } - - /** - * @throws \InvalidArgumentException if arguments are passed to the method - * @throws \BadMethodCallException if the database has been closed - * - * @return Metadata object for the database - */ - public function metadata() - { - if (func_num_args()) { - throw new \InvalidArgumentException( - 'Method takes no arguments.' - ); - } - - // Not technically required, but this makes it consistent with - // C extension and it allows us to change our implementation later. - if (!is_resource($this->fileHandle)) { - throw new \BadMethodCallException( - 'Attempt to read from a closed MaxMind DB.' - ); - } - - return $this->metadata; - } - - /** - * Closes the MaxMind DB and returns resources to the system. - * - * @throws \Exception - * if an I/O error occurs - */ - public function close() - { - if (!is_resource($this->fileHandle)) { - throw new \BadMethodCallException( - 'Attempt to close a closed MaxMind DB.' - ); - } - fclose($this->fileHandle); - } -} diff --git a/includes/libraries/geolite2/Reader/Decoder.php b/includes/libraries/geolite2/Reader/Decoder.php deleted file mode 100644 index 40ae27e049e..00000000000 --- a/includes/libraries/geolite2/Reader/Decoder.php +++ /dev/null @@ -1,311 +0,0 @@ - 'extended', - 1 => 'pointer', - 2 => 'utf8_string', - 3 => 'double', - 4 => 'bytes', - 5 => 'uint16', - 6 => 'uint32', - 7 => 'map', - 8 => 'int32', - 9 => 'uint64', - 10 => 'uint128', - 11 => 'array', - 12 => 'container', - 13 => 'end_marker', - 14 => 'boolean', - 15 => 'float', - ]; - - public function __construct( - $fileStream, - $pointerBase = 0, - $pointerTestHack = false - ) { - $this->fileStream = $fileStream; - $this->pointerBase = $pointerBase; - $this->pointerTestHack = $pointerTestHack; - - $this->switchByteOrder = $this->isPlatformLittleEndian(); - } - - public function decode($offset) - { - list(, $ctrlByte) = unpack( - 'C', - Util::read($this->fileStream, $offset, 1) - ); - $offset++; - - $type = $this->types[$ctrlByte >> 5]; - - // Pointers are a special case, we don't read the next $size bytes, we - // use the size to determine the length of the pointer and then follow - // it. - if ($type === 'pointer') { - list($pointer, $offset) = $this->decodePointer($ctrlByte, $offset); - - // for unit testing - if ($this->pointerTestHack) { - return [$pointer]; - } - - list($result) = $this->decode($pointer); - - return [$result, $offset]; - } - - if ($type === 'extended') { - list(, $nextByte) = unpack( - 'C', - Util::read($this->fileStream, $offset, 1) - ); - - $typeNum = $nextByte + 7; - - if ($typeNum < 8) { - throw new InvalidDatabaseException( - 'Something went horribly wrong in the decoder. An extended type ' - . 'resolved to a type number < 8 (' - . $this->types[$typeNum] - . ')' - ); - } - - $type = $this->types[$typeNum]; - $offset++; - } - - list($size, $offset) = $this->sizeFromCtrlByte($ctrlByte, $offset); - - return $this->decodeByType($type, $offset, $size); - } - - private function decodeByType($type, $offset, $size) - { - switch ($type) { - case 'map': - return $this->decodeMap($size, $offset); - case 'array': - return $this->decodeArray($size, $offset); - case 'boolean': - return [$this->decodeBoolean($size), $offset]; - } - - $newOffset = $offset + $size; - $bytes = Util::read($this->fileStream, $offset, $size); - switch ($type) { - case 'utf8_string': - return [$this->decodeString($bytes), $newOffset]; - case 'double': - $this->verifySize(8, $size); - - return [$this->decodeDouble($bytes), $newOffset]; - case 'float': - $this->verifySize(4, $size); - - return [$this->decodeFloat($bytes), $newOffset]; - case 'bytes': - return [$bytes, $newOffset]; - case 'uint16': - case 'uint32': - return [$this->decodeUint($bytes), $newOffset]; - case 'int32': - return [$this->decodeInt32($bytes), $newOffset]; - case 'uint64': - case 'uint128': - return [$this->decodeBigUint($bytes, $size), $newOffset]; - default: - throw new InvalidDatabaseException( - 'Unknown or unexpected type: ' . $type - ); - } - } - - private function verifySize($expected, $actual) - { - if ($expected !== $actual) { - throw new InvalidDatabaseException( - "The MaxMind DB file's data section contains bad data (unknown data type or corrupt data)" - ); - } - } - - private function decodeArray($size, $offset) - { - $array = []; - - for ($i = 0; $i < $size; $i++) { - list($value, $offset) = $this->decode($offset); - array_push($array, $value); - } - - return [$array, $offset]; - } - - private function decodeBoolean($size) - { - return $size === 0 ? false : true; - } - - private function decodeDouble($bits) - { - // XXX - Assumes IEEE 754 double on platform - list(, $double) = unpack('d', $this->maybeSwitchByteOrder($bits)); - - return $double; - } - - private function decodeFloat($bits) - { - // XXX - Assumes IEEE 754 floats on platform - list(, $float) = unpack('f', $this->maybeSwitchByteOrder($bits)); - - return $float; - } - - private function decodeInt32($bytes) - { - $bytes = $this->zeroPadLeft($bytes, 4); - list(, $int) = unpack('l', $this->maybeSwitchByteOrder($bytes)); - - return $int; - } - - private function decodeMap($size, $offset) - { - $map = []; - - for ($i = 0; $i < $size; $i++) { - list($key, $offset) = $this->decode($offset); - list($value, $offset) = $this->decode($offset); - $map[$key] = $value; - } - - return [$map, $offset]; - } - - private $pointerValueOffset = [ - 1 => 0, - 2 => 2048, - 3 => 526336, - 4 => 0, - ]; - - private function decodePointer($ctrlByte, $offset) - { - $pointerSize = (($ctrlByte >> 3) & 0x3) + 1; - - $buffer = Util::read($this->fileStream, $offset, $pointerSize); - $offset = $offset + $pointerSize; - - $packed = $pointerSize === 4 - ? $buffer - : (pack('C', $ctrlByte & 0x7)) . $buffer; - - $unpacked = $this->decodeUint($packed); - $pointer = $unpacked + $this->pointerBase - + $this->pointerValueOffset[$pointerSize]; - - return [$pointer, $offset]; - } - - private function decodeUint($bytes) - { - list(, $int) = unpack('N', $this->zeroPadLeft($bytes, 4)); - - return $int; - } - - private function decodeBigUint($bytes, $byteLength) - { - $maxUintBytes = log(PHP_INT_MAX, 2) / 8; - - if ($byteLength === 0) { - return 0; - } - - $numberOfLongs = ceil($byteLength / 4); - $paddedLength = $numberOfLongs * 4; - $paddedBytes = $this->zeroPadLeft($bytes, $paddedLength); - $unpacked = array_merge(unpack("N$numberOfLongs", $paddedBytes)); - - $integer = 0; - - // 2^32 - $twoTo32 = '4294967296'; - - foreach ($unpacked as $part) { - // We only use gmp or bcmath if the final value is too big - if ($byteLength <= $maxUintBytes) { - $integer = ($integer << 32) + $part; - } elseif (extension_loaded('gmp')) { - $integer = gmp_strval(gmp_add(gmp_mul($integer, $twoTo32), $part)); - } elseif (extension_loaded('bcmath')) { - $integer = bcadd(bcmul($integer, $twoTo32), $part); - } else { - throw new \RuntimeException( - 'The gmp or bcmath extension must be installed to read this database.' - ); - } - } - - return $integer; - } - - private function decodeString($bytes) - { - // XXX - NOOP. As far as I know, the end user has to explicitly set the - // encoding in PHP. Strings are just bytes. - return $bytes; - } - - private function sizeFromCtrlByte($ctrlByte, $offset) - { - $size = $ctrlByte & 0x1f; - $bytesToRead = $size < 29 ? 0 : $size - 28; - $bytes = Util::read($this->fileStream, $offset, $bytesToRead); - $decoded = $this->decodeUint($bytes); - - if ($size === 29) { - $size = 29 + $decoded; - } elseif ($size === 30) { - $size = 285 + $decoded; - } elseif ($size > 30) { - $size = ($decoded & (0x0FFFFFFF >> (32 - (8 * $bytesToRead)))) - + 65821; - } - - return [$size, $offset + $bytesToRead]; - } - - private function zeroPadLeft($content, $desiredLength) - { - return str_pad($content, $desiredLength, "\x00", STR_PAD_LEFT); - } - - private function maybeSwitchByteOrder($bytes) - { - return $this->switchByteOrder ? strrev($bytes) : $bytes; - } - - private function isPlatformLittleEndian() - { - $testint = 0x00FF; - $packed = pack('S', $testint); - - return $testint === current(unpack('v', $packed)); - } -} diff --git a/includes/libraries/geolite2/Reader/InvalidDatabaseException.php b/includes/libraries/geolite2/Reader/InvalidDatabaseException.php deleted file mode 100644 index d2a9a775f28..00000000000 --- a/includes/libraries/geolite2/Reader/InvalidDatabaseException.php +++ /dev/null @@ -1,10 +0,0 @@ -binaryFormatMajorVersion = - $metadata['binary_format_major_version']; - $this->binaryFormatMinorVersion = - $metadata['binary_format_minor_version']; - $this->buildEpoch = $metadata['build_epoch']; - $this->databaseType = $metadata['database_type']; - $this->languages = $metadata['languages']; - $this->description = $metadata['description']; - $this->ipVersion = $metadata['ip_version']; - $this->nodeCount = $metadata['node_count']; - $this->recordSize = $metadata['record_size']; - $this->nodeByteSize = $this->recordSize / 4; - $this->searchTreeSize = $this->nodeCount * $this->nodeByteSize; - } - - public function __get($var) - { - return $this->$var; - } -} diff --git a/includes/libraries/geolite2/Reader/Util.php b/includes/libraries/geolite2/Reader/Util.php deleted file mode 100644 index 87ebbf133f3..00000000000 --- a/includes/libraries/geolite2/Reader/Util.php +++ /dev/null @@ -1,26 +0,0 @@ -query( "ALTER TABLE {$wpdb->prefix}wc_download_log DROP FOREIGN KEY {$fk->CONSTRAINT_NAME}" ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $wpdb->query( "ALTER TABLE {$wpdb->prefix}wc_download_log DROP FOREIGN KEY {$fk->CONSTRAINT_NAME}" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared } } } @@ -2046,3 +2046,38 @@ function wc_update_370_mro_std_currency() { function wc_update_370_db_version() { WC_Install::update_db_version( '3.7.0' ); } + +/** + * We've moved the MaxMind database to a new location, as per the TOS' requirement that the database not + * be publicly accessible. + */ +function wc_update_390_move_maxmind_database() { + // Make sure to use all of the correct filters to pull the local database path. + $old_path = apply_filters( 'woocommerce_geolocation_local_database_path', WP_CONTENT_DIR . '/uploads/GeoLite2-Country.mmdb', 2 ); + + // Generate a prefix for the old file and store it in the integration as it would expect it. + $prefix = wp_generate_password( 32, false ); + update_option( 'woocommerce_maxmind_geolocation_settings', array( 'database_prefix' => $prefix ) ); + + // Generate the new path in the same way that the integration will. + $uploads_dir = wp_upload_dir(); + $new_path = trailingslashit( $uploads_dir['basedir'] ) . 'woocommerce_uploads/' . $prefix . '-GeoLite2-Country.mmdb'; + $new_path = apply_filters( 'woocommerce_geolocation_local_database_path', $new_path, 2 ); + + @rename( $old_path, $new_path ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged +} + +/** + * So that we can best meet MaxMind's TOS, the geolocation database update cron should run once per 15 days. + */ +function wc_update_390_change_geolocation_database_update_cron() { + wp_clear_scheduled_hook( 'woocommerce_geoip_updater' ); + wp_schedule_event( time() + ( DAY_IN_SECONDS * 15 ), 'fifteendays', 'woocommerce_geoip_updater' ); +} + +/** + * Update DB version. + */ +function wc_update_390_db_version() { + WC_Install::update_db_version( '3.9.0' ); +} diff --git a/package-lock.json b/package-lock.json index 35cdfbd2219..a34bb63a837 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3591,9 +3591,9 @@ } }, "@types/json-schema": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.3.tgz", - "integrity": "sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.4.tgz", + "integrity": "sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==", "dev": true }, "@types/minimatch": { @@ -3608,12 +3608,6 @@ "integrity": "sha512-Jrb/x3HT4PTJp6a4avhmJCDEVrPdqLfl3e8GGMbpkGGdwAV5UGlIs4vVEfsHHfylZVOKZWpOqmqFH8CbfOZ6kg==", "dev": true }, - "@types/normalize-package-data": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==", - "dev": true - }, "@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -3666,27 +3660,27 @@ "integrity": "sha512-wBlsw+8n21e6eTd4yVv8YD/E3xq0O6nNnJIquutAsFGE7EyMKz7W6RNT6BRu1SmdgmlCZ9tb0X+j+D6HGr8pZw==" }, "@typescript-eslint/experimental-utils": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.12.0.tgz", - "integrity": "sha512-jv4gYpw5N5BrWF3ntROvCuLe1IjRenLy5+U57J24NbPGwZFAjhnM45qpq0nDH1y/AZMb3Br25YiNVwyPbz6RkA==", + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.16.0.tgz", + "integrity": "sha512-bXTmAztXpqxliDKZgvWkl+5dHeRN+jqXVZ16peKKFzSXVzT6mz8kgBpHiVzEKO2NZ8OCU7dG61K9sRS/SkUUFQ==", "dev": true, "requires": { "@types/json-schema": "^7.0.3", - "@typescript-eslint/typescript-estree": "2.12.0", + "@typescript-eslint/typescript-estree": "2.16.0", "eslint-scope": "^5.0.0" } }, "@typescript-eslint/typescript-estree": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.12.0.tgz", - "integrity": "sha512-rGehVfjHEn8Frh9UW02ZZIfJs6SIIxIu/K1bbci8rFfDE/1lQ8krIJy5OXOV3DVnNdDPtoiPOdEANkLMrwXbiQ==", + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.16.0.tgz", + "integrity": "sha512-hyrCYjFHISos68Bk5KjUAXw0pP/455qq9nxqB1KkT67Pxjcfw+r6Yhcmqnp8etFL45UexCHUMrADHH7dI/m2WQ==", "dev": true, "requires": { "debug": "^4.1.1", "eslint-visitor-keys": "^1.1.0", "glob": "^7.1.6", "is-glob": "^4.0.1", - "lodash.unescape": "4.0.1", + "lodash": "^4.17.15", "semver": "^6.3.0", "tsutils": "^3.17.1" }, @@ -3714,6 +3708,12 @@ "path-is-absolute": "^1.0.0" } }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -5622,9 +5622,9 @@ } }, "commander": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/commander/-/commander-3.0.2.tgz", - "integrity": "sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.0.tgz", + "integrity": "sha512-NIQrwvv9V39FHgGFm36+U9SMQzbiHvU79k+iADraJTpmrFFfx7Ds0IvDoAdZsDrknlkRk14OYoWXb57uTh7/sw==", "dev": true }, "commondir": { @@ -6252,9 +6252,9 @@ }, "dependencies": { "commander": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", - "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true }, "lru-cache": { @@ -6516,12 +6516,13 @@ } }, "eslint-plugin-jest": { - "version": "23.1.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-23.1.1.tgz", - "integrity": "sha512-2oPxHKNh4j1zmJ6GaCBuGcb8FVZU7YjFUOJzGOPnl9ic7VA/MGAskArLJiRIlnFUmi1EUxY+UiATAy8dv8s5JA==", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-23.6.0.tgz", + "integrity": "sha512-GH8AhcFXspOLqak7fqnddLXEJsrFyvgO8Bm60SexvKSn1+3rWYESnCiWUOCUcBTprNSDSE4CtAZdM4EyV6gPPw==", "dev": true, "requires": { - "@typescript-eslint/experimental-utils": "^2.5.0" + "@typescript-eslint/experimental-utils": "^2.5.0", + "micromatch": "^4.0.2" } }, "eslint-plugin-react-hooks": { @@ -6876,9 +6877,9 @@ } }, "fault": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.2.tgz", - "integrity": "sha512-o2eo/X2syzzERAtN5LcGbiVQ0WwZSlN3qLtadwAz3X8Bu+XWD16dja/KMsjZLiQr+BLGPDnHGkc4yUJf1Xpkpw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.3.tgz", + "integrity": "sha512-sfFuP4X0hzrbGKjAUNXYvNqsZ5F6ohx/dZ9I0KQud/aiZNwg263r5L9yGB0clvXHCkzXh5W3t7RSHchggYIFmA==", "dev": true, "requires": { "format": "^0.2.2" @@ -7126,9 +7127,9 @@ "dev": true }, "flatten": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.2.tgz", - "integrity": "sha1-2uRqnXj74lKSJYzB54CkHZXAN4I=", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.3.tgz", + "integrity": "sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg==", "dev": true }, "flow-parser": { @@ -8679,9 +8680,9 @@ } }, "handlebars": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz", - "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.3.tgz", + "integrity": "sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==", "dev": true, "requires": { "neo-async": "^2.6.0", @@ -8922,28 +8923,73 @@ } }, "husky": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/husky/-/husky-3.1.0.tgz", - "integrity": "sha512-FJkPoHHB+6s4a+jwPqBudBDvYZsoQW5/HBuMSehC8qDiCe50kpcxeqFoDSlow+9I6wg47YxBoT3WxaURlrDIIQ==", + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/husky/-/husky-4.0.10.tgz", + "integrity": "sha512-Ptm4k2DqOwxeK/kzu5RaJmNRoGvESrgDXObFcZ8aJZcyXyMBHhM2FqZj6zYKdetadmP3wCwxEHCBuB9xGlRp8A==", "dev": true, "requires": { - "chalk": "^2.4.2", + "chalk": "^3.0.0", "ci-info": "^2.0.0", - "cosmiconfig": "^5.2.1", - "execa": "^1.0.0", - "get-stdin": "^7.0.0", + "cosmiconfig": "^6.0.0", "opencollective-postinstall": "^2.0.2", "pkg-dir": "^4.2.0", "please-upgrade-node": "^3.2.0", - "read-pkg": "^5.2.0", - "run-node": "^1.0.0", - "slash": "^3.0.0" + "slash": "^3.0.0", + "which-pm-runs": "^1.0.0" }, "dependencies": { - "get-stdin": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-7.0.0.tgz", - "integrity": "sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ==", + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "cosmiconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "dev": true, + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, "parse-json": { @@ -8958,23 +9004,20 @@ "lines-and-columns": "^1.1.6" } }, - "read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", "dev": true, "requires": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" + "has-flag": "^4.0.0" } - }, - "type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true } } }, @@ -11425,6 +11468,24 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz", "integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==" }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=", + "dev": true + }, + "lodash.isregexp": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isregexp/-/lodash.isregexp-4.0.1.tgz", + "integrity": "sha1-4T5kezDNVZdSoEzZEghvr32hwws=", + "dev": true + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=", + "dev": true + }, "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -11662,6 +11723,15 @@ "unist-util-visit": "^1.1.0" } }, + "mem": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", + "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", + "dev": true, + "requires": { + "mimic-fn": "^1.0.0" + } + }, "memize": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/memize/-/memize-1.0.5.tgz", @@ -11826,13 +11896,14 @@ } }, "mocha": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.2.2.tgz", - "integrity": "sha512-FgDS9Re79yU1xz5d+C4rv1G7QagNGHZ+iXF81hO8zY35YZZcLEsJVfFolfsqKFWunATEvNzMK0r/CwWd/szO9A==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.0.0.tgz", + "integrity": "sha512-CirsOPbO3jU86YKjjMzFLcXIb5YiGLUrjrXFHoJ3e2z9vWiaZVCZQ2+gtRGMPWF+nFhN6AWwLM/juzAQ6KRkbA==", "dev": true, "requires": { "ansi-colors": "3.2.3", "browser-stdout": "1.3.1", + "chokidar": "3.3.0", "debug": "3.2.6", "diff": "3.5.0", "escape-string-regexp": "1.0.5", @@ -11845,7 +11916,7 @@ "minimatch": "3.0.4", "mkdirp": "0.5.1", "ms": "2.1.1", - "node-environment-flags": "1.0.5", + "node-environment-flags": "1.0.6", "object.assign": "4.1.0", "strip-json-comments": "2.0.1", "supports-color": "6.0.0", @@ -11856,6 +11927,38 @@ "yargs-unparser": "1.6.0" }, "dependencies": { + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "binary-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", + "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==", + "dev": true + }, + "chokidar": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz", + "integrity": "sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A==", + "dev": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.1.1", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.2.0" + } + }, "debug": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", @@ -11874,6 +11977,22 @@ "locate-path": "^3.0.0" } }, + "fsevents": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.2.tgz", + "integrity": "sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==", + "dev": true, + "optional": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, "log-symbols": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", @@ -11889,6 +12008,21 @@ "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "dev": true }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "readdirp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.2.0.tgz", + "integrity": "sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ==", + "dev": true, + "requires": { + "picomatch": "^2.0.4" + } + }, "supports-color": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", @@ -11968,9 +12102,9 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" }, "node-environment-flags": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.5.tgz", - "integrity": "sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz", + "integrity": "sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw==", "dev": true, "requires": { "object.getownpropertydescriptors": "^2.0.3", @@ -13122,15 +13256,6 @@ "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", "dev": true }, - "mem": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", - "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", - "dev": true, - "requires": { - "mimic-fn": "^1.0.0" - } - }, "minimist": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", @@ -13180,14 +13305,6 @@ "requires": { "ansi-regex": "^3.0.0", "ansi-styles": "^3.2.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - } } }, "private": { @@ -13994,12 +14111,6 @@ "is-promise": "^2.1.0" } }, - "run-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/run-node/-/run-node-1.0.0.tgz", - "integrity": "sha512-kc120TBlQ3mih1LSzdAJXo4xn/GWS2ec0l3S+syHDXP9uRr0JAT8Qd3mdMuyjqCzeZktgP3try92cEgf9Nks8A==", - "dev": true - }, "run-parallel": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz", @@ -15694,44 +15805,40 @@ "dev": true }, "stylelint-config-recommended-scss": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/stylelint-config-recommended-scss/-/stylelint-config-recommended-scss-4.0.0.tgz", - "integrity": "sha512-aEy0ENUrH4ASgFCu2mMcqBUAX0l4CPXg0XucJXdW+I7mdqJ7ICddkxP1eamBNBZ1QToc/wsuLmTQcalk3qYpsw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/stylelint-config-recommended-scss/-/stylelint-config-recommended-scss-4.1.0.tgz", + "integrity": "sha512-4012ca0weVi92epm3RRBRZcRJIyl5vJjJ/tJAKng+Qat5+cnmuCwyOI2vXkKdjNfGd0gvzyKCKEkvTMDcbtd7Q==", "dev": true, "requires": { "stylelint-config-recommended": "^3.0.0" } }, "stylelint-config-wordpress": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/stylelint-config-wordpress/-/stylelint-config-wordpress-15.0.0.tgz", - "integrity": "sha512-53wJuCaS35MiO942ML3//lJ3sIvq9LC2Aw8y7DB9yIuO0Eqx2duWUFyxVQQDmEqDvcEg9gVXHKVca/YI+vMKIg==", + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/stylelint-config-wordpress/-/stylelint-config-wordpress-16.0.0.tgz", + "integrity": "sha512-fu8F2a3DTHjo7Id4rUbua2FprieKBDQ+jQ67XVBMsys8YyBjOd/CdcCRiWQug4sA1/A41lq0JlD2gOlR0dWmpw==", "dev": true, "requires": { "stylelint-config-recommended": "^3.0.0", - "stylelint-config-recommended-scss": "^4.0.0", - "stylelint-scss": "^3.11.1" + "stylelint-config-recommended-scss": "^4.1.0", + "stylelint-scss": "^3.13.0" } }, "stylelint-scss": { - "version": "3.11.1", - "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-3.11.1.tgz", - "integrity": "sha512-0FZNSfy5X2Or4VRA3Abwfrw1NHrI6jHT8ji9xSwP8Re2Kno0i90qbHwm8ohPO0kRB1RP9x1vCYBh4Tij+SZjIg==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-3.13.0.tgz", + "integrity": "sha512-SaLnvQyndaPcsgVJsMh6zJ1uKVzkRZJx+Wg/stzoB1mTBdEmGketbHrGbMQNymzH/0mJ06zDSpeCDvNxqIJE5A==", "dev": true, "requires": { - "lodash": "^4.17.15", + "lodash.isboolean": "^3.0.3", + "lodash.isregexp": "^4.0.1", + "lodash.isstring": "^4.0.1", "postcss-media-query-parser": "^0.2.3", "postcss-resolve-nested-selector": "^0.1.1", "postcss-selector-parser": "^6.0.2", "postcss-value-parser": "^4.0.2" }, "dependencies": { - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true - }, "postcss-selector-parser": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz", @@ -16628,6 +16735,12 @@ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" }, + "which-pm-runs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz", + "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=", + "dev": true + }, "wide-align": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", diff --git a/package.json b/package.json index 555451ec7dc..bc417f16af7 100644 --- a/package.json +++ b/package.json @@ -29,8 +29,8 @@ "babel-eslint": "10.0.3", "chai": "4.2.0", "chai-as-promised": "7.1.1", - "commander": "3.0.2", - "eslint-plugin-jest": "23.1.1", + "commander": "4.1.0", + "eslint-plugin-jest": "23.6.0", "config": "3.2.4", "cross-env": "6.0.3", "eslint": "6.8.0", @@ -53,17 +53,17 @@ "grunt-shell": "3.0.1", "grunt-stylelint": "0.13.0", "grunt-wp-i18n": "1.0.3", - "husky": "3.1.0", + "husky": "4.0.10", "istanbul": "1.0.0-alpha.2", "jest": "24.9.0", "jest-puppeteer": "4.4.0", "lint-staged": "9.5.0", - "mocha": "6.2.2", + "mocha": "7.0.0", "node-sass": "4.13.0", "prettier": "github:automattic/calypso-prettier#c56b4251", "puppeteer": "2.0.0", "stylelint": "12.0.1", - "stylelint-config-wordpress": "15.0.0" + "stylelint-config-wordpress": "16.0.0" }, "engines": { "node": ">=10.15.0", diff --git a/readme.txt b/readme.txt index 94a074c3613..bd0350e4727 100644 --- a/readme.txt +++ b/readme.txt @@ -201,6 +201,7 @@ INTERESTED IN DEVELOPMENT? * Tweak - Include processing orders in tracker data when opted in. #25071 * Tweak - Centralize check for default themes to fix Storefront appearance in the Setup Wizard. #25216 * Tweak - Adds a WordPress version check before recommending the WooCommerce Admin plugin during setup. #25260 +* Fix - Added license key support recent changes from MaxMind GeoLite2. #25378 * Fix - Honor tax rounding preference in edit item and refund flows. #24208 * Fix - Prevent incorrect number of decimal points in prices. #24281 * Fix - Fixed initial support for Gutenberg's Experimental Legacy Widget block. #24292 diff --git a/tests/data/GeoLite2-Country.tar.gz b/tests/data/GeoLite2-Country.tar.gz new file mode 100644 index 00000000000..f1aef5d4290 Binary files /dev/null and b/tests/data/GeoLite2-Country.tar.gz differ diff --git a/tests/unit-tests/geolocation/class-wc-tests-geolite-integration.php b/tests/unit-tests/geolocation/class-wc-tests-geolite-integration.php deleted file mode 100644 index 08a277503f2..00000000000 --- a/tests/unit-tests/geolocation/class-wc-tests-geolite-integration.php +++ /dev/null @@ -1,40 +0,0 @@ -assertEquals( 'US', $geolite->get_country_iso( $ipv4 ) ); - - // Check for IPv6. - $this->assertEquals( 'US', $geolite->get_country_iso( $ipv6 ) ); - - // Check for non-valid IP. - $this->assertEquals( '', $geolite->get_country_iso( 'foobar' ) ); - } -} diff --git a/tests/unit-tests/integrations/class-wc-tests-integrations.php b/tests/unit-tests/integrations/class-wc-tests-integrations.php index 083a873f11f..1adc9aff493 100644 --- a/tests/unit-tests/integrations/class-wc-tests-integrations.php +++ b/tests/unit-tests/integrations/class-wc-tests-integrations.php @@ -30,8 +30,8 @@ class WC_Tests_Integrations extends WC_Unit_Test_Case { */ public function test_filter() { $integrations = new WC_Integrations(); - $this->assertEquals( array(), $integrations->integrations ); - $this->assertEquals( array(), $integrations->get_integrations() ); + $this->assertArrayHasKey( 'maxmind_geolocation', $integrations->integrations ); + $this->assertArrayHasKey( 'maxmind_geolocation', $integrations->get_integrations() ); require_once dirname( __FILE__ ) . DIRECTORY_SEPARATOR . 'class-dummy-integration.php'; diff --git a/tests/unit-tests/integrations/maxmind-geolocation/class-wc-tests-maxmind-database.php b/tests/unit-tests/integrations/maxmind-geolocation/class-wc-tests-maxmind-database.php new file mode 100644 index 00000000000..aba5a35a1af --- /dev/null +++ b/tests/unit-tests/integrations/maxmind-geolocation/class-wc-tests-maxmind-database.php @@ -0,0 +1,146 @@ +http_responder = array( $this, 'mock_http_responses' ); + } + + /** + * Tests that the database path filters work as intended. + * + * @expectedDeprecated woocommerce_geolocation_local_database_path + */ + public function test_database_path_filters() { + $database_service = new WC_Integration_MaxMind_Database_Service( '' ); + + $path = $database_service->get_database_path(); + $this->assertEquals( WP_CONTENT_DIR . '/uploads/woocommerce_uploads/' . WC_Integration_MaxMind_Database_Service::DATABASE . WC_Integration_MaxMind_Database_Service::DATABASE_EXTENSION, $path ); + + add_filter( 'woocommerce_geolocation_local_database_path', array( $this, 'filter_database_path_deprecated' ), 1, 2 ); + $path = $database_service->get_database_path(); + remove_filter( 'woocommerce_geolocation_local_database_path', array( $this, 'filter_database_path_deprecated' ), 1 ); + + $this->assertEquals( '/deprecated_filter', $path ); + + add_filter( 'woocommerce_geolocation_local_database_path', array( $this, 'filter_database_path' ) ); + $path = $database_service->get_database_path(); + remove_filter( 'woocommerce_geolocation_local_database_path', array( $this, 'filter_database_path' ) ); + + $this->assertEquals( '/filter', $path ); + + // Now perform any tests with a database file prefix. + $database_service = new WC_Integration_MaxMind_Database_Service( 'testing' ); + + $path = $database_service->get_database_path(); + $this->assertEquals( WP_CONTENT_DIR . '/uploads/woocommerce_uploads/testing-' . WC_Integration_MaxMind_Database_Service::DATABASE . WC_Integration_MaxMind_Database_Service::DATABASE_EXTENSION, $path ); + } + + /** + * Tests that the database download works as expected. + */ + public function test_download_database_works() { + $database_service = new WC_Integration_MaxMind_Database_Service( '' ); + $expected_database = '/tmp/GeoLite2-Country_20200100/GeoLite2-Country.mmdb'; + + $result = $database_service->download_database( 'testing_license' ); + + $this->assertEquals( $expected_database, $result ); + + // Remove the downloaded file and folder. + unlink( $expected_database ); + rmdir( dirname( $expected_database ) ); + } + + /** + * Tests the that database download wraps the download and extraction errors. + */ + public function test_download_database_wraps_errors() { + $database_service = new WC_Integration_MaxMind_Database_Service( '' ); + + $result = $database_service->download_database( 'invalid_license' ); + + $this->assertWPError( $result ); + $this->assertEquals( 'woocommerce_maxmind_geolocation_database_license_key', $result->get_error_code() ); + + $result = $database_service->download_database( 'generic_error' ); + + $this->assertWPError( $result ); + $this->assertEquals( 'woocommerce_maxmind_geolocation_database_download', $result->get_error_code() ); + + $result = $database_service->download_database( 'archive_error' ); + + $this->assertWPError( $result ); + $this->assertEquals( 'woocommerce_maxmind_geolocation_database_archive', $result->get_error_code() ); + } + + /** + * Hook for the deprecated database path filter. + * + * @param string $database_path The path to the database file. + * @param string $deprecated Deprecated since 3.4.0. + * @return string + */ + public function filter_database_path_deprecated( $database_path, $deprecated ) { + return '/deprecated_filter'; + } + + /** + * Hook for the database path filter. + * + * @param string $database_path The path to the database file. + * @return string + */ + public function filter_database_path( $database_path ) { + return '/filter'; + } + + /** + * Helper method to define mocked HTTP responses using WP_HTTP_TestCase. + * Thanks to WP_HTTP_TestCase, it is not necessary to perform a regular request + * to an external server which would significantly slow down the tests. + * + * This function is called by WP_HTTP_TestCase::http_request_listner(). + * + * @param array $request Request arguments. + * @param string $url URL of the request. + * + * @return array|WP_Error|false mocked response, error, or false to let WP perform a regular request. + */ + protected function mock_http_responses( $request, $url ) { + $mocked_response = false; + + if ( 'https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key=testing_license&suffix=tar.gz' === $url ) { + // We need to copy the file to where the request is supposed to have streamed it. + copy( WC_Unit_Tests_Bootstrap::instance()->tests_dir . '/data/GeoLite2-Country.tar.gz', $request['filename'] ); + + $mocked_response = array( + 'response' => array( 'code' => 200 ), + ); + } elseif ( 'https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key=invalid_license&suffix=tar.gz' === $url ) { + return new WP_Error( 'http_404', 'Unauthorized', array( 'code' => 401 ) ); + } elseif ( 'https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key=generic_error&suffix=tar.gz' === $url ) { + return new WP_Error( 'http_404', 'Unauthorized', array( 'code' => 500 ) ); + } elseif ( 'https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key=archive_error&suffix=tar.gz' === $url ) { + $mocked_response = array( + 'response' => array( 'code' => 200 ), + ); + } + + return $mocked_response; + } +} diff --git a/tests/unit-tests/integrations/maxmind-geolocation/class-wc-tests-maxmind-integration.php b/tests/unit-tests/integrations/maxmind-geolocation/class-wc-tests-maxmind-integration.php new file mode 100644 index 00000000000..e7bbbd3e517 --- /dev/null +++ b/tests/unit-tests/integrations/maxmind-geolocation/class-wc-tests-maxmind-integration.php @@ -0,0 +1,125 @@ +database_service = $this->getMockBuilder( 'WC_Integration_maxMind_Database_Service' ) + ->disableOriginalConstructor() + ->getMock(); + add_filter( 'woocommerce_maxmind_geolocation_database_service', array( $this, 'override_integration_service' ) ); + } + + /** + * Make sure that the database is not updated if no target database path is given. + */ + public function test_update_database_does_nothing_without_database_path() { + $this->database_service->expects( $this->once() ) + ->method( 'get_database_path' ) + ->willReturn( '' ); + + ( new WC_Integration_MaxMind_Geolocation() )->update_database(); + } + + /** + * Makes sure that the database can be updated to a given database. + */ + public function test_update_database_to_parameter_file() { + $this->database_service->expects( $this->once() ) + ->method( 'get_database_path' ) + ->willReturn( '/testing' ); + + ( new WC_Integration_MaxMind_Geolocation() )->update_database( '/tmp/noop.mmdb' ); + } + + /** + * Makes sure that the integration uses the license key correctly. + */ + public function test_update_database_uses_license_key() { + $this->database_service->expects( $this->once() ) + ->method( 'get_database_path' ) + ->willReturn( '/testing' ); + $this->database_service->expects( $this->once() ) + ->method( 'download_database' ) + ->with( 'test_license' ) + ->willReturn( '/tmp/' . WC_Integration_MaxMind_Database_Service::DATABASE . '.' . WC_Integration_MaxMind_Database_Service::DATABASE_EXTENSION ); + + $integration = new WC_Integration_MaxMind_Geolocation(); + $integration->update_option( 'license_key', 'test_license' ); + + $integration->update_database(); + } + + /** + * Make sure that the geolocate_ip method does not squash existing country codes. + */ + public function test_geolocate_ip_returns_existing_country_code() { + $data = ( new WC_Integration_MaxMind_Geolocation() )->get_geolocation( array( 'country' => 'US' ), '192.168.1.1' ); + + $this->assertEquals( 'US', $data['country'] ); + } + + /** + * Make sure that the geolocate_ip method does nothing if IP is not set. + */ + public function test_geolocate_ip_returns_empty_without_ip_address() { + $data = ( new WC_Integration_MaxMind_Geolocation() )->get_geolocation( array(), '' ); + + $this->assertEmpty( $data ); + } + + /** + * Make sure that the geolocate_ip method uses the appropriate service methods.. + */ + public function test_geolocate_ip_uses_service() { + $this->database_service->expects( $this->once() ) + ->method( 'get_iso_country_code_for_ip' ) + ->with( '192.168.1.1' ) + ->willReturn( 'US' ); + + $data = ( new WC_Integration_MaxMind_Geolocation() )->get_geolocation( array(), '192.168.1.1' ); + + $this->assertEquals( 'US', $data['country'] ); + } + + /** + * Overrides the filesystem method. + * + * @return string + */ + public function override_filesystem_method() { + return 'Base'; + } + + /** + * Overrides the database service used by the integration. + * + * @return mixed + */ + public function override_integration_service() { + return $this->database_service; + } +} diff --git a/tests/unit-tests/shipping/shipping.php b/tests/unit-tests/shipping/shipping.php index f922899afd7..87c2d6b7569 100644 --- a/tests/unit-tests/shipping/shipping.php +++ b/tests/unit-tests/shipping/shipping.php @@ -60,19 +60,6 @@ class WC_Tests_Shipping extends WC_Unit_Test_Case { ); $this->assertFalse( $result ); - // Failure for invalid postcode. - $result = $shipping->is_package_shippable( - array( - 'destination' => array( - 'country' => 'US', - 'state' => 'CA', - 'postcode' => 'test', - 'address' => '', - ), - ) - ); - $this->assertFalse( $result ); - // Success for correct address. $result = $shipping->is_package_shippable( array( diff --git a/tests/unit-tests/util/api-functions.php b/tests/unit-tests/util/api-functions.php index 9379a4d2198..bdd1ebf7653 100644 --- a/tests/unit-tests/util/api-functions.php +++ b/tests/unit-tests/util/api-functions.php @@ -82,8 +82,12 @@ class WC_Tests_API_Functions extends WC_Unit_Test_Case { */ public function test_wc_rest_upload_image_from_url_should_return_error_when_invalid_image_is_passed() { // empty file. - $expected_error_message = 'Invalid image: File is empty. Please upload something more substantial. This error could also be caused by uploads being disabled in your php.ini or by post_max_size being defined as smaller than upload_max_filesize in php.ini.'; - $result = wc_rest_upload_image_from_url( 'http://somedomain.com/invalid-image-1.png' ); + if ( version_compare( get_bloginfo( 'version' ), '5.4-alpha', '>=' ) ) { + $expected_error_message = 'Invalid image: File is empty. Please upload something more substantial. This error could also be caused by uploads being disabled in your php.ini file or by post_max_size being defined as smaller than upload_max_filesize in php.ini.'; + } else { + $expected_error_message = 'Invalid image: File is empty. Please upload something more substantial. This error could also be caused by uploads being disabled in your php.ini or by post_max_size being defined as smaller than upload_max_filesize in php.ini.'; + } + $result = wc_rest_upload_image_from_url( 'http://somedomain.com/invalid-image-1.png' ); $this->assertWPError( $result ); $this->assertEquals( $expected_error_message, $result->get_error_message() ); diff --git a/woocommerce.php b/woocommerce.php index 1e0e98c5d38..567a96036f1 100644 --- a/woocommerce.php +++ b/woocommerce.php @@ -3,7 +3,7 @@ * Plugin Name: WooCommerce * Plugin URI: https://woocommerce.com/ * Description: An eCommerce toolkit that helps you sell anything. Beautifully. - * Version: 3.9.0-rc.2 + * Version: 3.9.0-rc.3 * Author: Automattic * Author URI: https://woocommerce.com * Text Domain: woocommerce