From ba9452187595d48dea8cb19e434c9c092509614c Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Wed, 15 Apr 2026 14:11:08 +0200 Subject: [PATCH 1/2] Add LiteSpeed Cache integration with 6 interactive tasks Add suggested tasks for LiteSpeed Cache plugin users to enable key performance settings directly from Progress Planner. Follows the existing AIOSEO integration pattern with custom AJAX handlers. Tasks: page cache, browser cache, CSS/JS minification, image lazy loading, and guest mode. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../litespeed-cache-browser-cache.js | 32 +++++ .../litespeed-cache-css-minification.js | 32 +++++ .../litespeed-cache-guest-mode.js | 32 +++++ .../litespeed-cache-image-lazy-load.js | 32 +++++ .../litespeed-cache-js-minification.js | 32 +++++ .../litespeed-cache-page-cache.js | 32 +++++ .../suggested-tasks/class-tasks-manager.php | 4 + .../class-add-litespeed-cache-providers.php | 52 +++++++ .../litespeed-cache/class-browser-cache.php | 126 +++++++++++++++++ .../class-css-minification.php | 126 +++++++++++++++++ .../litespeed-cache/class-guest-mode.php | 126 +++++++++++++++++ .../litespeed-cache/class-image-lazy-load.php | 126 +++++++++++++++++ .../litespeed-cache/class-js-minification.php | 126 +++++++++++++++++ ...s-litespeed-cache-interactive-provider.php | 56 ++++++++ .../litespeed-cache/class-page-cache.php | 127 ++++++++++++++++++ .../class-ajax-security-litespeed-cache.php | 53 ++++++++ 16 files changed, 1114 insertions(+) create mode 100644 assets/js/recommendations/litespeed-cache-browser-cache.js create mode 100644 assets/js/recommendations/litespeed-cache-css-minification.js create mode 100644 assets/js/recommendations/litespeed-cache-guest-mode.js create mode 100644 assets/js/recommendations/litespeed-cache-image-lazy-load.js create mode 100644 assets/js/recommendations/litespeed-cache-js-minification.js create mode 100644 assets/js/recommendations/litespeed-cache-page-cache.js create mode 100644 classes/suggested-tasks/providers/integrations/litespeed-cache/class-add-litespeed-cache-providers.php create mode 100644 classes/suggested-tasks/providers/integrations/litespeed-cache/class-browser-cache.php create mode 100644 classes/suggested-tasks/providers/integrations/litespeed-cache/class-css-minification.php create mode 100644 classes/suggested-tasks/providers/integrations/litespeed-cache/class-guest-mode.php create mode 100644 classes/suggested-tasks/providers/integrations/litespeed-cache/class-image-lazy-load.php create mode 100644 classes/suggested-tasks/providers/integrations/litespeed-cache/class-js-minification.php create mode 100644 classes/suggested-tasks/providers/integrations/litespeed-cache/class-litespeed-cache-interactive-provider.php create mode 100644 classes/suggested-tasks/providers/integrations/litespeed-cache/class-page-cache.php create mode 100644 classes/suggested-tasks/providers/traits/class-ajax-security-litespeed-cache.php diff --git a/assets/js/recommendations/litespeed-cache-browser-cache.js b/assets/js/recommendations/litespeed-cache-browser-cache.js new file mode 100644 index 000000000..55dd1340e --- /dev/null +++ b/assets/js/recommendations/litespeed-cache-browser-cache.js @@ -0,0 +1,32 @@ +/* global prplInteractiveTaskFormListener, progressPlanner */ + +/* + * LiteSpeed Cache: enable browser cache. + * + * Dependencies: progress-planner/recommendations/interactive-task + */ + +prplInteractiveTaskFormListener.customSubmit( { + taskId: 'litespeed-cache-browser-cache', + popoverId: 'prpl-popover-litespeed-cache-browser-cache', + callback: () => { + return new Promise( ( resolve, reject ) => { + fetch( progressPlanner.ajaxUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams( { + action: 'prpl_interactive_task_submit_litespeed-cache-browser-cache', + nonce: progressPlanner.nonce, + } ), + } ) + .then( ( response ) => { + resolve( { response, success: true } ); + } ) + .catch( ( error ) => { + reject( { success: false, error } ); + } ); + } ); + }, +} ); diff --git a/assets/js/recommendations/litespeed-cache-css-minification.js b/assets/js/recommendations/litespeed-cache-css-minification.js new file mode 100644 index 000000000..59dc868fa --- /dev/null +++ b/assets/js/recommendations/litespeed-cache-css-minification.js @@ -0,0 +1,32 @@ +/* global prplInteractiveTaskFormListener, progressPlanner */ + +/* + * LiteSpeed Cache: enable CSS minification. + * + * Dependencies: progress-planner/recommendations/interactive-task + */ + +prplInteractiveTaskFormListener.customSubmit( { + taskId: 'litespeed-cache-css-minification', + popoverId: 'prpl-popover-litespeed-cache-css-minification', + callback: () => { + return new Promise( ( resolve, reject ) => { + fetch( progressPlanner.ajaxUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams( { + action: 'prpl_interactive_task_submit_litespeed-cache-css-minification', + nonce: progressPlanner.nonce, + } ), + } ) + .then( ( response ) => { + resolve( { response, success: true } ); + } ) + .catch( ( error ) => { + reject( { success: false, error } ); + } ); + } ); + }, +} ); diff --git a/assets/js/recommendations/litespeed-cache-guest-mode.js b/assets/js/recommendations/litespeed-cache-guest-mode.js new file mode 100644 index 000000000..638e16c41 --- /dev/null +++ b/assets/js/recommendations/litespeed-cache-guest-mode.js @@ -0,0 +1,32 @@ +/* global prplInteractiveTaskFormListener, progressPlanner */ + +/* + * LiteSpeed Cache: enable guest mode. + * + * Dependencies: progress-planner/recommendations/interactive-task + */ + +prplInteractiveTaskFormListener.customSubmit( { + taskId: 'litespeed-cache-guest-mode', + popoverId: 'prpl-popover-litespeed-cache-guest-mode', + callback: () => { + return new Promise( ( resolve, reject ) => { + fetch( progressPlanner.ajaxUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams( { + action: 'prpl_interactive_task_submit_litespeed-cache-guest-mode', + nonce: progressPlanner.nonce, + } ), + } ) + .then( ( response ) => { + resolve( { response, success: true } ); + } ) + .catch( ( error ) => { + reject( { success: false, error } ); + } ); + } ); + }, +} ); diff --git a/assets/js/recommendations/litespeed-cache-image-lazy-load.js b/assets/js/recommendations/litespeed-cache-image-lazy-load.js new file mode 100644 index 000000000..d91fdbb94 --- /dev/null +++ b/assets/js/recommendations/litespeed-cache-image-lazy-load.js @@ -0,0 +1,32 @@ +/* global prplInteractiveTaskFormListener, progressPlanner */ + +/* + * LiteSpeed Cache: enable image lazy loading. + * + * Dependencies: progress-planner/recommendations/interactive-task + */ + +prplInteractiveTaskFormListener.customSubmit( { + taskId: 'litespeed-cache-image-lazy-load', + popoverId: 'prpl-popover-litespeed-cache-image-lazy-load', + callback: () => { + return new Promise( ( resolve, reject ) => { + fetch( progressPlanner.ajaxUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams( { + action: 'prpl_interactive_task_submit_litespeed-cache-image-lazy-load', + nonce: progressPlanner.nonce, + } ), + } ) + .then( ( response ) => { + resolve( { response, success: true } ); + } ) + .catch( ( error ) => { + reject( { success: false, error } ); + } ); + } ); + }, +} ); diff --git a/assets/js/recommendations/litespeed-cache-js-minification.js b/assets/js/recommendations/litespeed-cache-js-minification.js new file mode 100644 index 000000000..50952dae1 --- /dev/null +++ b/assets/js/recommendations/litespeed-cache-js-minification.js @@ -0,0 +1,32 @@ +/* global prplInteractiveTaskFormListener, progressPlanner */ + +/* + * LiteSpeed Cache: enable JS minification. + * + * Dependencies: progress-planner/recommendations/interactive-task + */ + +prplInteractiveTaskFormListener.customSubmit( { + taskId: 'litespeed-cache-js-minification', + popoverId: 'prpl-popover-litespeed-cache-js-minification', + callback: () => { + return new Promise( ( resolve, reject ) => { + fetch( progressPlanner.ajaxUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams( { + action: 'prpl_interactive_task_submit_litespeed-cache-js-minification', + nonce: progressPlanner.nonce, + } ), + } ) + .then( ( response ) => { + resolve( { response, success: true } ); + } ) + .catch( ( error ) => { + reject( { success: false, error } ); + } ); + } ); + }, +} ); diff --git a/assets/js/recommendations/litespeed-cache-page-cache.js b/assets/js/recommendations/litespeed-cache-page-cache.js new file mode 100644 index 000000000..da345b1f5 --- /dev/null +++ b/assets/js/recommendations/litespeed-cache-page-cache.js @@ -0,0 +1,32 @@ +/* global prplInteractiveTaskFormListener, progressPlanner */ + +/* + * LiteSpeed Cache: enable page cache. + * + * Dependencies: progress-planner/recommendations/interactive-task + */ + +prplInteractiveTaskFormListener.customSubmit( { + taskId: 'litespeed-cache-page-cache', + popoverId: 'prpl-popover-litespeed-cache-page-cache', + callback: () => { + return new Promise( ( resolve, reject ) => { + fetch( progressPlanner.ajaxUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams( { + action: 'prpl_interactive_task_submit_litespeed-cache-page-cache', + nonce: progressPlanner.nonce, + } ), + } ) + .then( ( response ) => { + resolve( { response, success: true } ); + } ) + .catch( ( error ) => { + reject( { success: false, error } ); + } ); + } ); + }, +} ); diff --git a/classes/suggested-tasks/class-tasks-manager.php b/classes/suggested-tasks/class-tasks-manager.php index 64b2aefb5..f0947a0f1 100644 --- a/classes/suggested-tasks/class-tasks-manager.php +++ b/classes/suggested-tasks/class-tasks-manager.php @@ -25,6 +25,7 @@ use Progress_Planner\Suggested_Tasks\Tasks_Interface; use Progress_Planner\Suggested_Tasks\Providers\Integrations\Yoast\Add_Yoast_Providers; use Progress_Planner\Suggested_Tasks\Providers\Integrations\AIOSEO\Add_AIOSEO_Providers; +use Progress_Planner\Suggested_Tasks\Providers\Integrations\Litespeed_Cache\Add_Litespeed_Cache_Providers; use Progress_Planner\Suggested_Tasks\Providers\User as User_Tasks; use Progress_Planner\Suggested_Tasks\Providers\Email_Sending; use Progress_Planner\Suggested_Tasks\Providers\Set_Valuable_Post_Types; @@ -119,6 +120,9 @@ public function add_plugin_integration() { // All in One SEO integration. new Add_AIOSEO_Providers(); + + // LiteSpeed Cache integration. + new Add_Litespeed_Cache_Providers(); } /** diff --git a/classes/suggested-tasks/providers/integrations/litespeed-cache/class-add-litespeed-cache-providers.php b/classes/suggested-tasks/providers/integrations/litespeed-cache/class-add-litespeed-cache-providers.php new file mode 100644 index 000000000..8f86fafa6 --- /dev/null +++ b/classes/suggested-tasks/providers/integrations/litespeed-cache/class-add-litespeed-cache-providers.php @@ -0,0 +1,52 @@ +providers = [ + new Page_Cache(), + new Browser_Cache(), + new Css_Minification(), + new Js_Minification(), + new Image_Lazy_Load(), + new Guest_Mode(), + ]; + + return \array_merge( + $providers, + $this->providers + ); + } +} diff --git a/classes/suggested-tasks/providers/integrations/litespeed-cache/class-browser-cache.php b/classes/suggested-tasks/providers/integrations/litespeed-cache/class-browser-cache.php new file mode 100644 index 000000000..4db049fc1 --- /dev/null +++ b/classes/suggested-tasks/providers/integrations/litespeed-cache/class-browser-cache.php @@ -0,0 +1,126 @@ +get_litespeed_option( 'cache-browser' ); + } + + /** + * Get the popover instructions. + * + * @return void + */ + public function print_popover_instructions() { + echo '

'; + \esc_html_e( 'Browser caching tells visitors\' browsers to store static files (images, CSS, JavaScript) locally. When they return to your site, these files load from their device instead of being downloaded again, making repeat visits significantly faster.', 'progress-planner' ); + echo '

