From c60cfe92b4a08ee0391f9361d5cd76b738392cb8 Mon Sep 17 00:00:00 2001 From: Joe Dolson Date: Thu, 17 Aug 2023 20:27:35 +0000 Subject: [PATCH] Administration: Add function to standardize admin notices. Add functions `wp_get_admin_notice()` and `wp_admin_notice()` to create and output admin notices & tests for usage. New functions accept a message and array of optional arguments. This commit does not implement the functions. Include new filters: `wp_admin_notice_args`, `wp_admin_notice_markup` and action: `wp_admin_notice`. Props joedolson, costdev, sakibmd, dasnitesh780, sabernhardt. Fixes #57791. git-svn-id: https://develop.svn.wordpress.org/trunk@56408 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/includes/misc.php | 128 +++++++++ tests/phpunit/tests/admin/wpAdminNotice.php | 261 +++++++++++++++++ .../phpunit/tests/admin/wpGetAdminNotice.php | 268 ++++++++++++++++++ 3 files changed, 657 insertions(+) create mode 100644 tests/phpunit/tests/admin/wpAdminNotice.php create mode 100644 tests/phpunit/tests/admin/wpGetAdminNotice.php diff --git a/src/wp-admin/includes/misc.php b/src/wp-admin/includes/misc.php index 34ced18df5..24cd26d661 100644 --- a/src/wp-admin/includes/misc.php +++ b/src/wp-admin/includes/misc.php @@ -1642,3 +1642,131 @@ function wp_check_php_version() { return $response; } + +/** + * Creates and returns the markup for an admin notice. + * + * @since 6.4.0 + * + * @param string $message The message. + * @param array $args { + * Optional. An array of arguments for the admin notice. Default empty array. + * + * @type string $type Optional. The type of admin notice. + * For example, 'error', 'success', 'warning', 'info'. + * Default empty string. + * @type bool $dismissible Optional. Whether the admin notice is dismissible. Default false. + * @type string $id Optional. The value of the admin notice's ID attribute. Default empty string. + * @type string[] $additional_classes Optional. A string array of class names. Default empty array. + * @type bool $paragraph_wrap Optional. Whether to wrap the message in paragraph tags. Default true. + * } + * @return string The markup for an admin notice. + */ +function wp_get_admin_notice( $message, $args = array() ) { + $defaults = array( + 'type' => '', + 'dismissible' => false, + 'id' => '', + 'additional_classes' => array(), + 'paragraph_wrap' => true, + ); + + $args = wp_parse_args( $args, $defaults ); + + /** + * Filters the arguments for an admin notice. + * + * @since 6.4.0 + * + * @param array $args The arguments for the admin notice. + * @param string $message The message for the admin notice. + */ + $args = apply_filters( 'wp_admin_notice_args', $args, $message ); + $id = ''; + $classes = 'notice'; + + if ( is_string( $args['id'] ) ) { + $trimmed_id = trim( $args['id'] ); + + if ( '' !== $trimmed_id ) { + $id = 'id="' . $trimmed_id . '" '; + } + } + + if ( is_string( $args['type'] ) ) { + $type = trim( $args['type'] ); + + if ( str_contains( $type, ' ' ) ) { + _doing_it_wrong( + __FUNCTION__, + sprintf( + /* translators: %s: The "type" key. */ + __( 'The %s key must be a string without spaces.' ), + 'type' + ), + '6.4.0' + ); + } + + if ( '' !== $type ) { + $classes .= ' notice-' . $type; + } + } + + if ( true === $args['dismissible'] ) { + $classes .= ' is-dismissible'; + } + + if ( is_array( $args['additional_classes'] ) && ! empty( $args['additional_classes'] ) ) { + $classes .= ' ' . implode( ' ', $args['additional_classes'] ); + } + + if ( false !== $args['paragraph_wrap'] ) { + $message = "

$message

