From c827f94e756bce3fb3bcd10722c520d5f175c6a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Robin?= Date: Mon, 1 Jul 2024 09:38:57 +0200 Subject: [PATCH] Closes #6715 (#6739) Co-authored-by: WordPressFan Co-authored-by: Mathieu Lamiot --- inc/Engine/Admin/Settings/Page.php | 14 +- inc/Engine/Admin/Settings/Subscriber.php | 2 + .../APIHandler/AbstractSafeAPIClient.php | 153 ++++++++++++++++++ .../Common/JobManager/ServiceProvider.php | 10 +- inc/Engine/License/API/PricingClient.php | 71 ++++---- inc/Engine/License/API/UserClient.php | 33 +++- inc/Engine/Plugin/InformationSubscriber.php | 19 ++- inc/Engine/Plugin/ServiceProvider.php | 2 + inc/Engine/Plugin/UpdaterSubscriber.php | 3 +- inc/Engine/WPRocketUninstall.php | 2 + .../License/API/UserClient/getUserData.php | 6 +- .../Engine/WPRocketUninstall/uninstall.php | 2 + .../API/PricingClient/getPricingData.php | 3 +- .../License/API/UserClient/getUserData.php | 12 +- 14 files changed, 259 insertions(+), 73 deletions(-) create mode 100644 inc/Engine/Common/JobManager/APIHandler/AbstractSafeAPIClient.php diff --git a/inc/Engine/Admin/Settings/Page.php b/inc/Engine/Admin/Settings/Page.php index a8dd03a273..d0c85d6577 100644 --- a/inc/Engine/Admin/Settings/Page.php +++ b/inc/Engine/Admin/Settings/Page.php @@ -284,9 +284,17 @@ public function customer_data() { $data['license_type'] = rocket_get_license_type( $user ); - $data['license_class'] = time() < $user->licence_expiration ? 'wpr-isValid' : 'wpr-isInvalid'; - $data['license_expiration'] = date_i18n( get_option( 'date_format' ), (int) $user->licence_expiration ); - $data['is_from_one_dot_com'] = (bool) $user->{'has_one-com_account'}; + if ( ! empty( $user->licence_expiration ) ) { + $data['license_class'] = time() < $user->licence_expiration ? 'wpr-isValid' : 'wpr-isInvalid'; + } + + if ( ! empty( $user->licence_expiration ) ) { + $data['license_expiration'] = date_i18n( get_option( 'date_format' ), (int) $user->licence_expiration ); + } + + if ( isset( $user->{'has_one-com_account'} ) ) { + $data['is_from_one_dot_com'] = (bool) $user->{'has_one-com_account'}; + } return $data; } diff --git a/inc/Engine/Admin/Settings/Subscriber.php b/inc/Engine/Admin/Settings/Subscriber.php index 0f6206b41a..0b649596e5 100644 --- a/inc/Engine/Admin/Settings/Subscriber.php +++ b/inc/Engine/Admin/Settings/Subscriber.php @@ -144,6 +144,8 @@ public function refresh_customer_data() { } delete_transient( 'wp_rocket_customer_data' ); + delete_transient( 'wpr_user_information_timeout_active' ); + delete_transient( 'wpr_user_information_timeout' ); return wp_send_json_success( $this->page->customer_data() ); } diff --git a/inc/Engine/Common/JobManager/APIHandler/AbstractSafeAPIClient.php b/inc/Engine/Common/JobManager/APIHandler/AbstractSafeAPIClient.php new file mode 100644 index 0000000000..2e4d0acb48 --- /dev/null +++ b/inc/Engine/Common/JobManager/APIHandler/AbstractSafeAPIClient.php @@ -0,0 +1,153 @@ +send_request( 'GET', $params, $safe ); + } + + /** + * Send a POST request. + * + * @param array $params The request parameters. + * @param bool $safe Send safe request WP functions or not, default to not. + * + * @return mixed The response from the API. + */ + public function send_post_request( $params = [], $safe = false ) { + return $this->send_request( 'POST', $params, $safe ); + } + + /** + * Send a request to the API. + * + * @param string $method The HTTP method (GET or POST). + * @param array $params The request parameters. + * @param bool $safe Send safe request WP functions or not, default to not. + * @return mixed The response from the API, or WP_Error if a timeout is active. + */ + private function send_request( $method, $params = [], $safe = false ) { + $api_url = $this->get_api_url(); + + $transient_key = $this->get_transient_key(); + if ( get_transient( $transient_key . '_timeout_active' ) ) { + return new WP_Error( 429, __( 'Too many requests.', 'rocket' ) ); + } + // Get previous_expiration early to avoid multiple parallel requests increasing the expiration multiple times. + $previous_expiration = (int) get_transient( $transient_key . '_timeout' ); + + $params['method'] = strtoupper( $method ); + + $response = $this->send_remote_request( $api_url, $method, $params, $safe ); + + if ( is_wp_error( $response ) ) { + $this->set_timeout_transients( $previous_expiration ); + return $response; + } + + $body = wp_remote_retrieve_body( $response ); + if ( empty( $body ) || ( is_array( $response ) && ! empty( $response['response']['code'] ) && 200 !== $response['response']['code'] ) ) { + $this->set_timeout_transients( $previous_expiration ); + return new WP_Error( 500, __( 'Not valid response.', 'rocket' ) ); + } + + $this->delete_timeout_transients(); + + return $response; + } + + /** + * Set the timeout transients. + * + * @param string $previous_expiration The previous value of _timeout_active transient. + */ + private function set_timeout_transients( $previous_expiration ) { + $transient_key = $this->get_transient_key(); + + $expiration = ( 0 === $previous_expiration ) + ? 300 + : ( 2 * $previous_expiration <= DAY_IN_SECONDS + ? 2 * $previous_expiration + : DAY_IN_SECONDS + ); + + set_transient( $transient_key . '_timeout', $expiration, WEEK_IN_SECONDS ); + set_transient( $transient_key . '_timeout_active', true, $expiration ); + } + + /** + * Delete the timeout transients. + * + * This method deletes the timeout transients for the API requests. It uses the transient key obtained from the `get_transient_key` method. + * The transients deleted are: + * - `{transient_key}_timeout_active`: This transient indicates if a timeout is currently active. + * - `{transient_key}_timeout`: This transient stores the timeout duration. + * + * @return void + */ + private function delete_timeout_transients() { + $transient_key = $this->get_transient_key(); + delete_transient( $transient_key . '_timeout_active' ); + delete_transient( $transient_key . '_timeout' ); + } + + /** + * Decide which WP core function will be used to send the request based on the params. + * + * @param string $api_url API Url. + * @param string $method Request method (GET or POST). + * @param array $params Parameters being sent with the request. + * @param bool $safe Send safe request WP functions or not, default to not. + * @return array|WP_Error + */ + private function send_remote_request( $api_url, $method, $params, $safe ) { + if ( ! $safe ) { + return wp_remote_request( $api_url, $params ); + } + + unset( $params['method'] ); + + switch ( $method ) { + case 'GET': + return wp_safe_remote_get( $api_url, $params ); + case 'POST': + return wp_safe_remote_post( $api_url, $params ); + } + + return new WP_Error( 400, __( 'Not valid request type.', 'rocket' ) ); + } +} diff --git a/inc/Engine/Common/JobManager/ServiceProvider.php b/inc/Engine/Common/JobManager/ServiceProvider.php index e12ff10ba9..d404ef36ff 100644 --- a/inc/Engine/Common/JobManager/ServiceProvider.php +++ b/inc/Engine/Common/JobManager/ServiceProvider.php @@ -4,16 +4,12 @@ namespace WP_Rocket\Engine\Common\JobManager; use WP_Rocket\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; -use WP_Rocket\Engine\Common\JobManager\Strategy\Context\RetryContext; -use WP_Rocket\Engine\Common\JobManager\Strategy\Factory\StrategyFactory; -use WP_Rocket\Engine\Common\JobManager\Strategy\Strategies\DefaultProcess; -use WP_Rocket\Engine\Common\JobManager\Strategy\Strategies\JobSetFail; -use WP_Rocket\Engine\Common\JobManager\Strategy\Strategies\ResetRetryProcess; use WP_Rocket\Engine\Common\Clock\WPRClock; -use WP_Rocket\Engine\Common\JobManager\Queue\Queue; use WP_Rocket\Engine\Common\JobManager\APIHandler\APIClient; use WP_Rocket\Engine\Common\JobManager\Cron\Subscriber as CronSubscriber; - +use WP_Rocket\Engine\Common\JobManager\Queue\Queue; +use WP_Rocket\Engine\Common\JobManager\Strategy\Context\RetryContext; +use WP_Rocket\Engine\Common\JobManager\Strategy\Factory\StrategyFactory; class ServiceProvider extends AbstractServiceProvider { /** diff --git a/inc/Engine/License/API/PricingClient.php b/inc/Engine/License/API/PricingClient.php index 045482293d..6d29d47023 100644 --- a/inc/Engine/License/API/PricingClient.php +++ b/inc/Engine/License/API/PricingClient.php @@ -2,9 +2,33 @@ namespace WP_Rocket\Engine\License\API; -class PricingClient { +use WP_Rocket\Engine\Common\JobManager\APIHandler\AbstractSafeAPIClient; + +class PricingClient extends AbstractSafeAPIClient { const PRICING_ENDPOINT = 'https://wp-rocket.me/stat/1.0/wp-rocket/pricing-2023.php'; + /** + * Get the transient key for plugin updates. + * + * This method returns the transient key used for caching plugin updates. + * + * @return string The transient key for plugin updates. + */ + protected function get_transient_key() { + return 'wp_rocket_pricing'; + } + + /** + * Get the API URL for plugin updates. + * + * This method returns the API URL used for fetching plugin updates. + * + * @return string The API URL for plugin updates. + */ + protected function get_api_url() { + return self::PRICING_ENDPOINT; + } + /** * Gets pricing data from cache if it exists, else gets it from the pricing endpoint * @@ -40,51 +64,12 @@ public function get_pricing_data() { * @return bool|object */ private function get_raw_pricing_data() { - if ( (bool) get_transient( 'wp_rocket_pricing_timeout_active' ) ) { - return false; - } - - $response = wp_safe_remote_get( - self::PRICING_ENDPOINT - ); - - if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { - $this->set_timeout_transients(); - - return false; - } - - $body = wp_remote_retrieve_body( $response ); - - if ( empty( $body ) ) { - $this->set_timeout_transients(); + $response = $this->send_get_request( [], true ); + if ( is_wp_error( $response ) || ( is_array( $response ) && 200 !== $response['response']['code'] ) ) { return false; } - delete_transient( 'wp_rocket_pricing_timeout' ); - delete_transient( 'wp_rocket_pricing_timeout_active' ); - - return json_decode( $body ); - } - - /** - * Set pricing timeout transients. - * - * @since 3.8.4 - * - * @return void - */ - private function set_timeout_transients() { - $timeout = (int) get_transient( 'wp_rocket_pricing_timeout' ); - $timeout = ( 0 === $timeout ) - ? 300 - : ( 2 * $timeout <= DAY_IN_SECONDS - ? 2 * $timeout : - DAY_IN_SECONDS - ); - - set_transient( 'wp_rocket_pricing_timeout', $timeout, WEEK_IN_SECONDS ); - set_transient( 'wp_rocket_pricing_timeout_active', true, $timeout ); + return json_decode( wp_remote_retrieve_body( $response ) ); } } diff --git a/inc/Engine/License/API/UserClient.php b/inc/Engine/License/API/UserClient.php index 00d6f15faa..62ab12f0f4 100644 --- a/inc/Engine/License/API/UserClient.php +++ b/inc/Engine/License/API/UserClient.php @@ -3,8 +3,9 @@ namespace WP_Rocket\Engine\License\API; use WP_Rocket\Admin\Options_Data; +use WP_Rocket\Engine\Common\JobManager\APIHandler\AbstractSafeAPIClient; -class UserClient { +class UserClient extends AbstractSafeAPIClient { const USER_ENDPOINT = 'https://wp-rocket.me/stat/1.0/wp-rocket/user.php'; /** @@ -14,6 +15,28 @@ class UserClient { */ private $options; + /** + * Get the transient key for plugin updates. + * + * This method returns the transient key used for caching plugin updates. + * + * @return string The transient key for plugin updates. + */ + protected function get_transient_key() { + return 'wpr_user_information'; + } + + /** + * Get the API URL for plugin updates. + * + * This method returns the API URL used for fetching plugin updates. + * + * @return string The API URL for plugin updates. + */ + protected function get_api_url() { + return self::USER_ENDPOINT; + } + /** * Instantiate the class * @@ -65,14 +88,14 @@ private function get_raw_user_data() { ? $this->options->get( 'consumer_email', '' ) : rocket_get_constant( 'WP_ROCKET_EMAIL', '' ); - $response = wp_safe_remote_post( - self::USER_ENDPOINT, + $response = $this->send_post_request( [ 'body' => 'user_id=' . rawurlencode( $customer_email ) . '&consumer_key=' . sanitize_key( $customer_key ), - ] + ], + true ); - if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { + if ( is_wp_error( $response ) ) { return false; } diff --git a/inc/Engine/Plugin/InformationSubscriber.php b/inc/Engine/Plugin/InformationSubscriber.php index 93f11c3e78..f0398ef45b 100644 --- a/inc/Engine/Plugin/InformationSubscriber.php +++ b/inc/Engine/Plugin/InformationSubscriber.php @@ -2,6 +2,7 @@ namespace WP_Rocket\Engine\Plugin; use WP_Rocket\Event_Management\Subscriber_Interface; +use WP_Error; /** * Manages the plugin information. @@ -33,9 +34,7 @@ class InformationSubscriber implements Subscriber_Interface { /** * Constructor * - * @param array $args { - * Required arguments to populate the class properties. - * + * @param array $args { Required arguments to populate the class properties. * @type string $plugin_file Full path to the plugin. * @type string $api_url URL to contact to get update info. * } @@ -79,12 +78,12 @@ public function exclude_rocket_from_wp_info( $bool, $action, $args ) { // phpcs: } /** - * Insert WP Rocket plugin info. + * Insert WP Rocket plugin info. * - * @param object|\WP_Error $res Response object or WP_Error. - * @param string $action The type of information being requested from the Plugin Install API. - * @param object $args Plugin API arguments. - * @return object|\WP_Error Updated response object or WP_Error. + * @param object|WP_Error $res Response object or WP_Error. + * @param string $action The type of information being requested from the Plugin Install API. + * @param object $args Plugin API arguments. + * @return object|WP_Error Updated response object or WP_Error. */ public function add_rocket_info( $res, $action, $args ) { if ( ! $this->is_requesting_rocket_info( $action, $args ) || empty( $res->external ) ) { @@ -112,7 +111,7 @@ public function add_wp_tested_version( $wp_tested_version ): string { } /** - * Tell if requesting WP Rocket plugin info. + * Tell if requesting WP Rocket plugin info. * * @param string $action The type of information being requested from the Plugin Install API. * @param object $args Plugin API arguments. @@ -125,7 +124,7 @@ private function is_requesting_rocket_info( $action, $args ) { /** * Gets the plugin information data * - * @return object|\WP_Error + * @return object|WP_Error */ private function get_plugin_information() { $response = wp_remote_get( $this->api_url ); diff --git a/inc/Engine/Plugin/ServiceProvider.php b/inc/Engine/Plugin/ServiceProvider.php index 398e79ec43..809bcad86a 100644 --- a/inc/Engine/Plugin/ServiceProvider.php +++ b/inc/Engine/Plugin/ServiceProvider.php @@ -55,6 +55,7 @@ public function register(): void { ] ) ->addTag( 'common_subscriber' ); + $this->getContainer()->addShared( 'plugin_information_subscriber', InformationSubscriber::class ) ->addArgument( [ @@ -63,6 +64,7 @@ public function register(): void { ] ) ->addTag( 'common_subscriber' ); + $this->getContainer()->addShared( 'plugin_updater_subscriber', UpdaterSubscriber::class ) ->addArgument( $this->getContainer()->get( 'plugin_renewal_notice' ) ) ->addArgument( diff --git a/inc/Engine/Plugin/UpdaterSubscriber.php b/inc/Engine/Plugin/UpdaterSubscriber.php index 5224b7aa8c..c5f847daa5 100644 --- a/inc/Engine/Plugin/UpdaterSubscriber.php +++ b/inc/Engine/Plugin/UpdaterSubscriber.php @@ -84,9 +84,8 @@ class UpdaterSubscriber implements Event_Manager_Aware_Subscriber_Interface { * Constructor * * @param RenewalNotice $renewal_notice RenewalNotice instance. - * @param array $args { - * Required arguments to populate the class properties. * + * @param array $args { Required arguments to populate the class properties. * @type string $plugin_file Full path to the plugin. * @type string $plugin_version Current version of the plugin. * @type string $vendor_url URL to the plugin provider. diff --git a/inc/Engine/WPRocketUninstall.php b/inc/Engine/WPRocketUninstall.php index eec668fc7b..36f29647b5 100644 --- a/inc/Engine/WPRocketUninstall.php +++ b/inc/Engine/WPRocketUninstall.php @@ -80,6 +80,8 @@ class WPRocketUninstall { 'wp_rocket_pricing_timeout', 'wp_rocket_pricing_timeout_active', 'rocket_get_refreshed_fragments_cache', + 'wpr_user_information_timeout_active', + 'wpr_user_information_timeout', ]; /** diff --git a/tests/Integration/inc/Engine/License/API/UserClient/getUserData.php b/tests/Integration/inc/Engine/License/API/UserClient/getUserData.php index 8fc3eea35c..9346c5c88b 100644 --- a/tests/Integration/inc/Engine/License/API/UserClient/getUserData.php +++ b/tests/Integration/inc/Engine/License/API/UserClient/getUserData.php @@ -30,13 +30,17 @@ public static function set_up_before_class() { public function set_up() { parent::set_up(); - + delete_transient( 'wp_rocket_customer_data' ); + delete_transient( 'wpr_user_information_timeout_active' ); + delete_transient( 'wpr_user_information_timeout' ); add_filter( 'pre_get_rocket_option_consumer_email', [ $this, 'set_consumer_email' ] ); add_filter( 'pre_get_rocket_option_consumer_key', [ $this, 'set_consumer_key' ] ); } public function tear_down() { delete_transient( 'wp_rocket_customer_data' ); + delete_transient( 'wpr_user_information_timeout_active' ); + delete_transient( 'wpr_user_information_timeout' ); remove_filter( 'pre_get_rocket_option_consumer_email', [ $this, 'set_consumer_email' ] ); remove_filter( 'pre_get_rocket_option_consumer_key', [ $this, 'set_consumer_key' ] ); remove_filter( 'pre_http_request', [ $this, 'set_response' ] ); diff --git a/tests/Integration/inc/Engine/WPRocketUninstall/uninstall.php b/tests/Integration/inc/Engine/WPRocketUninstall/uninstall.php index bfbc424f50..8bf3c1f7ba 100644 --- a/tests/Integration/inc/Engine/WPRocketUninstall/uninstall.php +++ b/tests/Integration/inc/Engine/WPRocketUninstall/uninstall.php @@ -62,6 +62,8 @@ class Test_Uninstall extends FilesystemTestCase { 'wp_rocket_pricing_timeout' => null, 'wp_rocket_pricing_timeout_active' => null, 'rocket_get_refreshed_fragments_cache' => null, + 'wpr_user_information_timeout_active' => null, + 'wpr_user_information_timeout' => null, ]; private $events = [ diff --git a/tests/Unit/inc/Engine/License/API/PricingClient/getPricingData.php b/tests/Unit/inc/Engine/License/API/PricingClient/getPricingData.php index 72595b52de..50c9defa49 100644 --- a/tests/Unit/inc/Engine/License/API/PricingClient/getPricingData.php +++ b/tests/Unit/inc/Engine/License/API/PricingClient/getPricingData.php @@ -17,6 +17,7 @@ class GetPricingData extends TestCase { * @dataProvider configTestData */ public function testShouldReturnExpected( $config, $expected ) { + $this->stubTranslationFunctions(); $client = new PricingClient(); Functions\expect( 'get_transient' ) @@ -48,7 +49,7 @@ public function testShouldReturnExpected( $config, $expected ) { if ( false !== $config['response'] ) { Functions\expect( 'wp_safe_remote_get' ) ->once() - ->with( PricingClient::PRICING_ENDPOINT ) + ->with( PricingClient::PRICING_ENDPOINT, [] ) ->andReturn( $config['response'] ); if ( diff --git a/tests/Unit/inc/Engine/License/API/UserClient/getUserData.php b/tests/Unit/inc/Engine/License/API/UserClient/getUserData.php index bcf4f1559f..ce85508c20 100644 --- a/tests/Unit/inc/Engine/License/API/UserClient/getUserData.php +++ b/tests/Unit/inc/Engine/License/API/UserClient/getUserData.php @@ -14,7 +14,7 @@ * * @group License */ -class GetPricingData extends TestCase { +class GetUserData extends TestCase { use ApiTrait; protected static $api_credentials_config_file = 'license.php'; @@ -39,6 +39,8 @@ public function setUp(): void { * @dataProvider configTestData */ public function testShouldReturnExpected( $config, $expected ) { + $this->stubTranslationFunctions(); + Functions\expect( 'get_transient' ) ->atLeast()->once() ->with( 'wp_rocket_customer_data' ) @@ -96,6 +98,14 @@ public function testShouldReturnExpected( $config, $expected ) { Functions\expect( 'set_transient' )->never(); } + if ( ! $expected ) { + Functions\expect( 'set_transient' )->with( 'wpr_user_information_timeout' )->andReturn(null); + Functions\expect( 'set_transient' )->with( 'wpr_user_information_timeout_active' )->andReturn(null); + } else { + Functions\expect( 'delete_transient' )->with( 'wpr_user_information_timeout' )->andReturn(null); + Functions\expect( 'delete_transient' )->with( 'wpr_user_information_timeout_active' )->andReturn(null); + } + $this->assertEquals( $expected, $this->client->get_user_data()