'; + } + + /** + * Print the popover input field for the form. + * + * @return void + */ + public function print_popover_form_contents() { + $this->print_submit_button( \__( 'Enable browser cache', 'progress-planner' ) ); + } + + /** + * Handle the interactive task submit. + * + * @return void + */ + public function handle_interactive_task_specific_submit() { + $this->verify_litespeed_cache_ajax_security(); + + $this->update_litespeed_option( 'cache-browser', 1 ); + + \wp_send_json_success( [ 'message' => \esc_html__( 'Setting updated.', 'progress-planner' ) ] ); + } + + /** + * Add task actions specific to this task. + * + * @param array $data The task data. + * @param array $actions The existing actions. + * + * @return array + */ + public function add_task_actions( $data = [], $actions = [] ) { + return $this->add_popover_action( $actions, \__( 'Enable', 'progress-planner' ) ); + } +} diff --git a/classes/suggested-tasks/providers/integrations/litespeed-cache/class-css-minification.php b/classes/suggested-tasks/providers/integrations/litespeed-cache/class-css-minification.php new file mode 100644 index 000000000..c9346566e --- /dev/null +++ b/classes/suggested-tasks/providers/integrations/litespeed-cache/class-css-minification.php @@ -0,0 +1,126 @@ +get_litespeed_option( 'optm-css_min' ); + } + + /** + * Get the popover instructions. + * + * @return void + */ + public function print_popover_instructions() { + echo '

