diff --git a/integration/class-litespeed-cache.php b/integration/class-litespeed-cache.php new file mode 100644 index 000000000..c154b1b8b --- /dev/null +++ b/integration/class-litespeed-cache.php @@ -0,0 +1,186 @@ + +RewriteEngine On +RewriteCond %{HTTP:Accept} application +RewriteRule ^ - [E=Cache-Control:vary=%{ENV:LSCACHE_VARY_VALUE}+isjson] +'; + + /** + * The option name to store the htaccess rules. + * + * @var string + */ + public static $option_name = 'activitypub_litespeed_cache_setup'; + + /** + * The marker to identify the rules in the htaccess file. + * + * @var string + */ + public static $marker = 'ActivityPub LiteSpeed Cache'; + + /** + * Initialize the integration. + */ + public static function init() { + \add_action( 'activate_litespeed-cache/litespeed-cache.php', array( self::class, 'add_htaccess_rules' ) ); + \add_action( 'deactivate_litespeed-cache/litespeed-cache.php', array( self::class, 'remove_htaccess_rules' ) ); + + \add_filter( 'site_status_tests', array( self::class, 'maybe_add_site_health' ) ); + } + + /** + * Add the Litespeed Cache htaccess rules. + */ + public static function add_htaccess_rules() { + $added_rules = self::append_with_markers( self::$marker, self::$rules ); + + \update_option( self::$option_name, $added_rules ); + } + + /** + * Remove the Litespeed Cache htaccess rules. + */ + public static function remove_htaccess_rules() { + self::append_with_markers( self::$marker, '' ); + + \delete_option( self::$option_name ); + } + + /** + * Maybe add the Litespeed Cache config to the site health. + * + * @param array $tests The site health tests. + * + * @return array The site health tests with the Litespeed Cache config test. + */ + public static function maybe_add_site_health( $tests ) { + if ( ! \is_plugin_active( 'litespeed-cache/litespeed-cache.php' ) ) { + return $tests; + } + + $tests['direct']['activitypub_test_litespeed_cache_integration'] = array( + 'label' => \__( 'Litespeed Cache Test', 'activitypub' ), + 'test' => array( self::class, 'test_litespeed_cache_integration' ), + ); + + return $tests; + } + + /** + * Test the Litespeed Cache integration. + * + * @return array The test results. + */ + public static function test_litespeed_cache_integration() { + $result = array( + 'label' => \__( 'Compatibility with Litespeed Cache', 'activitypub' ), + 'status' => 'good', + 'badge' => array( + 'label' => \__( 'ActivityPub', 'activitypub' ), + 'color' => 'green', + ), + 'description' => \sprintf( + '

%s

', + \__( 'Litespeed Cache is well configured to work with ActivityPub.', 'activitypub' ) + ), + 'actions' => '', + 'test' => 'test_litespeed_cache_integration', + ); + + if ( '0' === \get_option( self::$option_name, '0' ) ) { + $result['status'] = 'critical'; + $result['label'] = \__( 'Litespeed Cache might not be properly configured.', 'activitypub' ); + $result['badge']['color'] = 'red'; + $result['description'] = \sprintf( + '

%s

', + \__( 'Litespeed Cache isn’t currently set up to work with ActivityPub. While this isn’t a major problem, it’s a good idea to enable support. Without it, some technical files (like JSON) might accidentally show up in your website’s cache and be visible to visitors.', 'activitypub' ) + ); + $result['actions'] = \sprintf( + '

%s