"; + } + + $markup = sprintf( '
%3$s
', $id, $classes, $message ); + + /** + * Filters the markup for an admin notice. + * + * @since 6.4.0 + * + * @param string $markup The HTML markup for the admin notice. + * @param string $message The message for the admin notice. + * @param array $args The arguments for the admin notice. + */ + return apply_filters( 'wp_admin_notice_markup', $markup, $message, $args ); +} + +/** + * Outputs an admin notice. + * + * @since 6.4.0 + * + * @param string $message The message to output. + * @param array $args { + * Optional. An array of arguments for the admin notice. Default empty array. + * + * @type string $type Optional. The type of admin notice. + * For example, 'error', 'success', 'warning', 'info'. + * Default empty string. + * @type bool $dismissible Optional. Whether the admin notice is dismissible. Default false. + * @type string $id Optional. The value of the admin notice's ID attribute. Default empty string. + * @type string[] $additional_classes Optional. A string array of class names. Default empty array. + * @type bool $paragraph_wrap Optional. Whether to wrap the message in paragraph tags. Default true. + * } + */ +function wp_admin_notice( $message, $args = array() ) { + /** + * Fires before an admin notice is output. + * + * @since 6.4.0 + * + * @param string $message The message for the admin notice. + * @param array $args The arguments for the admin notice. + */ + do_action( 'wp_admin_notice', $message, $args ); + + echo wp_kses_post( wp_get_admin_notice( $message, $args ) ); +} diff --git a/tests/phpunit/tests/admin/wpAdminNotice.php b/tests/phpunit/tests/admin/wpAdminNotice.php new file mode 100644 index 0000000000..2feca7de7c --- /dev/null +++ b/tests/phpunit/tests/admin/wpAdminNotice.php @@ -0,0 +1,261 @@ +assertSame( $expected, $actual ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_output_admin_notice() { + return array( + 'defaults' => array( + 'message' => 'A notice with defaults.', + 'args' => array(), + 'expected' => '

A notice with defaults.

', + ), + 'an empty message (used for templates)' => array( + 'message' => '', + 'args' => array( + 'type' => 'error', + 'dismissible' => true, + 'id' => 'message', + 'additional_classes' => array( 'inline', 'hidden' ), + ), + 'expected' => '', + ), + 'an empty message (used for templates) without paragraph wrapping' => array( + 'message' => '', + 'args' => array( + 'type' => 'error', + 'dismissible' => true, + 'id' => 'message', + 'additional_classes' => array( 'inline', 'hidden' ), + 'paragraph_wrap' => false, + ), + 'expected' => '', + ), + 'an "error" notice' => array( + 'message' => 'An "error" notice.', + 'args' => array( + 'type' => 'error', + ), + 'expected' => '

An "error" notice.

', + ), + 'a "success" notice' => array( + 'message' => 'A "success" notice.', + 'args' => array( + 'type' => 'success', + ), + 'expected' => '

A "success" notice.

', + ), + 'a "warning" notice' => array( + 'message' => 'A "warning" notice.', + 'args' => array( + 'type' => 'warning', + ), + 'expected' => '

A "warning" notice.

', + ), + 'an "info" notice' => array( + 'message' => 'An "info" notice.', + 'args' => array( + 'type' => 'info', + ), + 'expected' => '

An "info" notice.

', + ), + 'a type that already starts with "notice-"' => array( + 'message' => 'A type that already starts with "notice-".', + 'args' => array( + 'type' => 'notice-info', + ), + 'expected' => '

A type that already starts with "notice-".

', + ), + 'a dismissible notice' => array( + 'message' => 'A dismissible notice.', + 'args' => array( + 'dismissible' => true, + ), + 'expected' => '

A dismissible notice.

', + ), + 'no type and an ID' => array( + 'message' => 'A notice with an ID.', + 'args' => array( + 'id' => 'message', + ), + 'expected' => '

A notice with an ID.

', + ), + 'a type and an ID' => array( + 'message' => 'A warning notice with an ID.', + 'args' => array( + 'type' => 'warning', + 'id' => 'message', + ), + 'expected' => '

A warning notice with an ID.

', + ), + 'no type and additional classes' => array( + 'message' => 'A notice with additional classes.', + 'args' => array( + 'additional_classes' => array( 'error', 'notice-alt' ), + ), + 'expected' => '

A notice with additional classes.

', + ), + 'a type and additional classes' => array( + 'message' => 'A warning notice with additional classes.', + 'args' => array( + 'type' => 'warning', + 'additional_classes' => array( 'error', 'notice-alt' ), + ), + 'expected' => '

A warning notice with additional classes.

', + ), + 'a dismissible notice with a type and additional classes' => array( + 'message' => 'A dismissible warning notice with a type and additional classes.', + 'args' => array( + 'type' => 'warning', + 'dismissible' => true, + 'additional_classes' => array( 'error', 'notice-alt' ), + ), + 'expected' => '

A dismissible warning notice with a type and additional classes.

', + ), + 'a notice without paragraph wrapping' => array( + 'message' => 'A notice without paragraph wrapping.', + 'args' => array( + 'paragraph_wrap' => false, + ), + 'expected' => '
A notice without paragraph wrapping.
', + ), + 'an unsafe type' => array( + 'message' => 'A notice with an unsafe type.', + 'args' => array( + 'type' => '">', + ), + 'expected' => '
alert("Howdy,admin!");">

A notice with an unsafe type.

', + ), + 'an unsafe ID' => array( + 'message' => 'A notice with an unsafe ID.', + 'args' => array( + 'id' => '">
alert( "Howdy, admin!" );

A notice with an unsafe ID.

', + ), + 'unsafe additional classes' => array( + 'message' => 'A notice with unsafe additional classes.', + 'args' => array( + 'additional_classes' => array( '">
alert( "Howdy, admin!" );

A notice with unsafe additional classes.

', + ), + 'a type that is not a string' => array( + 'message' => 'A notice with a type that is not a string.', + 'args' => array( + 'type' => array(), + ), + 'expected' => '

A notice with a type that is not a string.

', + ), + 'a type with only empty space' => array( + 'message' => 'A notice with a type with only empty space.', + 'args' => array( + 'type' => " \t\r\n", + ), + 'expected' => '

A notice with a type with only empty space.

', + ), + 'an ID that is not a string' => array( + 'message' => 'A notice with an ID that is not a string.', + 'args' => array( + 'id' => array( 'message' ), + ), + 'expected' => '

A notice with an ID that is not a string.

', + ), + 'an ID with only empty space' => array( + 'message' => 'A notice with an ID with only empty space.', + 'args' => array( + 'id' => " \t\r\n", + ), + 'expected' => '

A notice with an ID with only empty space.

', + ), + 'dismissible as a truthy value rather than (bool) true' => array( + 'message' => 'A notice with dismissible as a truthy value rather than (bool) true.', + 'args' => array( + 'dismissible' => 1, + ), + 'expected' => '

A notice with dismissible as a truthy value rather than (bool) true.

', + ), + 'additional classes that are not an array' => array( + 'message' => 'A notice with additional classes that are not an array.', + 'args' => array( + 'additional_classes' => 'class-1 class-2 class-3', + ), + 'expected' => '

A notice with additional classes that are not an array.

', + ), + 'paragraph wrapping as a falsy value rather than (bool) false' => array( + 'message' => 'A notice with paragraph wrapping as a falsy value rather than (bool) false.', + 'args' => array( + 'paragraph_wrap' => 0, + ), + 'expected' => '

A notice with paragraph wrapping as a falsy value rather than (bool) false.

', + ), + ); + } + + /** + * Tests that `_doing_it_wrong()` is thrown when a 'type' containing spaces is passed. + * + * @ticket 57791 + * + * @expectedIncorrectUsage wp_get_admin_notice + */ + public function test_should_throw_doing_it_wrong_with_a_type_containing_spaces() { + ob_start(); + wp_admin_notice( + 'A type containing spaces.', + array( 'type' => 'first second third fourth' ) + ); + $actual = ob_get_clean(); + + $this->assertSame( + '

A type containing spaces.

', + $actual + ); + } + + /** + * Tests that `wp_admin_notice()` fires the 'wp_admin_notice' action. + * + * @ticket 57791 + */ + public function test_should_fire_wp_admin_notice_action() { + $action = new MockAction(); + add_action( 'wp_admin_notice', array( $action, 'action' ) ); + + ob_start(); + wp_admin_notice( 'A notice.', array( 'type' => 'success' ) ); + ob_end_clean(); + + $this->assertSame( 1, $action->get_call_count() ); + } +} diff --git a/tests/phpunit/tests/admin/wpGetAdminNotice.php b/tests/phpunit/tests/admin/wpGetAdminNotice.php new file mode 100644 index 0000000000..b5ba311338 --- /dev/null +++ b/tests/phpunit/tests/admin/wpGetAdminNotice.php @@ -0,0 +1,268 @@ +assertSame( $expected, wp_get_admin_notice( $message, $args ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_return_admin_notice() { + return array( + 'defaults' => array( + 'message' => 'A notice with defaults.', + 'args' => array(), + 'expected' => '

A notice with defaults.

', + ), + 'an empty message (used for templates)' => array( + 'message' => '', + 'args' => array( + 'type' => 'error', + 'dismissible' => true, + 'id' => 'message', + 'additional_classes' => array( 'inline', 'hidden' ), + ), + 'expected' => '', + ), + 'an empty message (used for templates) without paragraph wrapping' => array( + 'message' => '', + 'args' => array( + 'type' => 'error', + 'dismissible' => true, + 'id' => 'message', + 'additional_classes' => array( 'inline', 'hidden' ), + 'paragraph_wrap' => false, + ), + 'expected' => '', + ), + 'an "error" notice' => array( + 'message' => 'An "error" notice.', + 'args' => array( + 'type' => 'error', + ), + 'expected' => '

An "error" notice.

', + ), + 'a "success" notice' => array( + 'message' => 'A "success" notice.', + 'args' => array( + 'type' => 'success', + ), + 'expected' => '

A "success" notice.

', + ), + 'a "warning" notice' => array( + 'message' => 'A "warning" notice.', + 'args' => array( + 'type' => 'warning', + ), + 'expected' => '

A "warning" notice.

', + ), + 'an "info" notice' => array( + 'message' => 'An "info" notice.', + 'args' => array( + 'type' => 'info', + ), + 'expected' => '

An "info" notice.

', + ), + 'a type that already starts with "notice-"' => array( + 'message' => 'A type that already starts with "notice-".', + 'args' => array( + 'type' => 'notice-info', + ), + 'expected' => '

A type that already starts with "notice-".

', + ), + 'a dismissible notice' => array( + 'message' => 'A dismissible notice.', + 'args' => array( + 'dismissible' => true, + ), + 'expected' => '

A dismissible notice.

', + ), + 'no type and an ID' => array( + 'message' => 'A notice with an ID.', + 'args' => array( + 'id' => 'message', + ), + 'expected' => '

A notice with an ID.

', + ), + 'a type and an ID' => array( + 'message' => 'A warning notice with an ID.', + 'args' => array( + 'type' => 'warning', + 'id' => 'message', + ), + 'expected' => '

A warning notice with an ID.

', + ), + 'no type and additional classes' => array( + 'message' => 'A notice with additional classes.', + 'args' => array( + 'additional_classes' => array( 'error', 'notice-alt' ), + ), + 'expected' => '

A notice with additional classes.

', + ), + 'a type and additional classes' => array( + 'message' => 'A warning notice with additional classes.', + 'args' => array( + 'type' => 'warning', + 'additional_classes' => array( 'error', 'notice-alt' ), + ), + 'expected' => '

A warning notice with additional classes.

', + ), + 'a dismissible notice with a type and additional classes' => array( + 'message' => 'A dismissible warning notice with a type and additional classes.', + 'args' => array( + 'type' => 'warning', + 'dismissible' => true, + 'additional_classes' => array( 'error', 'notice-alt' ), + ), + 'expected' => '

A dismissible warning notice with a type and additional classes.

', + ), + 'a notice without paragraph wrapping' => array( + 'message' => 'A notice without paragraph wrapping.', + 'args' => array( + 'paragraph_wrap' => false, + ), + 'expected' => '
A notice without paragraph wrapping.
', + ), + 'an unsafe type' => array( + 'message' => 'A notice with an unsafe type.', + 'args' => array( + 'type' => '">', + ), + 'expected' => '
">

A notice with an unsafe type.

', + ), + 'an unsafe ID' => array( + 'message' => 'A notice with an unsafe ID.', + 'args' => array( + 'id' => '">

A notice with an unsafe ID.

', + ), + 'unsafe additional classes' => array( + 'message' => 'A notice with unsafe additional classes.', + 'args' => array( + 'additional_classes' => array( '">

A notice with unsafe additional classes.

', + ), + 'a type that is not a string' => array( + 'message' => 'A notice with a type that is not a string.', + 'args' => array( + 'type' => array(), + ), + 'expected' => '

A notice with a type that is not a string.

', + ), + 'a type with only empty space' => array( + 'message' => 'A notice with a type with only empty space.', + 'args' => array( + 'type' => " \t\r\n", + ), + 'expected' => '

A notice with a type with only empty space.

', + ), + 'an ID that is not a string' => array( + 'message' => 'A notice with an ID that is not a string.', + 'args' => array( + 'id' => array( 'message' ), + ), + 'expected' => '

A notice with an ID that is not a string.

', + ), + 'an ID with only empty space' => array( + 'message' => 'A notice with an ID with only empty space.', + 'args' => array( + 'id' => " \t\r\n", + ), + 'expected' => '

A notice with an ID with only empty space.

', + ), + 'dismissible as a truthy value rather than (bool) true' => array( + 'message' => 'A notice with dismissible as a truthy value rather than (bool) true.', + 'args' => array( + 'dismissible' => 1, + ), + 'expected' => '

A notice with dismissible as a truthy value rather than (bool) true.

', + ), + 'additional classes that are not an array' => array( + 'message' => 'A notice with additional classes that are not an array.', + 'args' => array( + 'additional_classes' => 'class-1 class-2 class-3', + ), + 'expected' => '

A notice with additional classes that are not an array.

', + ), + 'paragraph wrapping as a falsy value rather than (bool) false' => array( + 'message' => 'A notice with paragraph wrapping as a falsy value rather than (bool) false.', + 'args' => array( + 'paragraph_wrap' => 0, + ), + 'expected' => '

A notice with paragraph wrapping as a falsy value rather than (bool) false.

', + ), + ); + } + + /** + * Tests that `wp_get_admin_notice()` throws a `_doing_it_wrong()` when + * a 'type' containing spaces is passed. + * + * @ticket 57791 + * + * @expectedIncorrectUsage wp_get_admin_notice + */ + public function test_should_throw_doing_it_wrong_with_a_type_containing_spaces() { + $this->assertSame( + '

A type containing spaces.

', + wp_get_admin_notice( + 'A type containing spaces.', + array( 'type' => 'first second third fourth' ) + ) + ); + } + + /** + * Tests that `wp_get_admin_notice()` applies filters. + * + * @ticket 57791 + * + * @dataProvider data_should_apply_filters + * + * @param string $hook_name The name of the filter hook. + */ + public function test_should_apply_filters( $hook_name ) { + $filter = new MockAction(); + add_filter( $hook_name, array( $filter, 'filter' ) ); + + wp_get_admin_notice( 'A notice.', array( 'type' => 'success' ) ); + + $this->assertSame( 1, $filter->get_call_count() ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_apply_filters() { + return array( + 'wp_admin_notice_args' => array( 'hook_name' => 'wp_admin_notice_args' ), + 'wp_admin_notice_markup' => array( 'hook_name' => 'wp_admin_notice_markup' ), + ); + } +}