'; + \esc_html_e( 'CSS minification removes unnecessary whitespace, comments, and formatting from your CSS files, reducing their size by 10-30%. This is a low-risk optimization that helps pages load faster by reducing the amount of data browsers need to download.', 'progress-planner' ); + echo '

'; + } + + /** + * Print the popover input field for the form. + * + * @return void + */ + public function print_popover_form_contents() { + $this->print_submit_button( \__( 'Enable CSS minification', 'progress-planner' ) ); + } + + /** + * Handle the interactive task submit. + * + * @return void + */ + public function handle_interactive_task_specific_submit() { + $this->verify_litespeed_cache_ajax_security(); + + $this->update_litespeed_option( 'optm-css_min', 1 ); + + \wp_send_json_success( [ 'message' => \esc_html__( 'Setting updated.', 'progress-planner' ) ] ); + } + + /** + * Add task actions specific to this task. + * + * @param array $data The task data. + * @param array $actions The existing actions. + * + * @return array + */ + public function add_task_actions( $data = [], $actions = [] ) { + return $this->add_popover_action( $actions, \__( 'Enable', 'progress-planner' ) ); + } +} diff --git a/classes/suggested-tasks/providers/integrations/litespeed-cache/class-guest-mode.php b/classes/suggested-tasks/providers/integrations/litespeed-cache/class-guest-mode.php new file mode 100644 index 000000000..6289759ca --- /dev/null +++ b/classes/suggested-tasks/providers/integrations/litespeed-cache/class-guest-mode.php @@ -0,0 +1,126 @@ +get_litespeed_option( 'guest' ); + } + + /** + * Get the popover instructions. + * + * @return void + */ + public function print_popover_instructions() { + echo '

