diff --git a/packages/editor-tools/editor-tools.php b/packages/editor-tools/editor-tools.php index 242bcc2..6f29e4e 100644 --- a/packages/editor-tools/editor-tools.php +++ b/packages/editor-tools/editor-tools.php @@ -5,7 +5,7 @@ * Version: 1.0.0 * Author: BoxUK * Author URI: https://boxuk.com - * + * * @package Boxuk\BoxWpEditorTools */ @@ -16,5 +16,12 @@ ( new Comments() )->init(); ( new EditorCleanup() )->init(); ( new PostTypes() )->init(); -( new TemplatePersistence() )->init(); -( new Security\Security() )->init(); +( new TemplatePersistence() )->init(); +( new Security\AuthorEnumeration() )->init(); +( new Security\Headers() )->init(); +( new Security\PasswordValidation() )->init(); +( new Security\UserSessions() )->init(); +( new Security\RestrictHTTPRequestMethods() )->init(); +( new Security\RSS() )->init(); +( new Security\SessionTimeoutModifier() )->init(); +( new Security\UserLogin() )->init(); diff --git a/packages/editor-tools/src/PostTypes.php b/packages/editor-tools/src/PostTypes.php index 376a731..1ca47a4 100644 --- a/packages/editor-tools/src/PostTypes.php +++ b/packages/editor-tools/src/PostTypes.php @@ -28,7 +28,10 @@ public function init(): void { */ public function register_post_types(): void { - $path = get_template_directory() . '/post-types.json'; + $path = apply_filters( + 'boxuk_post_types_json_file_path', + get_template_directory() . '/post-types.json' + ); if ( ! file_exists( $path ) ) { return; } diff --git a/packages/editor-tools/src/Security/AuthorEnumeration.php b/packages/editor-tools/src/Security/AuthorEnumeration.php index 1829441..6a84729 100644 --- a/packages/editor-tools/src/Security/AuthorEnumeration.php +++ b/packages/editor-tools/src/Security/AuthorEnumeration.php @@ -1,7 +1,7 @@ set_404(); - + add_filter( 'wp_title', array( $this, 'get_404_title' ), PHP_INT_MAX ); - + status_header( 404 ); nocache_headers(); - + return null; } else { return $redirect; @@ -47,7 +51,7 @@ public function prevent_author_enum( string $redirect ): ?string { /** * Get 404 Title - * + * * @return string */ public function get_404_title(): string { @@ -62,8 +66,12 @@ public function get_404_title(): string { */ public function handle_rest_endpoints( array $endpoints ): array { + if ( false === apply_filters( 'boxuk_prevent_author_rest_endpoint', true ) ) { + return $endpoints; + } + // Block editor requires this endpoint for getting user details for authors. - if ( current_user_can( 'edit_posts' ) ) { + if ( current_user_can( 'edit_posts' ) ) { return $endpoints; } diff --git a/packages/editor-tools/src/Security/Headers.php b/packages/editor-tools/src/Security/Headers.php index ab5131a..fcd89c5 100644 --- a/packages/editor-tools/src/Security/Headers.php +++ b/packages/editor-tools/src/Security/Headers.php @@ -24,12 +24,14 @@ public function init(): void { /** * Hook the nosniff and frame option headers to the send_headers action. - * + * * @return void */ public function send_headers(): void { - add_action( 'send_headers', 'send_frame_options_header', 10, 0 ); - add_action( 'send_headers', 'send_nosniff_header', 10, 0 ); + if ( true === apply_filters( 'boxuk_send_no_sniff_headers', true ) ) { + add_action( 'send_headers', 'send_frame_options_header', 10, 0 ); + add_action( 'send_headers', 'send_nosniff_header', 10, 0 ); + } } /** @@ -39,7 +41,9 @@ public function send_headers(): void { * @return array */ public function remove_vip_headers( array $headers ): array { - unset( $headers['X-hacker'], $headers['X-Powered-By'] ); + if ( true === apply_filters( 'boxuk_remove_vip_headers', true ) ) { + unset( $headers['X-hacker'], $headers['X-Powered-By'] ); + } return $headers; } } diff --git a/packages/editor-tools/src/Security/PasswordValidation.php b/packages/editor-tools/src/Security/PasswordValidation.php index c2a5f8d..bfbeaf4 100644 --- a/packages/editor-tools/src/Security/PasswordValidation.php +++ b/packages/editor-tools/src/Security/PasswordValidation.php @@ -39,7 +39,18 @@ public function init(): void { * @return void */ public function user_profile_update_errors( \WP_Error $errors ): void { + + if ( false === apply_filters( 'boxuk_validate_password', true ) ) { + return; + } + $password = sanitize_text_field( $_POST['pass1'] ?? '' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verification is handled by WP core. + + // Allow empty-password field if the user is just updating their profile. + if ( doing_action( 'user_profile_update_errors' ) && empty( $password ) ) { + return; + } + $this->validate_password( $password, $errors ); } @@ -95,6 +106,9 @@ public function validate_password( string $password, \WP_Error &$errors ): void * @return string */ public function password_hint( string $hint ): string { + if ( false === apply_filters( 'boxuk_validate_password', true ) ) { + return $hint; + } $hint = __( 'Hint: The password should be at least ten characters long, and include at least one upper case letter and one number. To make it stronger, use more upper and lower case letters, more numbers, and symbols like ! " ? $ % ^ & ).', 'boxuk' ); diff --git a/packages/editor-tools/src/Security/RSS.php b/packages/editor-tools/src/Security/RSS.php index 369899c..a756a56 100644 --- a/packages/editor-tools/src/Security/RSS.php +++ b/packages/editor-tools/src/Security/RSS.php @@ -1,7 +1,7 @@ exit(); } - + /** * Feed content type - * + * * @return string */ public function feed_content_type(): string { @@ -57,11 +61,11 @@ public function feed_content_type(): string { /** * Exit - * + * * Test stub for `exit` function. - * + * * @codeCoverageIgnore -- we can't handle the `exit`. - * + * * @return void */ public function exit(): void { diff --git a/packages/editor-tools/src/Security/RestrictHTTPRequestMethods.php b/packages/editor-tools/src/Security/RestrictHTTPRequestMethods.php index d9a76fa..540cdbe 100644 --- a/packages/editor-tools/src/Security/RestrictHTTPRequestMethods.php +++ b/packages/editor-tools/src/Security/RestrictHTTPRequestMethods.php @@ -44,8 +44,7 @@ public function init() { * @return void * */ public function block_request_if_not_using_allowed_method() { - - if ( $this->is_cli() ) { + if ( $this->is_cli() || ( ! apply_filters( 'boxuk_restrict_http_request_methods', true ) ) ) { return; } @@ -57,7 +56,7 @@ public function block_request_if_not_using_allowed_method() { /** * Get the current method. - * + * * @return string The current method or empty string if it can't be determined. */ public function get_method(): string { @@ -66,12 +65,12 @@ public function get_method(): string { /** * Check if the request is from the command line. - * + * * @return bool Whether the request is from the command line. - * + * * @codeCoverageIgnore -- We can't mock constants. */ public function is_cli(): bool { - return defined( 'WP_CLI' ) && WP_CLI; + return defined( 'WP_CLI' ) && \WP_CLI; } } diff --git a/packages/editor-tools/src/Security/Security.php b/packages/editor-tools/src/Security/Security.php deleted file mode 100644 index 34857e2..0000000 --- a/packages/editor-tools/src/Security/Security.php +++ /dev/null @@ -1,72 +0,0 @@ -init(); - } - - if ( $headers ) { - ( new Headers() )->init(); - } - - if ( $password_validation ) { - ( new PasswordValidation() )->init(); - } - - if ( $restricted_user_sessions ) { - ( new UserSessions() )->init(); - } - - if ( $restricted_http_request_methods ) { - ( new RestrictHTTPRequestMethods() )->init(); - } - - if ( $restrict_rss ) { - ( new RSS() )->init(); - } - - if ( $modify_session_timeouts ) { - ( new SessionTimeoutModifier() )->init(); - } - - if ( $user_login_hardening ) { - ( new UserLogin() )->init(); - } - } -} diff --git a/packages/editor-tools/src/Security/SessionTimeoutModifier.php b/packages/editor-tools/src/Security/SessionTimeoutModifier.php index 86dfd30..fdc59e7 100644 --- a/packages/editor-tools/src/Security/SessionTimeoutModifier.php +++ b/packages/editor-tools/src/Security/SessionTimeoutModifier.php @@ -24,7 +24,7 @@ public function init(): void { } /** - * Modify the default session timeout value. + * Modify the default session timeout value. * * @param int $wp_default_expiration the default WP session expiration timeout value, in seconds. * @param int $user_id the current user id. @@ -33,6 +33,10 @@ public function init(): void { * @return int */ public function auth_cookie_expiration_filter( int $wp_default_expiration, int $user_id, bool $remember_me ): int { + if ( false === apply_filters( 'boxuk_modify_session_timeout', true ) ) { + return $wp_default_expiration; + } + if ( $remember_me ) { return $wp_default_expiration; } diff --git a/packages/editor-tools/src/Security/UserLogin.php b/packages/editor-tools/src/Security/UserLogin.php index 44e0c02..90d5f6a 100644 --- a/packages/editor-tools/src/Security/UserLogin.php +++ b/packages/editor-tools/src/Security/UserLogin.php @@ -16,11 +16,11 @@ class UserLogin { /** * Init - * + * * @return void */ public function init(): void { - add_filter( 'map_meta_cap', array( $this, 'restrict_super_admins' ), 10, 2 ); + add_filter( 'map_meta_cap', array( $this, 'restrict_user_creation' ), 10, 2 ); add_action( 'login_init', array( $this, 'restrict_login_by_username' ) ); add_filter( 'show_password_fields', array( $this, 'show_password_fields' ), 10, 2 ); } @@ -32,17 +32,18 @@ public function init(): void { * @param string|null $cap the capability to check. * @return array */ - public function restrict_super_admins( array $caps, ?string $cap ): array { - if ( 'create_users' === $cap ) { + public function restrict_user_creation( array $caps, ?string $cap ): array { + + if ( 'create_users' === $cap && apply_filters( 'boxuk_restrict_user_creation', false ) ) { $caps[] = 'do_not_allow'; } - + return $caps; } /** * Restrict login by username. - * + * * Prevents users from logging in with their username and enforces the use of their email address. * This is added on `login_init` because if we add it at a global level it prevents users from * being able to reset their password. @@ -50,18 +51,24 @@ public function restrict_super_admins( array $caps, ?string $cap ): array { * @return void */ public function restrict_login_by_username(): void { - remove_filter( 'authenticate', 'wp_authenticate_username_password', 20 ); + if ( true === apply_filters( 'boxuk_restrict_login_by_username', true ) ) { + remove_filter( 'authenticate', 'wp_authenticate_username_password', 20 ); + } } /** * Remove password fields from profile if editing another user. - * + * * @param bool $value The existing value to determine if the password fields should show. * @param \WP_User $user The user object. * * @return bool */ public function show_password_fields( bool $value, \WP_User $user ): bool { + if ( false === apply_filters( 'boxuk_restrict_user_creation', false ) ) { + return $value; + } + if ( get_current_user_id() !== $user->ID ) { $value = false; } diff --git a/packages/editor-tools/tests/Security/TestAuthorEnumeration.php b/packages/editor-tools/tests/Security/TestAuthorEnumeration.php index eae3af7..1ebb685 100644 --- a/packages/editor-tools/tests/Security/TestAuthorEnumeration.php +++ b/packages/editor-tools/tests/Security/TestAuthorEnumeration.php @@ -20,7 +20,7 @@ class TestAuthorEnumeration extends TestCase { * Test `init` method */ public function test_init(): void { - + $author_enumeration = new AuthorEnumeration(); \WP_Mock::expectFilterAdded( 'redirect_canonical', array( $author_enumeration, 'prevent_author_enum' ) ); @@ -33,26 +33,29 @@ public function test_init(): void { /** * Test `prevent_author_enum` method - * - * @param string|bool $query_var The author query var value. - * @param bool $expected Whether the author enumeration should be disabled. - * + * + * @param string|bool $query_var The author query var value. + * @param bool $expected Whether the author enumeration should be disabled. + * @param bool $filter_enabled Whether the filter is enabled. + * * @return void - * + * * @dataProvider disable_author_enumeration_provider */ - public function test_prevent_author_enum( bool|string $query_var, bool $expected ): void { - + public function test_prevent_author_enum( bool|string $query_var, bool $expected, bool $filter_enabled ): void { + + \WP_Mock::onFilter( 'boxuk_prevent_author_enum' )->with( true )->reply( $filter_enabled ); + \WP_Mock::userFunction( 'get_query_var' ) ->with( 'author', false ) ->andReturn( $query_var ); $author_enumeration = new AuthorEnumeration(); - if ( ! $expected ) { + if ( ! $expected ) { \WP_Mock::expectFilterNotAdded( 'wp_title', array( $author_enumeration, 'get_404_title' ), PHP_INT_MAX ); $this->assertEquals( 'test', $author_enumeration->prevent_author_enum( 'test' ) ); - } else { + } else { global $wp_query; $wp_query = Mockery::mock( 'WP_Query' ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Mocking WP_Query $wp_query->expects( 'set_404' )->once(); @@ -63,14 +66,14 @@ public function test_prevent_author_enum( bool|string $query_var, bool $expected ->once(); \WP_Mock::userFunction( 'nocache_headers' ) ->once(); - + $this->assertNull( $author_enumeration->prevent_author_enum( 'test' ) ); } } /** * Data provider for `test_disable_author_enumeration` - * + * * @return array */ public function disable_author_enumeration_provider(): array { @@ -78,8 +81,20 @@ public function disable_author_enumeration_provider(): array { array( 'author', true, + true, + ), + array( + false, + false, + true, ), array( + 'author', + false, + false, + ), + array( + false, false, false, ), @@ -88,7 +103,7 @@ public function disable_author_enumeration_provider(): array { /** * Test `get_404_title` method - * + * * @return void */ public function test_get_404_title(): void { @@ -99,14 +114,19 @@ public function test_get_404_title(): void { /** * Test `handle_rest_endpoints` - * + * * @param bool $is_authorised_user If the current user has permissions. - * + * @param bool $filter_enabled If the filter is enabled. + * * @return void - * - * @dataProvider true_false_provider + * + * @dataProvider rest_endpoint_data_provider */ - public function test_handle_rest_endpoints( bool $is_authorised_user ) { + public function test_handle_rest_endpoints( bool $is_authorised_user, bool $filter_enabled ) { + + \WP_Mock::onFilter( 'boxuk_prevent_author_rest_endpoint' ) + ->with( true ) + ->reply( $filter_enabled ); $endpoints = array( '/wp/v2/users' => true, @@ -114,9 +134,10 @@ public function test_handle_rest_endpoints( bool $is_authorised_user ) { ); \WP_Mock::userFunction( 'current_user_can' ) + ->times( (int) $filter_enabled ) ->with( 'edit_posts' ) ->andReturn( $is_authorised_user ); - + $author_enumeration = new AuthorEnumeration(); $expected = $is_authorised_user ? $endpoints : array(); @@ -126,13 +147,14 @@ public function test_handle_rest_endpoints( bool $is_authorised_user ) { /** * Data provider for `test_handle_rest_endpoints` - * + * * @return array */ - public function true_false_provider(): array { + public function rest_endpoint_data_provider(): array { return array( - array( true ), - array( false ), + array( true, true ), + array( false, true ), + array( true, false ), ); } } diff --git a/packages/editor-tools/tests/Security/TestPasswordValidation.php b/packages/editor-tools/tests/Security/TestPasswordValidation.php index 34ec5e1..55219ab 100644 --- a/packages/editor-tools/tests/Security/TestPasswordValidation.php +++ b/packages/editor-tools/tests/Security/TestPasswordValidation.php @@ -20,7 +20,7 @@ class TestPasswordValidation extends TestCase { * Test `init` method */ public function test_init(): void { - + $password_validation = new PasswordValidation(); \WP_Mock::expectActionAdded( 'user_profile_update_errors', array( $password_validation, 'user_profile_update_errors' ) ); @@ -35,17 +35,31 @@ public function test_init(): void { /** * Test `user_profile_update_errors` method - * + * * @param string $password The password to check. + * @param bool $filter_enabled Whether the filter is enabled. + * @param string $hook_name The hook name. * @param string[] $expected_errors Whether an error should be expected. - * + * * @return void - * + * * @dataProvider user_profile_update_errors_provider */ - public function test_user_profile_update_errors( string $password, array $expected_errors ): void { + public function test_user_profile_update_errors( string $password, bool $filter_enabled, string $hook_name, array $expected_errors ): void { + $_POST['pass1'] = $password; + + \WP_Mock::onFilter( 'boxuk_validate_password' )->with( true )->reply( $filter_enabled ); + + \WP_Mock::userFunction( 'doing_action' ) + ->with( 'user_profile_update_errors' ) + ->times( (int) $filter_enabled ) + ->andReturn( 'user_profile_update_errors' === $hook_name ); + + \WP_Mock::userFunction( 'sanitize_text_field' ) + ->with( $password ) + ->times( (int) $filter_enabled ) + ->andReturn( $password ); - \WP_Mock::userFunction( 'sanitize_text_field' )->once()->andReturn( $password ); $error_holder = Mockery::mock( 'WP_Error' ); $error_holder->expects( 'add' )->times( count( $expected_errors ) )->andReturnUsing( @@ -54,6 +68,7 @@ function ( string $code, string $message ) use ( $expected_errors ) { } ); + $password_validation = new PasswordValidation(); $password_validation->user_profile_update_errors( $error_holder ); @@ -62,60 +77,117 @@ function ( string $code, string $message ) use ( $expected_errors ) { /** * Provider for `test_user_profile_update_errors` method. - * + * * @return array */ public function user_profile_update_errors_provider(): array { return array( - 'password too short' => array( + 'password too short' => array( 'password' => 'test', + 'enabled' => true, + 'hook_name' => 'validate_password_reset', 'expect_errors' => array( 'This value is too short. It should have 10 characters or more.', 'Password must contain at least one number.', 'Password must contain at least one uppercase letter.', ), ), - 'password too long' => array( + 'password too long' => array( 'password' => 'testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttest', + 'enabled' => true, + 'hook_name' => 'validate_password_reset', 'expect_errors' => array( 'This value is too long. It should have 72 characters or less.', 'Password must contain at least one uppercase letter.', 'Password must contain at least one number.', ), ), - 'no number' => array( - 'testtesttest', - array( + 'no number' => array( + 'password' => 'testtesttest', + 'enabled' => true, + 'hook_name' => 'validate_password_reset', + 'expect_errors' => array( 'Password must contain at least one number.', 'Password must contain at least one uppercase letter.', ), ), - 'no uppercase' => array( - 'testtesttest1', - array( + 'no uppercase' => array( + 'password' => 'testtesttest1', + 'enabled' => true, + 'hook_name' => 'validate_password_reset', + 'expect_errors' => array( 'Password must contain at least one uppercase letter.', ), ), - 'no lowercase' => array( - 'TESTTESTTEST1', - array( + 'no lowercase' => array( + 'password' => 'TESTTESTTEST1', + 'enabled' => true, + 'hook_name' => 'validate_password_reset', + 'expect_errors' => array( 'Password must contain at least one lowercase letter.', ), ), - 'valid password' => array( - 'Testtesttest1', - array(), + 'valid password' => array( + 'password' => 'Testtesttest1', + 'enabled' => true, + 'hook_name' => 'validate_password_reset', + 'expect_errors' => array(), + ), + 'disabled feature, valid password' => array( + 'password' => 'Testtesttest1', + 'enabled' => false, + 'hook_name' => 'validate_password_reset', + 'expect_errors' => array(), + ), + 'disabled feature, invalid password' => array( + 'password' => 'test', + 'enabled' => false, + 'hook_name' => 'validate_password_reset', + 'expect_errors' => array(), + ), + 'on profile-update with empty password' => array( + 'password' => '', + 'enabled' => true, + 'hook_name' => 'user_profile_update_errors', + 'expect_errors' => array(), ), ); } /** * Test Password Hint - * + * + * @param bool $enabled Whether the filter is enabled. + * @param string $expected The expected password hint. + * + * @dataProvider password_hint_provider + * * @return void */ - public function test_password_hint(): void { + public function test_password_hint( bool $enabled, string $expected ): void { + + \WP_Mock::onFilter( 'boxuk_validate_password' )->with( true )->reply( $enabled ); + $password_validation = new PasswordValidation(); - $this->assertEquals( 'Hint: The password should be at least ten characters long, and include at least one upper case letter and one number. To make it stronger, use more upper and lower case letters, more numbers, and symbols like ! " ? $ % ^ & ).', $password_validation->password_hint( '' ) ); + + $this->assertEquals( $expected, $password_validation->password_hint( 'test' ) ); + } + + /** + * Provider for `test_password_hint` method. + * + * @return array + */ + public function password_hint_provider(): array { + return array( + 'enabled' => array( + 'enabled' => true, + 'expected' => 'Hint: The password should be at least ten characters long, and include at least one upper case letter and one number. To make it stronger, use more upper and lower case letters, more numbers, and symbols like ! " ? $ % ^ & ).', + ), + 'disabled' => array( + 'enabled' => false, + 'expected' => 'test', + ), + ); } } diff --git a/packages/editor-tools/tests/Security/TestRSS.php b/packages/editor-tools/tests/Security/TestRSS.php index 1924e73..27c6cf2 100644 --- a/packages/editor-tools/tests/Security/TestRSS.php +++ b/packages/editor-tools/tests/Security/TestRSS.php @@ -18,28 +18,60 @@ class TestRSS extends TestCase { /** * Test `init` method + * + * @param bool $enabled Whether the feature is enabled. + * + * @dataProvider init_provider */ - public function test_init() { + public function test_init( bool $enabled ) { + \WP_Mock::onFilter( 'boxuk_disable_rss' )->with( true )->reply( $enabled ); $class_in_test = new RSS(); - \WP_Mock::expectActionAdded( 'do_feed', array( $class_in_test, 'send_404' ), 1 ); - \WP_Mock::expectActionAdded( 'do_feed_rdf', array( $class_in_test, 'send_404' ), 1 ); - \WP_Mock::expectActionAdded( 'do_feed_rss', array( $class_in_test, 'send_404' ), 1 ); - \WP_Mock::expectActionAdded( 'do_feed_rss2', array( $class_in_test, 'send_404' ), 1 ); - \WP_Mock::expectActionAdded( 'do_feed_atom', array( $class_in_test, 'send_404' ), 1 ); - \WP_Mock::expectActionAdded( 'do_feed_rss2_comments', array( $class_in_test, 'send_404' ), 1 ); - \WP_Mock::expectActionAdded( 'do_feed_atom_comments', array( $class_in_test, 'send_404' ), 1 ); + if ( ! $enabled ) { + \WP_Mock::expectActionNotAdded( 'do_feed', array( $class_in_test, 'send_404' ), 1 ); + \WP_Mock::expectActionNotAdded( 'do_feed_rdf', array( $class_in_test, 'send_404' ), 1 ); + \WP_Mock::expectActionNotAdded( 'do_feed_rss', array( $class_in_test, 'send_404' ), 1 ); + \WP_Mock::expectActionNotAdded( 'do_feed_rss2', array( $class_in_test, 'send_404' ), 1 ); + \WP_Mock::expectActionNotAdded( 'do_feed_atom', array( $class_in_test, 'send_404' ), 1 ); + \WP_Mock::expectActionNotAdded( 'do_feed_rss2_comments', array( $class_in_test, 'send_404' ), 1 ); + \WP_Mock::expectActionNotAdded( 'do_feed_atom_comments', array( $class_in_test, 'send_404' ), 1 ); + + \WP_Mock::userFunction( 'remove_action' ) + ->never()->with( 'wp_head', 'feed_links_extra', 3 ); + \WP_Mock::userFunction( 'remove_action' ) + ->never()->with( 'wp_head', 'feed_links', 2 ); + } else { + \WP_Mock::expectActionAdded( 'do_feed', array( $class_in_test, 'send_404' ), 1 ); + \WP_Mock::expectActionAdded( 'do_feed_rdf', array( $class_in_test, 'send_404' ), 1 ); + \WP_Mock::expectActionAdded( 'do_feed_rss', array( $class_in_test, 'send_404' ), 1 ); + \WP_Mock::expectActionAdded( 'do_feed_rss2', array( $class_in_test, 'send_404' ), 1 ); + \WP_Mock::expectActionAdded( 'do_feed_atom', array( $class_in_test, 'send_404' ), 1 ); + \WP_Mock::expectActionAdded( 'do_feed_rss2_comments', array( $class_in_test, 'send_404' ), 1 ); + \WP_Mock::expectActionAdded( 'do_feed_atom_comments', array( $class_in_test, 'send_404' ), 1 ); - \WP_Mock::userFunction( 'remove_action' ) - ->once()->with( 'wp_head', 'feed_links_extra', 3 ); - \WP_Mock::userFunction( 'remove_action' ) - ->once()->with( 'wp_head', 'feed_links', 2 ); + \WP_Mock::userFunction( 'remove_action' ) + ->once()->with( 'wp_head', 'feed_links_extra', 3 ); + \WP_Mock::userFunction( 'remove_action' ) + ->once()->with( 'wp_head', 'feed_links', 2 ); + } $class_in_test->init(); $this->assertConditionsMet(); } + /** + * Provider for `init` method + * + * @return array + */ + public function init_provider(): array { + return array( + 'enabled' => array( true ), + 'disabled' => array( false ), + ); + } + /** * Test `send_404` method */ diff --git a/packages/editor-tools/tests/Security/TestRestictHTTPRequestMethods.php b/packages/editor-tools/tests/Security/TestRestictHTTPRequestMethods.php index 177e8e4..a781443 100644 --- a/packages/editor-tools/tests/Security/TestRestictHTTPRequestMethods.php +++ b/packages/editor-tools/tests/Security/TestRestictHTTPRequestMethods.php @@ -20,7 +20,7 @@ class TestRestictHTTPRequestMethods extends TestCase { * Test `init` method */ public function test_init(): void { - + $class_in_test = new RestrictHTTPRequestMethods(); \WP_Mock::expectActionAdded( 'init', array( $class_in_test, 'block_request_if_not_using_allowed_method' ) ); @@ -32,47 +32,49 @@ public function test_init(): void { /** * Test `block_request_if_not_using_allowed_method` method - * + * * @param string $method The request method. * @param bool $is_cli Whether the request is from the command line. * @param bool $expected Whether the request should be blocked. - * + * * @return void - * + * * @dataProvider block_request_if_not_using_allowed_method_provider */ public function test_block_request_if_not_using_allowed_method( string $method, bool $is_cli, bool $expected ): void { - - $_SERVER['REQUEST_METHOD'] = $method; - - \WP_Mock::userFunction( 'sanitize_text_field' ) - ->with( $method ) - ->andReturn( $method ); - $class_in_test = Mockery::mock( RestrictHTTPRequestMethods::class ) ->makePartial(); - $class_in_test->expects( 'is_cli' )->once()->andReturn( $is_cli ); + $class_in_test->expects( 'is_cli' ) + ->once() + ->andReturn( $is_cli ); + + if ( ! $is_cli ) { + $class_in_test->expects( 'get_method' ) + ->once() + ->andReturn( $method ); + } else { + $class_in_test->expects( 'get_method' )->never(); + } - if ( $expected ) { + if ( $expected ) { \WP_Mock::userFunction( 'status_header' ) ->with( 403 ); \WP_Mock::userFunction( 'wp_die' ) ->with( 'Invalid request method.' ); - } else { + } else { + \WP_Mock::userFunction( 'status_header' )->never(); \WP_Mock::userFunction( 'wp_die' )->never(); } $class_in_test->block_request_if_not_using_allowed_method(); - unset( $_SERVER['REQUEST_METHOD'] ); - $this->assertConditionsMet(); } /** * Data provider for `test_block_request_if_not_using_allowed_method` - * + * * @return array */ public function block_request_if_not_using_allowed_method_provider(): array { @@ -100,4 +102,45 @@ public function block_request_if_not_using_allowed_method_provider(): array { array( 'INVALID', true, false ), ); } + + /** + * Test `get_method` method + * + * @param string $method The request method. + * + * @dataProvider get_method_provider + * + * @return void + */ + public function test_get_method( string $method ): void { + $class_in_test = new RestrictHTTPRequestMethods(); + + $_SERVER['REQUEST_METHOD'] = $method; + + \WP_Mock::userFunction( 'sanitize_text_field' ) + ->once() + ->with( $method ) + ->andReturn( $method ); + + $this->assertEquals( $method, $class_in_test->get_method() ); + } + + /** + * Data provider for `test_get_method` + * + * @return array + */ + public function get_method_provider(): array { + return array( + array( 'POST' ), + array( 'GET' ), + array( 'PUT' ), + array( 'PATCH' ), + array( 'DELETE' ), + array( 'HEAD' ), + array( 'OPTIONS' ), + array( 'PURGE' ), + array( 'INVALID' ), + ); + } } diff --git a/packages/editor-tools/tests/Security/TestSecurity.php b/packages/editor-tools/tests/Security/TestSecurity.php deleted file mode 100644 index 33592ce..0000000 --- a/packages/editor-tools/tests/Security/TestSecurity.php +++ /dev/null @@ -1,117 +0,0 @@ - $args The arguments to pass to the init method. - * - * @return void - * - * @dataProvider init_provider - */ - public function testInit( array $args ): void { - list( - $author_enumeration, - $headers, - $password_validation, - $restricted_user_sessions, - $restricted_http_request_methods, - $restrict_rss, - $modify_session_timeouts, - $user_login_hardening - ) = $args; - - $author_enumeration_spy = Mockery::mock( 'overload:' . AuthorEnumeration::class ); - $headers_spy = Mockery::mock( 'overload:' . Headers::class ); - $password_validation_spy = Mockery::mock( 'overload:' . PasswordValidation::class ); - $user_sessions_spy = Mockery::mock( 'overload:' . UserSessions::class ); - $restrict_http_request_methods_spy = Mockery::mock( 'overload:' . RestrictHTTPRequestMethods::class ); - $restrict_rss_spy = Mockery::mock( 'overload:' . RSS::class ); - $session_timeout_modifier_spy = Mockery::mock( 'overload:' . SessionTimeoutModifier::class ); - $user_login_hardening_spy = Mockery::mock( 'overload:' . UserLogin::class ); - - if ( $author_enumeration ) { - $author_enumeration_spy->shouldReceive( 'init' )->once(); - } - - if ( $headers ) { - $headers_spy->shouldReceive( 'init' )->once(); - } - - if ( $password_validation ) { - $password_validation_spy->shouldReceive( 'init' )->once(); - } - - if ( $restricted_user_sessions ) { - $user_sessions_spy->shouldReceive( 'init' )->once(); - } - - if ( $restricted_http_request_methods ) { - $restrict_http_request_methods_spy->shouldReceive( 'init' )->once(); - } - - if ( $restrict_rss ) { - $restrict_rss_spy->shouldReceive( 'init' )->once(); - } - - if ( $modify_session_timeouts ) { - $session_timeout_modifier_spy->shouldReceive( 'init' )->once(); - } - - if ( $user_login_hardening ) { - $user_login_hardening_spy->shouldReceive( 'init' )->once(); - } - - $security = new Security(); - $security->init( - $author_enumeration, - $headers, - $password_validation, - $restricted_user_sessions, - $restricted_http_request_methods, - $restrict_rss, - $modify_session_timeouts, - $user_login_hardening - ); - - $this->assertConditionsMet(); - } - - /** - * Data provider for `testInit` - * - * @return array - */ - public function init_provider(): array { - return array( - array( array( true, true, true, true, true, true, true, true ) ), - array( array( true, true, true, true, true, true, true, false ) ), - array( array( true, true, true, true, true, true, false, false ) ), - array( array( true, true, true, true, true, false, false, false ) ), - array( array( true, true, true, true, false, false, false, false ) ), - array( array( true, true, true, false, false, false, false, false ) ), - array( array( true, true, false, false, false, false, false, false ) ), - array( array( true, false, false, false, false, false, false, false ) ), - array( array( false, false, false, false, false, false, false, false ) ), - ); - } -} diff --git a/packages/editor-tools/tests/Security/TestSessionTimeoutModifier.php b/packages/editor-tools/tests/Security/TestSessionTimeoutModifier.php index bedaf77..2af6a93 100644 --- a/packages/editor-tools/tests/Security/TestSessionTimeoutModifier.php +++ b/packages/editor-tools/tests/Security/TestSessionTimeoutModifier.php @@ -31,7 +31,7 @@ public function setUp(): void { /** * Test `init` method - * + * * @return void */ public function test_init(): void { @@ -44,27 +44,31 @@ public function test_init(): void { /** * Test `auth_cookie_expiration_filter` method - * + * * @param bool $remember_me Whether the user ticked the 'remember me' box. + * @param bool $enabled Whether the feature is enabled. * @param int $expected The expected expiration time. - * + * * @return void - * + * * @dataProvider auth_cookie_expiration_filter_provider */ - public function test_auth_cookie_expiration_filter( bool $remember_me, int $expected ): void { + public function test_auth_cookie_expiration_filter( bool $remember_me, bool $enabled, int $expected ): void { + \WP_Mock::onFilter( 'boxuk_modify_session_timeout' )->with( true )->reply( $enabled ); $this->assertEquals( $expected, $this->sut->auth_cookie_expiration_filter( 200, 1, $remember_me ) ); } /** * Data provider for `test_auth_cookie_expiration_filter` - * + * * @return array */ public function auth_cookie_expiration_filter_provider(): array { return array( - array( true, 200 ), - array( false, 36000 ), + array( true, true, 200 ), + array( false, true, 36000 ), + array( true, false, 200 ), + array( false, false, 200 ), ); } } diff --git a/packages/editor-tools/tests/Security/TestUserLogin.php b/packages/editor-tools/tests/Security/TestUserLogin.php index 7543e67..88dd08d 100644 --- a/packages/editor-tools/tests/Security/TestUserLogin.php +++ b/packages/editor-tools/tests/Security/TestUserLogin.php @@ -20,10 +20,10 @@ class TestUserLogin extends TestCase { * Test `init` method */ public function test_init(): void { - + $user_login = new UserLogin(); - \WP_Mock::expectFilterAdded( 'map_meta_cap', array( $user_login, 'restrict_super_admins' ), 10, 2 ); + \WP_Mock::expectFilterAdded( 'map_meta_cap', array( $user_login, 'restrict_user_creation' ), 10, 2 ); \WP_Mock::expectActionAdded( 'login_init', array( $user_login, 'restrict_login_by_username' ) ); \WP_Mock::expectFilterAdded( 'show_password_fields', array( $user_login, 'show_password_fields' ), 10, 2 ); @@ -34,35 +34,41 @@ public function test_init(): void { /** * Test `restrict_super_admins` method - * + * * @param string $cap The capabilities to check. * @param string[] $expected The expected value. - * + * @param bool $feature_enabled Whether the feature is enabled. + * * @return void - * + * * @dataProvider restrict_super_admins_provider */ - public function test_restrict_super_admins( string $cap, array $expected ): void { + public function test_restrict_super_admins( string $cap, array $expected, bool $feature_enabled ): void { + + \WP_Mock::onFilter( 'boxuk_restrict_user_creation' )->with( false )->reply( $feature_enabled ); + $user_login = new UserLogin(); - $this->assertEquals( $expected, $user_login->restrict_super_admins( array(), $cap ) ); + $this->assertEquals( $expected, $user_login->restrict_user_creation( array(), $cap ) ); } /** * Provider for `restrict_super_admins` method - * + * * @return array */ public function restrict_super_admins_provider(): array { - return array( - 'should_restrict' => array( 'create_users', array( 'do_not_allow' ) ), - 'should_not_restrict' => array( 'edit_posts', array() ), + return array( + 'should_restrict' => array( 'create_users', array( 'do_not_allow' ), true ), + 'should_not_restrict' => array( 'edit_posts', array(), true ), + 'disabled feature' => array( 'create_users', array(), false ), + 'other cap disabled' => array( 'edit_posts', array(), false ), ); } /** * Test `restrict_login_by_username` method - * + * * @return void */ public function test_restrict_login_by_username(): void { @@ -75,38 +81,44 @@ public function test_restrict_login_by_username(): void { /** * Test `show_password_fields` method - * + * * @param bool $value The existing value to determine if the password fields should show. * @param int $user_id The user id. * @param int $current_id The current user id. * @param bool $expected The expected value. - * + * @param bool $enabled Whether the feature is enabled. + * * @return void - * + * * @dataProvider show_password_fields_provider */ - public function test_show_password_fields( bool $value, int $user_id, int $current_id, bool $expected ): void { + public function test_show_password_fields( bool $value, int $user_id, int $current_id, bool $expected, bool $enabled ): void { + + \WP_Mock::onFilter( 'boxuk_restrict_user_creation' )->with( false )->reply( $enabled ); + $user_login = new UserLogin(); $user = Mockery::mock( 'WP_User' ); $user->ID = $user_id; - \WP_Mock::userFunction( 'get_current_user_id' )->once()->andReturn( $current_id ); + \WP_Mock::userFunction( 'get_current_user_id' )->times( (int) $enabled )->andReturn( $current_id ); $this->assertEquals( $expected, $user_login->show_password_fields( $value, $user ) ); } /** * Provider for `show_password_fields` method - * + * * @return array */ public function show_password_fields_provider(): array { return array( - array( true, 1, 1, true ), - array( true, 1, 2, false ), - array( false, 1, 1, false ), - array( false, 1, 2, false ), + array( true, 1, 1, true, true ), + array( true, 1, 2, false, true ), + array( false, 1, 1, false, true ), + array( false, 1, 2, false, true ), + array( false, 1, 1, false, false ), + array( true, 1, 1, true, false ), ); } }