%s
', + \__( 'To enable the ActivityPub integration with Litespeed Cache, add the following rules to your .htaccess file:', 'activitypub' ), + \esc_html( self::$rules ) + ); + } + + return $result; + } + + /** + * Append rules to a file with markers. + * + * @param string $marker The marker to identify the rules in the file. + * @param string $rules The rules to append. + * + * @return bool True on success, false on failure. + */ + public static function append_with_markers( $marker, $rules ) { + $htaccess_file = self::get_htaccess_file_path(); + + if ( ! \wp_is_writable( $htaccess_file ) ) { + return false; + } + + // Ensure get_home_path() is declared. + require_once ABSPATH . 'wp-admin/includes/file.php'; + + global $wp_filesystem; + \WP_Filesystem(); + + $htaccess = $wp_filesystem->get_contents( $htaccess_file ); + + if ( strpos( $htaccess, $marker ) !== false ) { + return \insert_with_markers( $htaccess_file, $marker, $rules ); + } + + $start_marker = "# BEGIN {$marker}"; + $end_marker = "# END {$marker}"; + + $rules = $start_marker . PHP_EOL . $rules . PHP_EOL . $end_marker; + $htaccess = $rules . PHP_EOL . PHP_EOL . $htaccess; + + return $wp_filesystem->put_contents( $htaccess_file, $htaccess, FS_CHMOD_FILE ); + } + + /** + * Get the htaccess file. + * + * @return string|false The htaccess file or false. + */ + public static function get_htaccess_file_path() { + $htaccess_file = false; + + // phpcs:ignore WordPress.PHP.NoSilencedErrors + if ( @file_exists( \get_home_path() . '.htaccess' ) ) { + /** The htaccess file resides in ABSPATH */ + $htaccess_file = \get_home_path() . '.htaccess'; + } + + /** + * Filter the htaccess file path. + * + * @param string|false $htaccess_file The htaccess file path. + */ + return \apply_filters( 'activitypub_litespeed_cache_htaccess_file', $htaccess_file ); + } +} diff --git a/integration/load.php b/integration/load.php index 78b0dd6b9..03a408cb7 100644 --- a/integration/load.php +++ b/integration/load.php @@ -135,6 +135,15 @@ function ( $transformer, $data, $object_class ) { * @see https://wordpress.org/plugins/surge/ */ Surge::init(); + + /** + * Load the LiteSpeed Cache integration. + * + * Only load code that needs LiteSpeed Cache to run once LiteSpeed Cache is loaded and initialized. + * + * @see https://wordpress.org/plugins/litespeed-cache/ + */ + Litespeed_Cache::init(); } \add_action( 'plugins_loaded', __NAMESPACE__ . '\plugin_init' ); @@ -142,6 +151,10 @@ function ( $transformer, $data, $object_class ) { \register_activation_hook( ACTIVITYPUB_PLUGIN_FILE, array( __NAMESPACE__ . '\Surge', 'add_cache_config' ) ); \register_deactivation_hook( ACTIVITYPUB_PLUGIN_FILE, array( __NAMESPACE__ . '\Surge', 'remove_cache_config' ) ); +// Register activation and deactivation hooks for Litespeed Cache integration. +\register_activation_hook( ACTIVITYPUB_PLUGIN_FILE, array( __NAMESPACE__ . '\Litespeed_Cache', 'add_htaccess_rules' ) ); +\register_deactivation_hook( ACTIVITYPUB_PLUGIN_FILE, array( __NAMESPACE__ . '\Litespeed_Cache', 'remove_htaccess_rules' ) ); + /** * Register the Stream Connector for ActivityPub. diff --git a/tests/integration/class-test-litespeed-cache.php b/tests/integration/class-test-litespeed-cache.php new file mode 100644 index 000000000..7c0ee4256 --- /dev/null +++ b/tests/integration/class-test-litespeed-cache.php @@ -0,0 +1,123 @@ +htaccess_file = \sys_get_temp_dir() . '/.htaccess-test'; + $this->original_htaccess = "# BEGIN WordPress\n# END WordPress"; + // phpcs:ignore + \file_put_contents( $this->htaccess_file, $this->original_htaccess ); + // Patch get_home_path to use our temp dir. + \add_filter( 'activitypub_litespeed_cache_home_path', array( $this, 'get_home_path' ) ); + } + + /** + * Tear down the test environment. + */ + public function tear_down() { + parent::tear_down(); + if ( \file_exists( $this->htaccess_file ) ) { + \wp_delete_file( $this->htaccess_file ); + } + \remove_all_filters( 'activitypub_litespeed_cache_home_path' ); + } + + /** + * Get the home path for the test environment. + * + * @return string The home path. + */ + public function get_home_path() { + return \dirname( $this->htaccess_file ) . '/'; + } + + /** + * Test adding htaccess rules. + */ + public function test_add_htaccess_rules() { + $function = function () { + return $this->htaccess_file; + }; + \add_filter( 'activitypub_litespeed_cache_htaccess_file', $function ); + + Litespeed_Cache::add_htaccess_rules(); + // phpcs:ignore + $contents = \file_get_contents( $this->htaccess_file ); + $this->assertStringContainsString( Litespeed_Cache::$rules, $contents, 'Litespeed rules should be present in htaccess' ); + + \remove_filter( 'activitypub_litespeed_cache_htaccess_file', $function ); + } + + /** + * Test removing htaccess rules. + */ + public function test_remove_htaccess_rules() { + // First add, then remove. + Litespeed_Cache::add_htaccess_rules(); + Litespeed_Cache::remove_htaccess_rules(); + // phpcs:ignore + $contents = \file_get_contents( $this->htaccess_file ); + $this->assertStringNotContainsString( Litespeed_Cache::$rules, $contents, 'Litespeed rules should be removed from htaccess' ); + } + + /** + * Test no duplicate rules. + */ + public function test_no_duplicate_rules() { + $function = function () { + return $this->htaccess_file; + }; + \add_filter( 'activitypub_litespeed_cache_htaccess_file', $function ); + + Litespeed_Cache::add_htaccess_rules(); + Litespeed_Cache::add_htaccess_rules(); + // phpcs:ignore + $contents = \file_get_contents( $this->htaccess_file ); + // Count number of rule blocks. + $rule_count = substr_count( $contents, Litespeed_Cache::$rules ); + $this->assertEquals( 1, $rule_count, 'Litespeed rules should appear only once' ); + + \remove_filter( 'activitypub_litespeed_cache_htaccess_file', $function ); + } + + /** + * Test that the option is updated when rules are added. + * + * @return void + */ + public function test_option_updated_on_add() { + Litespeed_Cache::add_htaccess_rules(); + $option = \get_option( Litespeed_Cache::$option_name ); + $this->assertTrue( $option, 'Option should be updated to true after adding rules' ); + } +}