'; + \esc_html_e( 'Guest mode serves a cached version of your pages to first-time visitors before determining their specific cache vary. This improves the cache hit ratio and ensures most anonymous visitors get the fastest possible page loads.', 'progress-planner' ); + echo '

'; + } + + /** + * Print the popover input field for the form. + * + * @return void + */ + public function print_popover_form_contents() { + $this->print_submit_button( \__( 'Enable guest mode', 'progress-planner' ) ); + } + + /** + * Handle the interactive task submit. + * + * @return void + */ + public function handle_interactive_task_specific_submit() { + $this->verify_litespeed_cache_ajax_security(); + + $this->update_litespeed_option( 'guest', 1 ); + + \wp_send_json_success( [ 'message' => \esc_html__( 'Setting updated.', 'progress-planner' ) ] ); + } + + /** + * Add task actions specific to this task. + * + * @param array $data The task data. + * @param array $actions The existing actions. + * + * @return array + */ + public function add_task_actions( $data = [], $actions = [] ) { + return $this->add_popover_action( $actions, \__( 'Enable', 'progress-planner' ) ); + } +} diff --git a/classes/suggested-tasks/providers/integrations/litespeed-cache/class-image-lazy-load.php b/classes/suggested-tasks/providers/integrations/litespeed-cache/class-image-lazy-load.php new file mode 100644 index 000000000..e64622807 --- /dev/null +++ b/classes/suggested-tasks/providers/integrations/litespeed-cache/class-image-lazy-load.php @@ -0,0 +1,126 @@ +get_litespeed_option( 'media-lazy' ); + } + + /** + * Get the popover instructions. + * + * @return void + */ + public function print_popover_instructions() { + echo '

