diff --git a/docs/editor-tools/BlockLoader.md b/docs/editor-tools/BlockLoader.md deleted file mode 100644 index 2ec3d8c..0000000 --- a/docs/editor-tools/BlockLoader.md +++ /dev/null @@ -1,15 +0,0 @@ -# Block Loader - -The Block Loader will automatically load all blocks by looking for `block.json` files in a given path. These are loaded with `register_block_type`, see [WordPress Docs](https://developer.wordpress.org/reference/functions/register_block_type/) for more context. - -## Usage -You should initialise the block-loader early, before `init`. -```php -add_action( - 'plugins_loaded', - function () { - $block_loader = new \Boxuk\BoxWpEditorTools\BlockLoader(); - $block_loader->init(); - } -); -``` diff --git a/docs/editor-tools/Comments.md b/docs/editor-tools/Comments.md deleted file mode 100644 index 24fc88f..0000000 --- a/docs/editor-tools/Comments.md +++ /dev/null @@ -1,14 +0,0 @@ -# Comment Disablement - -This will remove the ability for comments from your WP installation. - -## Usage -You should call the `init()` method as early as possible in your code. -```php -add_action( - 'plugins_loaded', - function () { - ( new \Boxuk\BoxWpEditorTools\Comments() )->init(); - } -); -``` diff --git a/docs/editor-tools/Security.md b/docs/editor-tools/Security.md deleted file mode 100644 index f2b4e07..0000000 --- a/docs/editor-tools/Security.md +++ /dev/null @@ -1,43 +0,0 @@ -# Security - -Some security hardening features are available for you to use. - -## Usage -There's a default set of configurations, so you can just need to call the `init()` method as early as possible. -```php -add_action( - 'plugins_loaded', - function() { - ( new Boxuk\BoxWpEditorTools\Security\Security() )->init(); - } -); -``` - -## Advanced Usage - -The `init()` method of the `Security` class accepts a collection of boolean values to enable/disable features. They _all_ default to enabled, so you can pass `false` to disable any feature if it's causing you an issue. Alternatively, you can only initialise the required classes as needed. - - **Author Enumeration** - prevents the well-known author enumeration vector via the REST API. - - **Headers** - Adds frame options and no-sniff headers, along with removing som eheaders that VIP add to the response. - - **Password Validation** - adds a bunch of rules to enforce stronger passwords ( 10-72 characters, one upper, one lower, one number ). - - **User Sessions** - Prevents users from having multiple sessions logged in at one time. An option to set the maximum number of sessions is added to the Settings screen in the Admin Area. - - **HTTP Request Methods** - ensures that all HTTP requests are valid. - - **RSS** - prevents RSS feeds being generated from the content - - **Session Timeouts** - Changes the default WP session to 10 hours (instead of 'session', ie PHP session cookie!) unless the user selects 'remember me'. - -For example: -```php -add_action( - 'plugins_loaded', - function() { - ( new Boxuk\BoxWpEditorTools\Security\Security() )->init( - true, // author-enumeration - true, // headers - false, // password validation - false, // concurrent user-sessions - true, // HTTP request methods - false, // RSS - true // Session timeouts - ); - } -); -``` diff --git a/docs/editor-tools/index.md b/docs/editor-tools/index.md deleted file mode 100644 index 5c50589..0000000 --- a/docs/editor-tools/index.md +++ /dev/null @@ -1,33 +0,0 @@ -# Box WordPress Editor Tools - -A collection of tools for modifying the WordPress Editor. - -## Quick Start! - -The tools here don't load automatically, so you need to get them going. -The quickest way would be to add to `functions.php`: -```php -( new \Boxuk\BoxWpEditorTools\BlockLoader() )->init(); // loads all block.json from /wp-content/themes/{theme}/build/**/*/block.json -( new \Boxuk\BoxWpEditorTools\Comments() )->init(); // disables comments -( new \Boxuk\BoxWpEditorTools\EditorCleanup() )->init(); // Cleans the block-editor to prevent loading plugins -( new \Boxuk\BoxWpEditorTools\PostTypes() )->init(); // registers post-types defined in /wp-content/themes/{theme}/post-types.json -( new \Boxuk\BoxWpEditorTools\TemplatePersistence() )->init(); // saves template changes to disk, not to the database. -( new \Boxuk\BoxWpEditorTools\Security\Security() )->init(); // Enables security hardening. -``` -There's more options than that, so checkout the links below: - -## Features - - - [Asset Loader](./docs/AssetLoader.md) - help load assets generated by [wp-scripts](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-scripts/). - - [Block Loader](./docs/BlockLoader.md) - auto-loads blocks to the editor. - - [Comment Disablement](./docs/Comments.md) - disables comments. - - [Editor Cleanup](./docs/EditorCleanup.md) - removes some unnecessary bits from the block editor. - - [Post Type Registrations](./docs/PostTypes.md) - speeds up post-type registration with a single JSON file. - - [Template Persistence](./docs/TemplatePersistence.md) - speeds up template modifications by saving to disk instead of the database. - - [Security](./docs/Security.md) - Adds security hardening. - -## Contributing - -The dependancies include [WordPress Stubs](https://github.com/php-stubs/wordpress-stubs), so your IDE should automatically include type information for all WP core functions. If they're not, it's likely a mis-configuration of your IDE. There's helper guides in the WordPress Stubs repo. - -Working on the repo requires packaging this into a functioning WordPress installation. A ready-to-go solution is yet to be developed so a PR is welcome, preferrably where Docker is the only dependancy. diff --git a/docs/index.md b/docs/index.md index 0fd9d5e..741950f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,5 +1,5 @@ # WP Packages Contents: -- [Editor Tools](./editor-tools/index.md) +- [Editor Tools](../packages/editor-tools/README.md) - [Iconography](./iconography/index.md) \ No newline at end of file diff --git a/packages/editor-tools/README.md b/packages/editor-tools/README.md index 9acae6f..7b0aea9 100644 --- a/packages/editor-tools/README.md +++ b/packages/editor-tools/README.md @@ -1,3 +1,47 @@ +# Box WordPress Editor Tools + +A collection of tools for modifying the WordPress Editor. + +## Quick Start! + +All the tools load automatically if you've installed this as a plugin to your WordPress site. If not, you can use each class as needed. + +There's more options for each tool, so checkout the links below: + +## Features + + - [Asset Loader](./docs/AssetLoader.md) - help load assets generated by [wp-scripts](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-scripts/). + - [Block Loader](./docs/BlockLoader.md) - auto-loads blocks to the editor. + - [Comment Disablement](./docs/Comments.md) - disables comments. + - [Editor Cleanup](./docs/EditorCleanup.md) - removes some unnecessary bits from the block editor. + - [Post Type Registrations](./docs/PostTypes.md) - speeds up post-type registration with a single JSON file. + - [Template Persistence](./docs/TemplatePersistence.md) - speeds up template modifications by saving to disk instead of the database. + - [Security](./docs/Security.md) - Adds security hardening. + +## Contributing + +# Box WordPress Editor Tools + +A collection of tools for modifying the WordPress Editor. + +## Quick Start! + +All the tools load automatically if you include this plugin in your WP installation, or you can access the classes independently if you don't want to use this library as a plugin. + +There's more options too, so checkout the links below: + +## Features + + - [Asset Loader](./AssetLoader.md) - help load assets generated by [wp-scripts](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-scripts/). + - [Block Loader](./BlockLoader.md) - auto-loads blocks to the editor. + - [Comment Disablement](./Comments.md) - disables comments. + - [Editor Cleanup](./EditorCleanup.md) - removes some unnecessary bits from the block editor. + - [Post Type Registrations](./PostTypes.md) - speeds up post-type registration with a single JSON file. + - [Template Persistence](./TemplatePersistence.md) - speeds up template modifications by saving to disk instead of the database. + - [Security](./Security.md) - Adds security hardening. + +## Contributing + Please do not submit any Pull Requests here. They will be closed. --- @@ -5,3 +49,5 @@ Please submit your PR here instead: https://github.com/boxuk/wp-packages This repository is what we call a "subtree split": a read-only subset of that main repository. We're looking forward to your PR there! + + diff --git a/docs/editor-tools/AssetLoader.md b/packages/editor-tools/docs/AssetLoader.md similarity index 100% rename from docs/editor-tools/AssetLoader.md rename to packages/editor-tools/docs/AssetLoader.md diff --git a/packages/editor-tools/docs/BlockLoader.md b/packages/editor-tools/docs/BlockLoader.md new file mode 100644 index 0000000..01b0173 --- /dev/null +++ b/packages/editor-tools/docs/BlockLoader.md @@ -0,0 +1,16 @@ +# Block Loader + +The Block Loader will automatically load all blocks by looking for `block.json` files in a given path. These are loaded with `register_block_type`, see [WordPress Docs](https://developer.wordpress.org/reference/functions/register_block_type/) for more context. + +## Usage +Block loader is included automatically, and defaults to loading from your `wp-content/theme/THEME_NAME/build` folder. +You can change this value using the `boxuk_block_loader_base_path` filter. + +```php +add_filter( + 'boxuk_block_loader_base_path', + function ( string $default_path ): string { + return __DIR__ . '/my-special-path'; + } +); +``` diff --git a/packages/editor-tools/docs/Comments.md b/packages/editor-tools/docs/Comments.md new file mode 100644 index 0000000..cb68485 --- /dev/null +++ b/packages/editor-tools/docs/Comments.md @@ -0,0 +1,11 @@ +# Comment Disablement + +This will remove the ability for comments from your WP installation. + +## Usage + +Comments are disbaled by default, but can be enabled by modifing the `boxuk_disable_comments` filter: + +```php +add_filter( 'boxuk_disable_comments', '__return_false' ); +``` diff --git a/docs/editor-tools/EditorCleanup.md b/packages/editor-tools/docs/EditorCleanup.md similarity index 78% rename from docs/editor-tools/EditorCleanup.md rename to packages/editor-tools/docs/EditorCleanup.md index ede8e8d..c86ccdd 100644 --- a/docs/editor-tools/EditorCleanup.md +++ b/packages/editor-tools/docs/EditorCleanup.md @@ -5,12 +5,4 @@ This will hide some additional features added to the block editor: - **WordPress VIP Featured Plugins**: WordPress VIP adds a banner to highlight featured plugins for their platform. Plugins are managed by the developer so this should be hidden. ## Usage -The `init` method should be hooked early in the WordPress Lifecycle -```php -add_action( - 'plugins_loaded', - function () { - ( new Boxuk\BoxWpEditorTools\EditorCleanup() )->init(); - } -); -``` +The editor cleanup is enabled automatically and does not have any configurable options. diff --git a/docs/editor-tools/PostTypes.md b/packages/editor-tools/docs/PostTypes.md similarity index 84% rename from docs/editor-tools/PostTypes.md rename to packages/editor-tools/docs/PostTypes.md index 29cfd17..625da29 100644 --- a/docs/editor-tools/PostTypes.md +++ b/packages/editor-tools/docs/PostTypes.md @@ -3,21 +3,12 @@ A shorthand for registering post types via a JSON file instead of a whole bunch of PHP to make life just a little bit easier. ## Usage - -You need to initialise the class as early as possible. -```php -add_ation( - 'plugins_loaded', - function() { - ( new Boxuk\BoxWpEditorTools\PostTypes() )->init(); - } -); -``` +This feature is enabled automatically with the plugin. You will also need to define a JSON file which configures your post-types. This should be caleld `post-types.json` and live in the root of your theme (`wp-content/themes/{theme_name}/post-types.json`). ```json { - "$schema": "https://github.com/boxuk/box-wp-editor-tools/post-type.schema.json", + "$schema": "https://raw.githubusercontent.com/boxuk/wp-packages/main/schema/post-type.schema.json", "post_types": { "project": { "has_archive": true, @@ -40,6 +31,16 @@ You will also need to define a JSON file which configures your post-types. This } ``` +You can modify the path of this file using the `boxuk_post_types_json_file_path` filter. +```php +add_filter( + 'boxuk_post_types_json_file_path', + function ( string $default_path ): string { + return __DIR__ . '/my-path-to/post-types.json'; + } +); +``` + The schema file will help your IDE populate any fields you think you need. After parsing, the file is passed to `register_post_type()` so the [WordPress documentation](https://developer.wordpress.org/reference/functions/register_post_type/) should be useful! diff --git a/packages/editor-tools/docs/Security.md b/packages/editor-tools/docs/Security.md new file mode 100644 index 0000000..cc44466 --- /dev/null +++ b/packages/editor-tools/docs/Security.md @@ -0,0 +1,29 @@ +# Security + +Some security hardening features are available for you to use. + +## Usage +The security hardening features will load automatically with sensible defaults, but there's a number of filters you can use to configure. + +## Filters + +| Filter Name | Description | Default Value | +| ----------- | ----------- | ------------- | +| `boxuk_prevent_author_enum` | Prevents access to the author-archive pages | `true` | +| `boxuk_prevent_author_rest_endpoint` | Prevents access to the author API endpoint | `true` | +| `boxuk_send_no_sniff_headers` | Sends `nosniff` and `frame_options` headers | `true` | +| `boxuk_remove_vip_headers` | Removes `X-Hacker` and `X-Powered-By` headers | `true` | +| `boxuk_validate_password` | Enforces strong password validations | `true` | +| `boxuk_restrict_http_request_methods` | Restricts HTTP methods to the known list | `true` | +| `boxuk_disable_rss` | Disables the RSS functionality | `true` | +| `boxuk_modify_session_timeout` | Modifies the default session period to 10 hours | `true` | +| `boxuk_restrict_user_creation` | Restricts creating users in the admin interface | `false` | +| `boxuk_restrict_login_by_username` | Restricts logging in by username (allows by email only) | `true` | + + +Filters should be added at the earliest possible point, so avoid adding via a hook (ie don't add to `init` you might be too late). WordPress includes helpful return true/false methods to make it super easy to configure. + +```php +add_filter( 'example_filter', '__return_true' ); +add_filter( 'example_filter', '__return_false' ); +``` diff --git a/docs/editor-tools/TemplatePersistence.md b/packages/editor-tools/docs/TemplatePersistence.md similarity index 71% rename from docs/editor-tools/TemplatePersistence.md rename to packages/editor-tools/docs/TemplatePersistence.md index 457782c..c04195c 100644 --- a/docs/editor-tools/TemplatePersistence.md +++ b/packages/editor-tools/docs/TemplatePersistence.md @@ -4,15 +4,19 @@ One of the biggest annoyances about developing block-based themes is that if you This change saves your edits to templates to their respective `.html` file. This applies to all `wp_template`, so anything accessed via **Templates** in the site-editor. ## Usage -You need to enable usage of Template Persistence. It's likely you only want to load this on local developer environments. +Template persistence is enabled automatically, but can be disabled by passing a value to the `boxuk_disable_template_persistence` filter: + ```php -add_action( - 'plugins_loaded', - function () { - if ( 'local' === wp_get_environment_type() ) { - ( new Boxuk\BoxWpEditorTools\TemplatePersistence() )->init(); - } +add_filter( 'boxuk_disable_template_persistence', '__return_true' ); +``` + +Since local environments can vary so much, we have not included any checks in the template persistence class to validate which environment is used. See [docs for `wp_get_environment_type()`](https://developer.wordpress.org/reference/functions/wp_get_environment_type/) for the above example code. + +```php +add_filter( + 'boxuk_disable_template_persistence' + function ( bool $default ): bool { + return wp_get_environment_type() === 'local'; } ); ``` -Since local environments can vary so much, we have not included any checks in the template persistence class to validate which environment is used. See [docs for `wp_get_environment_type()`](https://developer.wordpress.org/reference/functions/wp_get_environment_type/) for the above example code. 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 ), ); } }