diff --git a/projects/packages/waf/changelog/enhance-waf-error-handling b/projects/packages/waf/changelog/enhance-waf-error-handling new file mode 100644 index 0000000000000..e16536b09fe72 --- /dev/null +++ b/projects/packages/waf/changelog/enhance-waf-error-handling @@ -0,0 +1,5 @@ +Significance: patch +Type: changed +Comment: Enhance error handling behind the scenes. + + diff --git a/projects/packages/waf/src/class-rest-controller.php b/projects/packages/waf/src/class-rest-controller.php index 15fbce7c9c2b2..38e4d5615f463 100644 --- a/projects/packages/waf/src/class-rest-controller.php +++ b/projects/packages/waf/src/class-rest-controller.php @@ -17,6 +17,8 @@ class REST_Controller { /** * Register REST API endpoints. + * + * @return void */ public static function register_rest_routes() { register_rest_route( @@ -52,29 +54,29 @@ public static function register_rest_routes() { /** * Update rules endpoint + * + * @return WP_REST_Response|WP_Error */ public static function update_rules() { - $success = true; - $message = 'Rules updated succesfully'; - try { Waf_Rules_Manager::generate_automatic_rules(); Waf_Rules_Manager::generate_rules(); - } catch ( \Exception $e ) { - $success = false; - $message = $e->getMessage(); + } catch ( Waf_Exception $e ) { + return $e->get_wp_error(); } return rest_ensure_response( array( - 'success' => $success, - 'message' => $message, + 'success' => true, + 'message' => __( 'Rules updated succesfully', 'jetpack-waf' ), ) ); } /** * WAF Endpoint + * + * @return WP_REST_Response */ public static function waf() { return rest_ensure_response( Waf_Runner::get_config() ); @@ -84,7 +86,8 @@ public static function waf() { * Update WAF Endpoint * * @param WP_REST_Request $request The API request. - * @return WP_REST_Response + * + * @return WP_REST_Response|WP_Error */ public static function update_waf( $request ) { // Automatic Rules Enabled @@ -112,7 +115,11 @@ public static function update_waf( $request ) { update_option( Waf_Runner::SHARE_DATA_OPTION_NAME, (bool) $request[ Waf_Runner::SHARE_DATA_OPTION_NAME ] ); } - Waf_Runner::update_waf(); + try { + Waf_Runner::update_waf(); + } catch ( Waf_Exception $e ) { + return $e->get_wp_error(); + } return self::waf(); } diff --git a/projects/packages/waf/src/class-waf-initializer.php b/projects/packages/waf/src/class-waf-initializer.php index 3f3866506efe7..a119c9be7a41c 100644 --- a/projects/packages/waf/src/class-waf-initializer.php +++ b/projects/packages/waf/src/class-waf-initializer.php @@ -49,33 +49,37 @@ public static function init() { } /** - * On module activation set up waf mode + * Activate the WAF on module activation. * - * @return bool|WP_Error True of the WAF activation was successful, WP_Error otherwise. + * @return bool|WP_Error True if the WAF activation is successful, WP_Error otherwise. */ public static function on_activation() { update_option( Waf_Runner::MODE_OPTION_NAME, 'normal' ); add_option( Waf_Rules_Manager::AUTOMATIC_RULES_ENABLED_OPTION_NAME, false ); - $waf_activated = Waf_Runner::activate(); - if ( is_wp_error( $waf_activated ) ) { - return $waf_activated; - } - try { + Waf_Runner::activate(); ( new Waf_Standalone_Bootstrap() )->generate(); - } catch ( \Exception $e ) { - return new WP_Error( 'waf_activation_failed', $e->getMessage() ); + } catch ( Waf_Exception $e ) { + return $e->get_wp_error(); } return true; } /** - * On module deactivation, unset waf mode + * Deactivate the WAF on module deactivation. + * + * @return bool|WP_Error True if the WAF deactivation is successful, WP_Error otherwise. */ public static function on_deactivation() { - Waf_Runner::deactivate(); + try { + Waf_Runner::deactivate(); + } catch ( Waf_Exception $e ) { + return $e->get_wp_error(); + } + + return true; } /** @@ -131,23 +135,22 @@ public static function check_for_waf_update() { if ( ! method_exists( Waf_Constants::class, 'define_mode' ) ) { try { ( new Waf_Standalone_Bootstrap() )->generate(); - } catch ( \Exception $e ) { - return new WP_Error( 'waf_update_failed', $e->getMessage() ); + } catch ( Waf_Exception $e ) { + return $e->get_wp_error(); } - return true; } Waf_Constants::define_mode(); if ( ! Waf_Runner::is_allowed_mode( JETPACK_WAF_MODE ) ) { - return new WP_Error( 'waf_update_failed', 'Invalid firewall mode.' ); + return new WP_Error( 'waf_mode_invalid', 'Invalid firewall mode.' ); } try { Waf_Rules_Manager::generate_ip_rules(); Waf_Rules_Manager::generate_rules(); ( new Waf_Standalone_Bootstrap() )->generate(); - } catch ( \Exception $e ) { - return new WP_Error( 'waf_update_failed', $e->getMessage() ); + } catch ( Waf_Exception $e ) { + return $e->get_wp_error(); } } diff --git a/projects/packages/waf/src/class-waf-rules-manager.php b/projects/packages/waf/src/class-waf-rules-manager.php index 05fc2bfe1fe21..2c9f890179bf8 100644 --- a/projects/packages/waf/src/class-waf-rules-manager.php +++ b/projects/packages/waf/src/class-waf-rules-manager.php @@ -43,14 +43,14 @@ class Waf_Rules_Manager { */ public static function add_hooks() { // Re-activate the WAF any time an option is added or updated. - add_action( 'add_option_' . self::AUTOMATIC_RULES_ENABLED_OPTION_NAME, array( Waf_Runner::class, 'activate' ), 10, 0 ); - add_action( 'update_option_' . self::AUTOMATIC_RULES_ENABLED_OPTION_NAME, array( Waf_Runner::class, 'activate' ), 10, 0 ); - add_action( 'add_option_' . self::IP_LISTS_ENABLED_OPTION_NAME, array( Waf_Runner::class, 'activate' ), 10, 0 ); - add_action( 'update_option_' . self::IP_LISTS_ENABLED_OPTION_NAME, array( Waf_Runner::class, 'activate' ), 10, 0 ); - add_action( 'add_option_' . self::IP_ALLOW_LIST_OPTION_NAME, array( Waf_Runner::class, 'activate' ), 10, 0 ); - add_action( 'update_option_' . self::IP_ALLOW_LIST_OPTION_NAME, array( Waf_Runner::class, 'activate' ), 10, 0 ); - add_action( 'add_option_' . self::IP_BLOCK_LIST_OPTION_NAME, array( Waf_Runner::class, 'activate' ), 10, 0 ); - add_action( 'update_option_' . self::IP_BLOCK_LIST_OPTION_NAME, array( Waf_Runner::class, 'activate' ), 10, 0 ); + add_action( 'add_option_' . self::AUTOMATIC_RULES_ENABLED_OPTION_NAME, array( static::class, 'reactivate_on_rules_option_change' ), 10, 0 ); + add_action( 'update_option_' . self::AUTOMATIC_RULES_ENABLED_OPTION_NAME, array( static::class, 'reactivate_on_rules_option_change' ), 10, 0 ); + add_action( 'add_option_' . self::IP_LISTS_ENABLED_OPTION_NAME, array( static::class, 'reactivate_on_rules_option_change' ), 10, 0 ); + add_action( 'update_option_' . self::IP_LISTS_ENABLED_OPTION_NAME, array( static::class, 'reactivate_on_rules_option_change' ), 10, 0 ); + add_action( 'add_option_' . self::IP_ALLOW_LIST_OPTION_NAME, array( static::class, 'reactivate_on_rules_option_change' ), 10, 0 ); + add_action( 'update_option_' . self::IP_ALLOW_LIST_OPTION_NAME, array( static::class, 'reactivate_on_rules_option_change' ), 10, 0 ); + add_action( 'add_option_' . self::IP_BLOCK_LIST_OPTION_NAME, array( static::class, 'reactivate_on_rules_option_change' ), 10, 0 ); + add_action( 'update_option_' . self::IP_BLOCK_LIST_OPTION_NAME, array( static::class, 'reactivate_on_rules_option_change' ), 10, 0 ); // Register the cron job. add_action( 'jetpack_waf_rules_update_cron', array( static::class, 'update_rules_cron' ) ); } @@ -58,12 +58,14 @@ public static function add_hooks() { /** * Schedule the cron job to update the WAF rules. * - * @return void + * @return bool|WP_Error True if the event is scheduled, WP_Error on failure. */ public static function schedule_rules_cron() { if ( ! wp_next_scheduled( 'jetpack_waf_rules_update_cron' ) ) { - wp_schedule_event( time(), 'twicedaily', 'jetpack_waf_rules_update_cron' ); + return wp_schedule_event( time(), 'twicedaily', 'jetpack_waf_rules_update_cron', array(), true ); } + + return true; } /** @@ -74,59 +76,72 @@ public static function schedule_rules_cron() { public static function update_rules_cron() { Waf_Constants::define_mode(); if ( ! Waf_Runner::is_allowed_mode( JETPACK_WAF_MODE ) ) { - return new WP_Error( 'waf_cron_update_failed', 'Invalid firewall mode.' ); + return new WP_Error( 'waf_invalid_mode', 'Invalid firewall mode.' ); } try { self::generate_automatic_rules(); self::generate_ip_rules(); self::generate_rules(); - } catch ( \Exception $e ) { - return new WP_Error( 'waf_cron_update_failed', $e->getMessage() ); + } catch ( Waf_Exception $e ) { + return $e->get_wp_error(); } update_option( self::RULE_LAST_UPDATED_OPTION_NAME, time() ); return true; } + /** + * Re-activate the WAF any time an option is added or updated. + * + * @return bool|WP_Error True if re-activation is successful, WP_Error on failure. + */ + public static function reactivate_on_rules_option_change() { + try { + Waf_Runner::activate(); + } catch ( Waf_Exception $e ) { + return $e->get_wp_error(); + } + + return true; + } + /** * Updates the rule set if rules version has changed * - * @return bool|WP_Error True if rules update is successful, WP_Error on failure. + * @throws Waf_Exception If the firewall mode is invalid. + * @throws Waf_Exception If the rules update fails. + * + * @return void */ public static function update_rules_if_changed() { Waf_Constants::define_mode(); if ( ! Waf_Runner::is_allowed_mode( JETPACK_WAF_MODE ) ) { - return new WP_Error( 'waf_update_failed', 'Invalid firewall mode.' ); + throw new Waf_Exception( 'Invalid firewall mode.' ); } $version = get_option( self::VERSION_OPTION_NAME ); if ( self::RULES_VERSION !== $version ) { - update_option( self::VERSION_OPTION_NAME, self::RULES_VERSION ); + self::generate_automatic_rules(); + self::generate_ip_rules(); + self::generate_rules(); - try { - self::generate_automatic_rules(); - self::generate_ip_rules(); - self::generate_rules(); - } catch ( \Exception $e ) { - return new WP_Error( 'waf_update_failed', $e->getMessage() ); - } + update_option( self::VERSION_OPTION_NAME, self::RULES_VERSION ); } - - return true; } /** * Retrieve rules from the API * - * @throws \Exception If site is not registered. - * @throws \Exception If API did not respond 200. - * @throws \Exception If data is missing from response. + * @throws Waf_Exception If site is not registered. + * @throws Rules_API_Exception If API did not respond 200. + * @throws Rules_API_Exception If data is missing from response. + * * @return array */ public static function get_rules_from_api() { $blog_id = Jetpack_Options::get_option( 'id' ); if ( ! $blog_id ) { - throw new \Exception( 'Site is not registered' ); + throw new Waf_Exception( 'Site is not registered' ); } $response = Client::wpcom_json_api_request_as_blog( @@ -140,14 +155,14 @@ public static function get_rules_from_api() { $response_code = wp_remote_retrieve_response_code( $response ); if ( 200 !== $response_code ) { - throw new \Exception( 'API connection failed.', (int) $response_code ); + throw new Rules_API_Exception( 'API connection failed.', (int) $response_code ); } $rules_json = wp_remote_retrieve_body( $response ); $rules = json_decode( $rules_json, true ); if ( empty( $rules['data'] ) ) { - throw new \Exception( 'Data missing from response.' ); + throw new Rules_API_Exception( 'Data missing from response.' ); } return $rules['data']; @@ -158,6 +173,7 @@ public static function get_rules_from_api() { * * @param string $required_file The file to check if exists and require. * @param string $return_code The PHP code to execute if the file require returns true. Defaults to 'return;'. + * * @return string The wrapped require statement. */ private static function wrap_require( $required_file, $return_code = 'return;' ) { @@ -167,17 +183,15 @@ private static function wrap_require( $required_file, $return_code = 'return;' ) /** * Generates the rules.php script * - * @throws \Exception If file writing fails. + * @global \WP_Filesystem_Base $wp_filesystem WordPress filesystem abstraction. + * + * @throws File_System_Exception If file writing fails initializing rule files. + * @throws File_System_Exception If file writing fails writing to the rules entrypoint file. + * * @return void */ public static function generate_rules() { - /** - * WordPress filesystem abstraction. - * - * @var \WP_Filesystem_Base $wp_filesystem - */ global $wp_filesystem; - Waf_Runner::initialize_filesystem(); $rules = "is_file( $rule_file ) ) { if ( ! $wp_filesystem->put_contents( $rule_file, "put_contents( $entrypoint_file_path, $rules ) ) { - throw new \Exception( 'Failed writing rules file to: ' . $entrypoint_file_path ); + throw new File_System_Exception( 'Failed writing rules file to: ' . $entrypoint_file_path ); } } /** * Generates the automatic-rules.php script * - * @throws \Exception If rules cannot be generated and saved. + * @global \WP_Filesystem_Base $wp_filesystem WordPress filesystem abstraction. + * + * @throws Waf_Exception If rules cannot be fetched from the API. + * @throws File_System_Exception If file writing fails. + * * @return void */ public static function generate_automatic_rules() { - /** - * WordPress filesystem abstraction. - * - * @var \WP_Filesystem_Base $wp_filesystem - */ global $wp_filesystem; - Waf_Runner::initialize_filesystem(); $automatic_rules_file_path = Waf_Runner::get_waf_file_path( self::AUTOMATIC_RULES_FILE ); @@ -241,10 +253,10 @@ public static function generate_automatic_rules() { try { $rules = self::get_rules_from_api(); - } catch ( \Exception $exception ) { + } catch ( Waf_Exception $e ) { // Do not throw API exceptions for users who do not have access - if ( 401 !== $exception->getCode() ) { - throw $exception; + if ( 401 !== $e->getCode() ) { + throw $e; } } @@ -254,7 +266,7 @@ public static function generate_automatic_rules() { } if ( ! $wp_filesystem->put_contents( $automatic_rules_file_path, $rules ) ) { - throw new \Exception( 'Failed writing automatic rules file to: ' . $automatic_rules_file_path ); + throw new File_System_Exception( 'Failed writing automatic rules file to: ' . $automatic_rules_file_path ); } update_option( self::AUTOMATIC_RULES_LAST_UPDATED_OPTION_NAME, time() ); @@ -263,18 +275,15 @@ public static function generate_automatic_rules() { /** * Generates the rules.php script * - * @throws \Exception If filesystem is not available. - * @throws \Exception If file writing fails. + * @global \WP_Filesystem_Base $wp_filesystem WordPress filesystem abstraction. + * + * @throws File_System_Exception If writing to IP allow list file fails. + * @throws File_System_Exception If writing to IP block list file fails. + * * @return void */ public static function generate_ip_rules() { - /** - * WordPress filesystem abstraction. - * - * @var \WP_Filesystem_Base $wp_filesystem - */ global $wp_filesystem; - Waf_Runner::initialize_filesystem(); $allow_ip_file_path = Waf_Runner::get_waf_file_path( self::IP_ALLOW_RULES_FILE ); @@ -298,7 +307,7 @@ public static function generate_ip_rules() { $allow_rules_content .= 'return $waf->is_ip_in_array( $waf_allow_list );' . "\n"; if ( ! $wp_filesystem->put_contents( $allow_ip_file_path, "is_ip_in_array( $waf_block_list );' . "\n"; if ( ! $wp_filesystem->put_contents( $block_ip_file_path, "activate( self::WAF_MODULE_NAME, false, false ); @@ -136,6 +139,8 @@ public static function enable() { /** * Disabled the WAF module on the site. + * + * @return bool */ public static function disable() { return ( new Modules() )->deactivate( self::WAF_MODULE_NAME ); @@ -227,7 +232,7 @@ public static function run() { // phpcs:ignore include $rules_file_path; } -} catch ( \Exception $err ) { // phpcs:ignore + } catch ( \Exception $err ) { // phpcs:ignore // Intentionally doing nothing. } @@ -253,8 +258,9 @@ public static function errorHandler( $code, $message, $file, $line ) { // phpcs: /** * Initializes the WP filesystem and WAF directory structure. * + * @throws File_System_Exception If filesystem is unavailable. + * * @return void - * @throws \Exception If filesystem is unavailable. */ public static function initialize_filesystem() { if ( ! function_exists( '\\WP_Filesystem' ) ) { @@ -262,7 +268,7 @@ public static function initialize_filesystem() { } if ( ! \WP_Filesystem() ) { - throw new \Exception( 'No filesystem available.' ); + throw new File_System_Exception( 'No filesystem available.' ); } self::initialize_waf_directory(); @@ -271,12 +277,15 @@ public static function initialize_filesystem() { /** * Activates the WAF by generating the rules script and setting the version * - * @return bool|WP_Error True if the WAF was activated sucessfully, WP_Error if not. + * @throws Waf_Exception If the firewall mode is invalid. + * @throws Waf_Exception If the activation fails. + * + * @return void */ public static function activate() { Waf_Constants::define_mode(); if ( ! self::is_allowed_mode( JETPACK_WAF_MODE ) ) { - new WP_Error( 'waf_activation_failed', 'Invalid firewall mode.' ); + throw new Waf_Exception( 'Invalid firewall mode.' ); } $version = get_option( Waf_Rules_Manager::VERSION_OPTION_NAME ); @@ -286,24 +295,22 @@ public static function activate() { add_option( self::SHARE_DATA_OPTION_NAME, true ); - try { - self::initialize_filesystem(); - Waf_Rules_Manager::generate_automatic_rules(); - Waf_Rules_Manager::generate_ip_rules(); - self::create_blocklog_table(); - Waf_Rules_Manager::generate_rules(); - } catch ( \Exception $e ) { - return new WP_Error( 'waf_activation_failed', $e->getMessage() ); - } + self::initialize_filesystem(); - return true; + Waf_Rules_Manager::generate_automatic_rules(); + Waf_Rules_Manager::generate_ip_rules(); + Waf_Rules_Manager::generate_rules(); + + self::create_blocklog_table(); } /** * Ensures that the waf directory is created. * + * @throws File_System_Exception If filesystem is unavailable. + * @throws File_System_Exception If creating the directory fails. + * * @return void - * @throws \Exception In case there's a problem when creating the directory. */ public static function initialize_waf_directory() { WP_Filesystem(); @@ -311,12 +318,12 @@ public static function initialize_waf_directory() { global $wp_filesystem; if ( ! $wp_filesystem ) { - throw new \Exception( 'Can not work without the file system being initialized.' ); + throw new File_System_Exception( 'Can not work without the file system being initialized.' ); } if ( ! $wp_filesystem->is_dir( JETPACK_WAF_DIR ) ) { if ( ! $wp_filesystem->mkdir( JETPACK_WAF_DIR ) ) { - throw new \Exception( 'Failed creating WAF file directory: ' . JETPACK_WAF_DIR ); + throw new File_System_Exception( 'Failed creating WAF file directory: ' . JETPACK_WAF_DIR ); } } } @@ -348,15 +355,15 @@ public static function create_blocklog_table() { /** * Deactivates the WAF by deleting the relevant options and emptying rules file. * + * @throws File_System_Exception If file writing fails. + * * @return void - * @throws \Exception If file writing fails. */ public static function deactivate() { delete_option( self::MODE_OPTION_NAME ); delete_option( Waf_Rules_Manager::VERSION_OPTION_NAME ); global $wp_filesystem; - self::initialize_filesystem(); // If the rules file doesn't exist, there's nothing else to do. @@ -366,24 +373,21 @@ public static function deactivate() { // Empty the rules entrypoint file. if ( ! $wp_filesystem->put_contents( self::get_waf_file_path( Waf_Rules_Manager::RULES_ENTRYPOINT_FILE ), "generate(); - } catch ( \Exception $e ) { - return new WP_Error( 'waf_update_failed', $e->getMessage() ); - } - - return true; + ( new Waf_Standalone_Bootstrap() )->generate(); } /** @@ -404,7 +408,7 @@ public static function automatic_rules_available() { try { self::initialize_filesystem(); - } catch ( \Exception $e ) { + } catch ( Waf_Exception $e ) { return false; } diff --git a/projects/packages/waf/src/class-waf-standalone-bootstrap.php b/projects/packages/waf/src/class-waf-standalone-bootstrap.php index 116ebcc734e4b..597e31688fd17 100644 --- a/projects/packages/waf/src/class-waf-standalone-bootstrap.php +++ b/projects/packages/waf/src/class-waf-standalone-bootstrap.php @@ -8,7 +8,6 @@ namespace Automattic\Jetpack\Waf; use Composer\InstalledVersions; -use Exception; /** * Handles the bootstrap. @@ -17,6 +16,8 @@ class Waf_Standalone_Bootstrap { /** * Ensures that constants are initialized if this class is used. + * + * @return void */ public function __construct() { $this->guard_against_missing_abspath(); @@ -26,13 +27,14 @@ public function __construct() { /** * Ensures that this class is not used unless we are in the right context. * + * @throws Waf_Exception If we are outside of WordPress. + * * @return void - * @throws Exception If we are outside of WordPress. */ private function guard_against_missing_abspath() { if ( ! defined( 'ABSPATH' ) ) { - throw new Exception( 'Cannot generate the WAF bootstrap if we are not running in WordPress context.' ); + throw new Waf_Exception( 'Cannot generate the WAF bootstrap if we are not running in WordPress context.' ); } } @@ -65,8 +67,9 @@ protected function initialize_filesystem() { /** * Finds the path to the autoloader, which can then be used to require the autoloader in the generated boostrap file. * + * @throws Waf_Exception In case the autoloader file can not be found. + * * @return string|null - * @throws Exception In case the autoloader file can not be found. */ private function locate_autoloader_file() { global $jetpack_autoloader_loader; @@ -102,7 +105,7 @@ private function locate_autoloader_file() { // Check that the determined file actually exists. if ( ! file_exists( $autoload_file ) ) { - throw new Exception( 'Can not find autoloader, and the WAF standalone boostrap will not work without it.' ); + throw new Waf_Exception( 'Can not find autoloader, and the WAF standalone boostrap will not work without it.' ); } return $autoload_file; @@ -120,8 +123,11 @@ public function get_bootstrap_file_path() { /** * Generates the bootstrap file. * + * @throws File_System_Exception If the filesystem is not available. + * @throws File_System_Exception If the WAF directory can not be created. + * @throws File_System_Exception If the bootstrap file can not be created. + * * @return string Absolute path to the bootstrap file. - * @throws Exception In case the file can not be written. */ public function generate() { @@ -129,9 +135,11 @@ public function generate() { global $wp_filesystem; if ( ! $wp_filesystem ) { - throw new Exception( 'Can not work without the file system being initialized.' ); + throw new File_System_Exception( 'Can not work without the file system being initialized.' ); } + $autoloader_file = $this->locate_autoloader_file(); + $bootstrap_file = $this->get_bootstrap_file_path(); $mode_option = get_option( Waf_Runner::MODE_OPTION_NAME, false ); $share_data_option = get_option( Waf_Runner::SHARE_DATA_OPTION_NAME, false ); @@ -144,18 +152,18 @@ public function generate() { . sprintf( "define( 'JETPACK_WAF_SHARE_DATA', %s );\n", var_export( $share_data_option, true ) ) . sprintf( "define( 'JETPACK_WAF_DIR', %s );\n", var_export( JETPACK_WAF_DIR, true ) ) . sprintf( "define( 'JETPACK_WAF_WPCONFIG', %s );\n", var_export( JETPACK_WAF_WPCONFIG, true ) ) - . 'require_once ' . var_export( $this->locate_autoloader_file(), true ) . ";\n" + . 'require_once ' . var_export( $autoloader_file, true ) . ";\n" . "Automattic\Jetpack\Waf\Waf_Runner::initialize();\n"; // phpcs:enable if ( ! $wp_filesystem->is_dir( JETPACK_WAF_DIR ) ) { if ( ! $wp_filesystem->mkdir( JETPACK_WAF_DIR ) ) { - throw new Exception( 'Failed creating WAF standalone bootstrap file directory: ' . JETPACK_WAF_DIR ); + throw new File_System_Exception( 'Failed creating WAF standalone bootstrap file directory: ' . JETPACK_WAF_DIR ); } } if ( ! $wp_filesystem->put_contents( $bootstrap_file, $code ) ) { - throw new Exception( 'Failed writing WAF standalone bootstrap file to: ' . $bootstrap_file ); + throw new File_System_Exception( 'Failed writing WAF standalone bootstrap file to: ' . $bootstrap_file ); } return $bootstrap_file; diff --git a/projects/packages/waf/src/exceptions/class-file-system-exception.php b/projects/packages/waf/src/exceptions/class-file-system-exception.php new file mode 100644 index 0000000000000..e1bb18e53ea46 --- /dev/null +++ b/projects/packages/waf/src/exceptions/class-file-system-exception.php @@ -0,0 +1,24 @@ +getMessage() ); + } + +} diff --git a/projects/packages/waf/tests/php/integration/test-waf-activation.php b/projects/packages/waf/tests/php/integration/test-waf-activation.php index d87940c27522a..a88a02ef57930 100644 --- a/projects/packages/waf/tests/php/integration/test-waf-activation.php +++ b/projects/packages/waf/tests/php/integration/test-waf-activation.php @@ -49,6 +49,30 @@ public function return_sample_response() { ); } + /** + * Return a 503 wpcom rules response. + * + * @return array + */ + public function return_503_response() { + return array( + 'body' => '', + 'response' => array( + 'code' => 503, + 'message' => '', + ), + ); + } + + /** + * Return an invalid filesystem method. + * + * @return string + */ + public function return_invalid_filesystem_method() { + return 'Code is poetry.'; + } + /** * Test WAF activation. */ @@ -80,4 +104,79 @@ public function testActivation() { remove_filter( 'pre_http_request', array( $this, 'return_sample_response' ) ); } + /** + * Test WAF deactivation. + */ + public function testDeactivation() { + $deactivated = Waf_Initializer::on_deactivation(); + + // Ensure the WAF was deactivated successfully. + $this->assertTrue( $deactivated ); + + // Ensure the options were deleted. + $this->assertSame( get_option( Waf_Runner::SHARE_DATA_OPTION_NAME ), false ); + $this->assertSame( get_option( Waf_Runner::MODE_OPTION_NAME ), false ); + + // Ensure the rules entrypoint file was emptied. + $this->assertSame( file_get_contents( Waf_Runner::get_waf_file_path( Waf_Rules_Manager::RULES_ENTRYPOINT_FILE ) ), "assertTrue( is_wp_error( $activated ) ); + $this->assertSame( 'file_system_error', $activated->get_error_code() ); + + // Clean up. + remove_filter( 'pre_http_request', array( $this, 'return_sample_response' ) ); + remove_filter( 'filesystem_method', array( $this, 'return_invalid_filesystem_method' ) ); + } + + /** + * Test WAF deactivation when the filesystem is unavailable. + */ + public function testDeactivationWhenFilesystemUnavailable() { + // Break the filesystem. + add_filter( 'filesystem_method', array( $this, 'return_invalid_filesystem_method' ) ); + + // Deactivate the firewall. + $deactivated = Waf_Initializer::on_deactivation(); + + // Validate the error. + $this->assertTrue( is_wp_error( $deactivated ) ); + $this->assertSame( 'file_system_error', $deactivated->get_error_code() ); + + // Clean up. + remove_filter( 'filesystem_method', array( $this, 'return_invalid_filesystem_method' ) ); + } + + /** + * Test WAF activation when the rules API request fails. + */ + public function testActivationReturnsWpErrorWhenRulesApiRequestFails() { + // Mock the WPCOM request for retrieving the automatic rules. + add_filter( 'pre_http_request', array( $this, 'return_503_response' ) ); + + // Initialize the firewall. + $activated = Waf_Initializer::on_activation(); + + // Validate the error. + $this->assertTrue( is_wp_error( $activated ) ); + $this->assertSame( 'rules_api_error', $activated->get_error_code() ); + + // Clean up. + remove_filter( 'pre_http_request', array( $this, 'return_503_response' ) ); + } + } diff --git a/projects/packages/waf/tests/php/integration/test-waf-rest-api.php b/projects/packages/waf/tests/php/integration/test-waf-rest-api.php new file mode 100644 index 0000000000000..15a2dbce5f367 --- /dev/null +++ b/projects/packages/waf/tests/php/integration/test-waf-rest-api.php @@ -0,0 +1,179 @@ + " wp_json_encode( $sample_response ), + 'response' => array( + 'code' => 200, + 'message' => '', + ), + ); + } + + /** + * Return a 503 wpcom rules response. + * + * @return array + */ + public function return_503_response() { + return array( + 'body' => '', + 'response' => array( + 'code' => 503, + 'message' => '', + ), + ); + } + + /** + * Return an invalid filesystem method. + * + * @return string + */ + public function return_invalid_filesystem_method() { + return 'Code is poetry.'; + } + + /** + * Test /jetpack/v4/waf/update-rules. + */ + public function testUpdateRulesEndpoint() { + // Mock the WPCOM request for retrieving the automatic rules. + add_filter( 'pre_http_request', array( $this, 'return_sample_response' ) ); + + // Call /jetpack/v4/waf/update-rules. + $response = REST_Controller::update_rules(); + + // Validate the response. + $this->assertTrue( $response->data['success'] ); + + // Clean up. + remove_filter( 'pre_http_request', array( $this, 'return_sample_response' ) ); + } + + /** + * Test /jetpack/v4/waf/update-rules when the filesystem is unavailable. + */ + public function testUpdateRulesEndpointFilesystemUnavailable() { + // Mock the WPCOM request for retrieving the automatic rules. + add_filter( 'pre_http_request', array( $this, 'return_sample_response' ) ); + + // Break the filesystem. + add_filter( 'filesystem_method', array( $this, 'return_invalid_filesystem_method' ) ); + + // Call /jetpack/v4/waf/update-rules. + $response = REST_Controller::update_rules(); + + // Validate the response. + $this->assertTrue( is_wp_error( $response ) ); + $this->assertSame( 'file_system_error', $response->get_error_code() ); + + // Clean up. + remove_filter( 'pre_http_request', array( $this, 'return_sample_response' ) ); + remove_filter( 'filesystem_method', array( $this, 'return_invalid_filesystem_method' ) ); + } + + /** + * Test /jetpack/v4/waf/update-rules when the WPCOM request fails. + */ + public function testUpdateRulesEndpointWpcomRequestFails() { + // Mock the WPCOM request for retrieving the automatic rules. + add_filter( 'pre_http_request', array( $this, 'return_503_response' ) ); + + // Call /jetpack/v4/waf/update-rules. + $response = REST_Controller::update_rules(); + + // Validate the response. + $this->assertTrue( is_wp_error( $response ) ); + $this->assertSame( 'rules_api_error', $response->get_error_code() ); + + // Clean up. + remove_filter( 'pre_http_request', array( $this, 'return_503_response' ) ); + } + + /** + * Test /jetpack/v4/waf POST. + */ + public function testUpdateWaf() { + // Mock the request. + $request = new WP_REST_Request( 'POST', '/jetpack/v4/waf' ); + $request->set_header( 'content-type', 'application/json' ); + $request->set_body( + wp_json_encode( + array( + 'jetpack_waf_automatic_rules_enabled' => true, + ) + ) + ); + + // Call the endpoint. + $response = REST_Controller::update_waf( $request ); + + // Validate the response. + $this->assertFalse( is_wp_error( $response ) ); + } + + /** + * Test /jetpack/v4/waf POST when filesystem is unavailable. + */ + public function testUpdateWafFilesystemUnavailable() { + // Break the filesystem. + add_filter( 'filesystem_method', array( $this, 'return_invalid_filesystem_method' ) ); + + // Mock the request. + $request = new WP_REST_Request( 'POST', '/jetpack/v4/waf' ); + $request->set_header( 'content-type', 'application/json' ); + $request->set_body( + wp_json_encode( + array( + 'jetpack_waf_automatic_rules_enabled' => true, + ) + ) + ); + + // Call the endpoint. + $response = REST_Controller::update_waf( $request ); + + // Validate the response. + $this->assertTrue( is_wp_error( $response ) ); + $this->assertSame( 'file_system_error', $response->get_error_code() ); + + // Clean up. + remove_filter( 'filesystem_method', array( $this, 'return_invalid_filesystem_method' ) ); + } +}