'; + \esc_html_e( 'Image lazy loading defers loading of images that are below the visible area of the page. Images are only loaded when the visitor scrolls near them. This significantly reduces initial page load time, especially on image-heavy pages.', 'progress-planner' ); + echo '

'; + } + + /** + * Print the popover input field for the form. + * + * @return void + */ + public function print_popover_form_contents() { + $this->print_submit_button( \__( 'Enable image lazy loading', 'progress-planner' ) ); + } + + /** + * Handle the interactive task submit. + * + * @return void + */ + public function handle_interactive_task_specific_submit() { + $this->verify_litespeed_cache_ajax_security(); + + $this->update_litespeed_option( 'media-lazy', 1 ); + + \wp_send_json_success( [ 'message' => \esc_html__( 'Setting updated.', 'progress-planner' ) ] ); + } + + /** + * Add task actions specific to this task. + * + * @param array $data The task data. + * @param array $actions The existing actions. + * + * @return array + */ + public function add_task_actions( $data = [], $actions = [] ) { + return $this->add_popover_action( $actions, \__( 'Enable', 'progress-planner' ) ); + } +} diff --git a/classes/suggested-tasks/providers/integrations/litespeed-cache/class-js-minification.php b/classes/suggested-tasks/providers/integrations/litespeed-cache/class-js-minification.php new file mode 100644 index 000000000..9861c9add --- /dev/null +++ b/classes/suggested-tasks/providers/integrations/litespeed-cache/class-js-minification.php @@ -0,0 +1,126 @@ +get_litespeed_option( 'optm-js_min' ); + } + + /** + * Get the popover instructions. + * + * @return void + */ + public function print_popover_instructions() { + echo '

'; + \esc_html_e( 'JavaScript minification strips unnecessary characters from your JS files without changing their functionality. This reduces file sizes and helps your pages load faster, especially on slower connections.', 'progress-planner' ); + echo '

