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
This commit is contained in:
Joe Dolson
2023-08-17 20:27:35 +00:00
parent 5b517ae3a6
commit c60cfe92b4
3 changed files with 657 additions and 0 deletions

View File

@@ -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.' ),
'<code>type</code>'
),
'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 = "<p>$message</p>";
}
$markup = sprintf( '<div %1$sclass="%2$s">%3$s</div>', $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 ) );
}

View File

@@ -0,0 +1,261 @@
<?php
/**
* Tests for `wp_admin_notice()`.
*
* @group admin
*
* @covers ::wp_admin_notice
*/
class Tests_Admin_WpAdminNotice extends WP_UnitTestCase {
/**
* Tests that `wp_admin_notice()` outputs the expected admin notice markup.
*
* @ticket 57791
*
* @dataProvider data_should_output_admin_notice
*
* @param string $message The message to output.
* @param array $args Arguments for the admin notice.
* @param string $expected The expected admin notice markup.
*/
public function test_should_output_admin_notice( $message, $args, $expected ) {
ob_start();
wp_admin_notice( $message, $args );
$actual = ob_get_clean();
$this->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' => '<div class="notice"><p>A notice with defaults.</p></div>',
),
'an empty message (used for templates)' => array(
'message' => '',
'args' => array(
'type' => 'error',
'dismissible' => true,
'id' => 'message',
'additional_classes' => array( 'inline', 'hidden' ),
),
'expected' => '<div id="message" class="notice notice-error is-dismissible inline hidden"><p></p></div>',
),
'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' => '<div id="message" class="notice notice-error is-dismissible inline hidden"></div>',
),
'an "error" notice' => array(
'message' => 'An "error" notice.',
'args' => array(
'type' => 'error',
),
'expected' => '<div class="notice notice-error"><p>An "error" notice.</p></div>',
),
'a "success" notice' => array(
'message' => 'A "success" notice.',
'args' => array(
'type' => 'success',
),
'expected' => '<div class="notice notice-success"><p>A "success" notice.</p></div>',
),
'a "warning" notice' => array(
'message' => 'A "warning" notice.',
'args' => array(
'type' => 'warning',
),
'expected' => '<div class="notice notice-warning"><p>A "warning" notice.</p></div>',
),
'an "info" notice' => array(
'message' => 'An "info" notice.',
'args' => array(
'type' => 'info',
),
'expected' => '<div class="notice notice-info"><p>An "info" notice.</p></div>',
),
'a type that already starts with "notice-"' => array(
'message' => 'A type that already starts with "notice-".',
'args' => array(
'type' => 'notice-info',
),
'expected' => '<div class="notice notice-notice-info"><p>A type that already starts with "notice-".</p></div>',
),
'a dismissible notice' => array(
'message' => 'A dismissible notice.',
'args' => array(
'dismissible' => true,
),
'expected' => '<div class="notice is-dismissible"><p>A dismissible notice.</p></div>',
),
'no type and an ID' => array(
'message' => 'A notice with an ID.',
'args' => array(
'id' => 'message',
),
'expected' => '<div id="message" class="notice"><p>A notice with an ID.</p></div>',
),
'a type and an ID' => array(
'message' => 'A warning notice with an ID.',
'args' => array(
'type' => 'warning',
'id' => 'message',
),
'expected' => '<div id="message" class="notice notice-warning"><p>A warning notice with an ID.</p></div>',
),
'no type and additional classes' => array(
'message' => 'A notice with additional classes.',
'args' => array(
'additional_classes' => array( 'error', 'notice-alt' ),
),
'expected' => '<div class="notice error notice-alt"><p>A notice with additional classes.</p></div>',
),
'a type and additional classes' => array(
'message' => 'A warning notice with additional classes.',
'args' => array(
'type' => 'warning',
'additional_classes' => array( 'error', 'notice-alt' ),
),
'expected' => '<div class="notice notice-warning error notice-alt"><p>A warning notice with additional classes.</p></div>',
),
'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' => '<div class="notice notice-warning is-dismissible error notice-alt"><p>A dismissible warning notice with a type and additional classes.</p></div>',
),
'a notice without paragraph wrapping' => array(
'message' => '<span>A notice without paragraph wrapping.</span>',
'args' => array(
'paragraph_wrap' => false,
),
'expected' => '<div class="notice"><span>A notice without paragraph wrapping.</span></div>',
),
'an unsafe type' => array(
'message' => 'A notice with an unsafe type.',
'args' => array(
'type' => '"><script>alert("Howdy,admin!");</script>',
),
'expected' => '<div class="notice notice-">alert("Howdy,admin!");"&gt;<p>A notice with an unsafe type.</p></div>',
),
'an unsafe ID' => array(
'message' => 'A notice with an unsafe ID.',
'args' => array(
'id' => '"><script>alert( "Howdy, admin!" );</script> <div class="notice',
),
'expected' => '<div id="">alert( "Howdy, admin!" ); <div class="notice"><p>A notice with an unsafe ID.</p></div>',
),
'unsafe additional classes' => array(
'message' => 'A notice with unsafe additional classes.',
'args' => array(
'additional_classes' => array( '"><script>alert( "Howdy, admin!" );</script> <div class="notice' ),
),
'expected' => '<div class="notice ">alert( "Howdy, admin!" ); <div class="notice"><p>A notice with unsafe additional classes.</p></div>',
),
'a type that is not a string' => array(
'message' => 'A notice with a type that is not a string.',
'args' => array(
'type' => array(),
),
'expected' => '<div class="notice"><p>A notice with a type that is not a string.</p></div>',
),
'a type with only empty space' => array(
'message' => 'A notice with a type with only empty space.',
'args' => array(
'type' => " \t\r\n",
),
'expected' => '<div class="notice"><p>A notice with a type with only empty space.</p></div>',
),
'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' => '<div class="notice"><p>A notice with an ID that is not a string.</p></div>',
),
'an ID with only empty space' => array(
'message' => 'A notice with an ID with only empty space.',
'args' => array(
'id' => " \t\r\n",
),
'expected' => '<div class="notice"><p>A notice with an ID with only empty space.</p></div>',
),
'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' => '<div class="notice"><p>A notice with dismissible as a truthy value rather than (bool) true.</p></div>',
),
'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' => '<div class="notice"><p>A notice with additional classes that are not an array.</p></div>',
),
'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' => '<div class="notice"><p>A notice with paragraph wrapping as a falsy value rather than (bool) false.</p></div>',
),
);
}
/**
* 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(
'<div class="notice notice-first second third fourth"><p>A type containing spaces.</p></div>',
$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() );
}
}

View File

@@ -0,0 +1,268 @@
<?php
/**
* Tests for `wp_get_admin_notice()`.
*
* @group admin
*
* @covers ::wp_get_admin_notice
*/
class Tests_Admin_WpGetAdminNotice extends WP_UnitTestCase {
/**
* Tests that `wp_get_admin_notice()` returns the expected admin notice markup.
*
* @ticket 57791
*
* @dataProvider data_should_return_admin_notice
*
* @param string $message The message.
* @param array $args Arguments for the admin notice.
* @param string $expected The expected admin notice markup.
*/
public function test_should_return_admin_notice( $message, $args, $expected ) {
$this->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' => '<div class="notice"><p>A notice with defaults.</p></div>',
),
'an empty message (used for templates)' => array(
'message' => '',
'args' => array(
'type' => 'error',
'dismissible' => true,
'id' => 'message',
'additional_classes' => array( 'inline', 'hidden' ),
),
'expected' => '<div id="message" class="notice notice-error is-dismissible inline hidden"><p></p></div>',
),
'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' => '<div id="message" class="notice notice-error is-dismissible inline hidden"></div>',
),
'an "error" notice' => array(
'message' => 'An "error" notice.',
'args' => array(
'type' => 'error',
),
'expected' => '<div class="notice notice-error"><p>An "error" notice.</p></div>',
),
'a "success" notice' => array(
'message' => 'A "success" notice.',
'args' => array(
'type' => 'success',
),
'expected' => '<div class="notice notice-success"><p>A "success" notice.</p></div>',
),
'a "warning" notice' => array(
'message' => 'A "warning" notice.',
'args' => array(
'type' => 'warning',
),
'expected' => '<div class="notice notice-warning"><p>A "warning" notice.</p></div>',
),
'an "info" notice' => array(
'message' => 'An "info" notice.',
'args' => array(
'type' => 'info',
),
'expected' => '<div class="notice notice-info"><p>An "info" notice.</p></div>',
),
'a type that already starts with "notice-"' => array(
'message' => 'A type that already starts with "notice-".',
'args' => array(
'type' => 'notice-info',
),
'expected' => '<div class="notice notice-notice-info"><p>A type that already starts with "notice-".</p></div>',
),
'a dismissible notice' => array(
'message' => 'A dismissible notice.',
'args' => array(
'dismissible' => true,
),
'expected' => '<div class="notice is-dismissible"><p>A dismissible notice.</p></div>',
),
'no type and an ID' => array(
'message' => 'A notice with an ID.',
'args' => array(
'id' => 'message',
),
'expected' => '<div id="message" class="notice"><p>A notice with an ID.</p></div>',
),
'a type and an ID' => array(
'message' => 'A warning notice with an ID.',
'args' => array(
'type' => 'warning',
'id' => 'message',
),
'expected' => '<div id="message" class="notice notice-warning"><p>A warning notice with an ID.</p></div>',
),
'no type and additional classes' => array(
'message' => 'A notice with additional classes.',
'args' => array(
'additional_classes' => array( 'error', 'notice-alt' ),
),
'expected' => '<div class="notice error notice-alt"><p>A notice with additional classes.</p></div>',
),
'a type and additional classes' => array(
'message' => 'A warning notice with additional classes.',
'args' => array(
'type' => 'warning',
'additional_classes' => array( 'error', 'notice-alt' ),
),
'expected' => '<div class="notice notice-warning error notice-alt"><p>A warning notice with additional classes.</p></div>',
),
'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' => '<div class="notice notice-warning is-dismissible error notice-alt"><p>A dismissible warning notice with a type and additional classes.</p></div>',
),
'a notice without paragraph wrapping' => array(
'message' => '<span>A notice without paragraph wrapping.</span>',
'args' => array(
'paragraph_wrap' => false,
),
'expected' => '<div class="notice"><span>A notice without paragraph wrapping.</span></div>',
),
'an unsafe type' => array(
'message' => 'A notice with an unsafe type.',
'args' => array(
'type' => '"><script>alert("Howdy,admin!");</script>',
),
'expected' => '<div class="notice notice-"><script>alert("Howdy,admin!");</script>"><p>A notice with an unsafe type.</p></div>',
),
'an unsafe ID' => array(
'message' => 'A notice with an unsafe ID.',
'args' => array(
'id' => '"><script>alert( "Howdy, admin!" );</script> <div class="notice',
),
'expected' => '<div id=""><script>alert( "Howdy, admin!" );</script> <div class="notice" class="notice"><p>A notice with an unsafe ID.</p></div>',
),
'unsafe additional classes' => array(
'message' => 'A notice with unsafe additional classes.',
'args' => array(
'additional_classes' => array( '"><script>alert( "Howdy, admin!" );</script> <div class="notice' ),
),
'expected' => '<div class="notice "><script>alert( "Howdy, admin!" );</script> <div class="notice"><p>A notice with unsafe additional classes.</p></div>',
),
'a type that is not a string' => array(
'message' => 'A notice with a type that is not a string.',
'args' => array(
'type' => array(),
),
'expected' => '<div class="notice"><p>A notice with a type that is not a string.</p></div>',
),
'a type with only empty space' => array(
'message' => 'A notice with a type with only empty space.',
'args' => array(
'type' => " \t\r\n",
),
'expected' => '<div class="notice"><p>A notice with a type with only empty space.</p></div>',
),
'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' => '<div class="notice"><p>A notice with an ID that is not a string.</p></div>',
),
'an ID with only empty space' => array(
'message' => 'A notice with an ID with only empty space.',
'args' => array(
'id' => " \t\r\n",
),
'expected' => '<div class="notice"><p>A notice with an ID with only empty space.</p></div>',
),
'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' => '<div class="notice"><p>A notice with dismissible as a truthy value rather than (bool) true.</p></div>',
),
'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' => '<div class="notice"><p>A notice with additional classes that are not an array.</p></div>',
),
'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' => '<div class="notice"><p>A notice with paragraph wrapping as a falsy value rather than (bool) false.</p></div>',
),
);
}
/**
* 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(
'<div class="notice notice-first second third fourth"><p>A type containing spaces.</p></div>',
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' ),
);
}
}