'; + } + + /** + * Print the popover input field for the form. + * + * @return void + */ + public function print_popover_form_contents() { + $this->print_submit_button( \__( 'Enable JS minification', 'progress-planner' ) ); + } + + /** + * Handle the interactive task submit. + * + * @return void + */ + public function handle_interactive_task_specific_submit() { + $this->verify_litespeed_cache_ajax_security(); + + $this->update_litespeed_option( 'optm-js_min', 1 ); + + \wp_send_json_success( [ 'message' => \esc_html__( 'Setting updated.', 'progress-planner' ) ] ); + } + + /** + * Add task actions specific to this task. + * + * @param array $data The task data. + * @param array $actions The existing actions. + * + * @return array + */ + public function add_task_actions( $data = [], $actions = [] ) { + return $this->add_popover_action( $actions, \__( 'Enable', 'progress-planner' ) ); + } +} diff --git a/classes/suggested-tasks/providers/integrations/litespeed-cache/class-litespeed-cache-interactive-provider.php b/classes/suggested-tasks/providers/integrations/litespeed-cache/class-litespeed-cache-interactive-provider.php new file mode 100644 index 000000000..8e625a36a --- /dev/null +++ b/classes/suggested-tasks/providers/integrations/litespeed-cache/class-litespeed-cache-interactive-provider.php @@ -0,0 +1,56 @@ +get_litespeed_option( 'cache' ) !== 1; + } + + /** + * Get the popover instructions. + * + * @return void + */ + public function print_popover_instructions() { + echo '

'; + \esc_html_e( 'Page caching is the foundation of LiteSpeed Cache. It stores pre-built versions of your pages so they load instantly for visitors instead of being generated fresh each time. This is the single most impactful performance setting you can enable.', 'progress-planner' ); + echo '

'; + } + + /** + * Print the popover input field for the form. + * + * @return void + */ + public function print_popover_form_contents() { + $this->print_submit_button( \__( 'Enable page cache', 'progress-planner' ) ); + } + + /** + * Handle the interactive task submit. + * + * @return void + */ + public function handle_interactive_task_specific_submit() { + $this->verify_litespeed_cache_ajax_security(); + + $this->update_litespeed_option( 'cache', 1 ); + + \wp_send_json_success( [ 'message' => \esc_html__( 'Setting updated.', 'progress-planner' ) ] ); + } + + /** + * Add task actions specific to this task. + * + * @param array $data The task data. + * @param array $actions The existing actions. + * + * @return array + */ + public function add_task_actions( $data = [], $actions = [] ) { + return $this->add_popover_action( $actions, \__( 'Enable', 'progress-planner' ) ); + } +} diff --git a/classes/suggested-tasks/providers/traits/class-ajax-security-litespeed-cache.php b/classes/suggested-tasks/providers/traits/class-ajax-security-litespeed-cache.php new file mode 100644 index 000000000..daed8c7be --- /dev/null +++ b/classes/suggested-tasks/providers/traits/class-ajax-security-litespeed-cache.php @@ -0,0 +1,53 @@ + \esc_html__( 'LiteSpeed Cache is not active.', 'progress-planner' ) ] ); + } + } + + /** + * Perform complete LiteSpeed Cache AJAX security checks. + * + * Runs LiteSpeed Cache active check, capability check, and nonce verification. + * This is a convenience method for LiteSpeed Cache interactive tasks. + * + * @param string $capability The capability to require (default: 'manage_options'). + * @param string $action The nonce action to verify (default: 'progress_planner'). + * @param string $field The POST field containing the nonce (default: 'nonce'). + * + * @return void Exits with wp_send_json_error() if any check fails. + */ + protected function verify_litespeed_cache_ajax_security( $capability = 'manage_options', $action = 'progress_planner', $field = 'nonce' ) { + $this->verify_litespeed_cache_active_or_fail(); + $this->verify_ajax_security( $capability, $action, $field ); + } +} From 3208f5232e6e278936588d7894b81a54cac50968 Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Wed, 26 Aug 2026 13:45:06 +0200 Subject: [PATCH 2/2] Save LiteSpeed settings through LiteSpeed's own Conf class update_litespeed_option() wrote the option row directly. Saving a setting in LiteSpeed is more than a database write: Conf::update_confs() casts the value to the option's declared type, purges the caches the change invalidates, updates the relevant cron, and refreshes LiteSpeed's in-memory config. Guest mode is the clearest case. conf.cls.php resets the crawler's disabled list when `guest` changes, and one of this PR's six tasks writes exactly that key, so the reset was being skipped. Verified against LiteSpeed Cache 7.9. Enabling guest mode through the provider now prints "Crawler disabled list is cleared!" and the in-memory config agrees with the stored value. With the old direct write, no reset fired and Conf::conf('guest') still reported false while the option row said 1 -- the running request saw a different value than the database. Falls back to update_option() when LiteSpeed's class is absent, so this keeps working if the plugin's internals move. Adds tests with a stub standing in for \LiteSpeed\Conf, since the plugin is not installed in CI: that updates are handed to Conf::update_confs(), that a value LiteSpeed did not store is reported as failure rather than assumed, that each provider passes its own option key, and that should_add_task() follows the setting including a cast string value. Verified by mutation -- restoring the direct write fails three of them. --- ...s-litespeed-cache-interactive-provider.php | 28 ++++ .../stubs/class-litespeed-conf-stub.php | 94 +++++++++++ tests/phpunit/test-class-litespeed-cache.php | 158 ++++++++++++++++++ 3 files changed, 280 insertions(+) create mode 100644 tests/phpunit/stubs/class-litespeed-conf-stub.php create mode 100644 tests/phpunit/test-class-litespeed-cache.php diff --git a/classes/suggested-tasks/providers/integrations/litespeed-cache/class-litespeed-cache-interactive-provider.php b/classes/suggested-tasks/providers/integrations/litespeed-cache/class-litespeed-cache-interactive-provider.php index 8e625a36a..f29d2ea2b 100644 --- a/classes/suggested-tasks/providers/integrations/litespeed-cache/class-litespeed-cache-interactive-provider.php +++ b/classes/suggested-tasks/providers/integrations/litespeed-cache/class-litespeed-cache-interactive-provider.php @@ -45,12 +45,40 @@ protected function get_litespeed_option( $option_key ) { /** * Update a LiteSpeed Cache option value. * + * Goes through LiteSpeed's own Conf::update_confs() rather than writing the + * option row directly, because saving a setting is more than a database + * write there: it casts the value to the option's declared type, purges the + * caches the change invalidates, updates the relevant cron, and refreshes + * LiteSpeed's in-memory config so the rest of the request sees the new + * value. + * + * Guest mode is the clearest example -- Conf::update_confs() clears the + * crawler's disabled list when `guest` changes, so writing the row on its + * own leaves that list stale. + * + * Falls back to a direct write when LiteSpeed's class is unavailable, which + * keeps this working if the plugin's internals move. + * * @param string $option_key The LiteSpeed option key (e.g., 'cache', 'cache-browser'). * @param mixed $value The value to set. * * @return bool Whether the option was updated. */ protected function update_litespeed_option( $option_key, $value ) { + $conf_class = '\LiteSpeed\Conf'; + + // @phpstan-ignore-next-line function.impossibleType -- LiteSpeed is not a dependency, so PHPStan cannot see the class; class_exists() guards it at runtime. + if ( \class_exists( $conf_class ) && \method_exists( $conf_class, 'cls' ) ) { + $conf = $conf_class::cls(); + + if ( \is_object( $conf ) && \method_exists( $conf, 'update_confs' ) ) { + $conf->update_confs( [ $option_key => $value ] ); + + // update_confs() returns nothing, so report on the stored value. + return (int) $this->get_litespeed_option( $option_key ) === (int) $value; + } + } + return \update_option( 'litespeed.conf.' . $option_key, $value ); } } diff --git a/tests/phpunit/stubs/class-litespeed-conf-stub.php b/tests/phpunit/stubs/class-litespeed-conf-stub.php new file mode 100644 index 000000000..0d59d1398 --- /dev/null +++ b/tests/phpunit/stubs/class-litespeed-conf-stub.php @@ -0,0 +1,94 @@ + value pairs. + * + * @return void + */ + public function update_confs( $the_matrix = [] ) { + self::$calls[] = $the_matrix; + + if ( ! self::$should_store ) { + return; + } + + foreach ( $the_matrix as $key => $value ) { + \update_option( 'litespeed.conf.' . $key, $value ); + } + } + + /** + * Get the recorded calls. + * + * @return array[] + */ + public static function get_calls() { + return self::$calls; + } + + /** + * Forget the recorded calls. + * + * @return void + */ + public static function reset_calls() { + self::$calls = []; + } + + /** + * Set whether the stub stores values. + * + * @param bool $should_store Whether to store. + * + * @return void + */ + public static function set_should_store( $should_store ) { + self::$should_store = (bool) $should_store; + } + } +} diff --git a/tests/phpunit/test-class-litespeed-cache.php b/tests/phpunit/test-class-litespeed-cache.php new file mode 100644 index 000000000..bead3e7c7 --- /dev/null +++ b/tests/phpunit/test-class-litespeed-cache.php @@ -0,0 +1,158 @@ +getMethod( 'update_litespeed_option' ); + $method->setAccessible( true ); + + return $method->invoke( $provider, $option_key, $value ); + } + + /** + * With LiteSpeed present, the update goes through its own Conf class. + * + * Writing the option row directly would leave LiteSpeed's caches unpurged + * and its in-memory config stale -- for `guest` it also skips the crawler + * disabled-list reset that Conf::update_confs() performs. + * + * @return void + */ + public function test_update_routes_through_litespeed_conf() { + require_once __DIR__ . '/stubs/class-litespeed-conf-stub.php'; + + $result = $this->update_option_via( new Guest_Mode(), 'guest', 1 ); + + $this->assertSame( + [ [ 'guest' => 1 ] ], + \LiteSpeed\Conf::get_calls(), + 'The value should have been handed to Conf::update_confs().' + ); + $this->assertTrue( $result, 'The update should report success.' ); + } + + /** + * The provider reports failure when LiteSpeed did not store the value. + * + * Conf::update_confs() returns nothing, so success is read back from the + * stored option rather than assumed. + * + * @return void + */ + public function test_update_reports_failure_when_value_is_not_stored() { + require_once __DIR__ . '/stubs/class-litespeed-conf-stub.php'; + + \LiteSpeed\Conf::set_should_store( false ); + + $result = $this->update_option_via( new Guest_Mode(), 'guest', 1 ); + + $this->assertFalse( $result, 'A value LiteSpeed refused to store is not a success.' ); + + \LiteSpeed\Conf::set_should_store( true ); + } + + /** + * Each provider hands LiteSpeed its own option key. + * + * @return void + */ + public function test_each_provider_updates_its_own_option() { + require_once __DIR__ . '/stubs/class-litespeed-conf-stub.php'; + + $this->update_option_via( new Page_Cache(), 'cache', 1 ); + + $this->assertSame( [ [ 'cache' => 1 ] ], \LiteSpeed\Conf::get_calls() ); + } + + /** + * The task is offered while the setting is off, and not once it is on. + * + * @return void + */ + public function test_should_add_task_follows_the_setting() { + if ( ! \defined( 'LSCWP_V' ) ) { + \define( 'LSCWP_V', '7.9' ); + } + + $provider = new Guest_Mode(); + + \update_option( 'litespeed.conf.guest', 0 ); + $this->assertTrue( $provider->should_add_task(), 'A disabled setting should be suggested.' ); + + \update_option( 'litespeed.conf.guest', 1 ); + $this->assertFalse( $provider->should_add_task(), 'An enabled setting should not be suggested.' ); + } + + /** + * A string "1" counts as enabled. + * + * LiteSpeed casts values on save, so the stored option is not necessarily + * the integer the provider wrote. + * + * @return void + */ + public function test_should_add_task_treats_string_one_as_enabled() { + if ( ! \defined( 'LSCWP_V' ) ) { + \define( 'LSCWP_V', '7.9' ); + } + + \update_option( 'litespeed.conf.guest', '1' ); + + $this->assertFalse( + ( new Guest_Mode() )->should_add_task(), + 'A string "1" is still enabled.' + ); + } +}