diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index 8f0237028d..6f64ffab5d 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -170,6 +170,8 @@ add_filter( 'widget_text_content', 'wptexturize' ); add_filter( 'widget_text_content', 'convert_smilies', 20 ); add_filter( 'widget_text_content', 'wpautop' ); +add_filter( 'widget_html_code_content', 'balanceTags' ); + add_filter( 'date_i18n', 'wp_maybe_decline_date' ); // RSS filters diff --git a/src/wp-includes/default-widgets.php b/src/wp-includes/default-widgets.php index 87ad9dbfb3..32a908ac11 100644 --- a/src/wp-includes/default-widgets.php +++ b/src/wp-includes/default-widgets.php @@ -60,3 +60,6 @@ require_once( ABSPATH . WPINC . '/widgets/class-wp-widget-tag-cloud.php' ); /** WP_Nav_Menu_Widget class */ require_once( ABSPATH . WPINC . '/widgets/class-wp-nav-menu-widget.php' ); + +/** WP_Widget_HTML_Code class */ +require_once( ABSPATH . WPINC . '/widgets/class-wp-widget-html-code.php' ); diff --git a/src/wp-includes/widgets.php b/src/wp-includes/widgets.php index caa575b149..b44048d641 100644 --- a/src/wp-includes/widgets.php +++ b/src/wp-includes/widgets.php @@ -1474,6 +1474,8 @@ function wp_widgets_init() { register_widget( 'WP_Nav_Menu_Widget' ); + register_widget( 'WP_Widget_HTML_Code' ); + /** * Fires after all default WordPress widgets have been registered. * diff --git a/src/wp-includes/widgets/class-wp-widget-html-code.php b/src/wp-includes/widgets/class-wp-widget-html-code.php new file mode 100644 index 0000000000..d16900183f --- /dev/null +++ b/src/wp-includes/widgets/class-wp-widget-html-code.php @@ -0,0 +1,139 @@ + '', + 'content' => '', + ); + + /** + * Sets up a new HTML Code widget instance. + * + * @since 4.8.1 + */ + public function __construct() { + $widget_ops = array( + 'classname' => 'widget_html_code', + 'description' => __( 'Arbitrary HTML code.' ), + 'customize_selective_refresh' => true, + ); + $control_ops = array(); + parent::__construct( 'html_code', __( 'HTML Code' ), $widget_ops, $control_ops ); + } + + /** + * Outputs the content for the current HTML Code widget instance. + * + * @since 4.8.1 + * + * @param array $args Display arguments including 'before_title', 'after_title', + * 'before_widget', and 'after_widget'. + * @param array $instance Settings for the current HTML Code widget instance. + */ + public function widget( $args, $instance ) { + + $instance = array_merge( $this->default_instance, $instance ); + + /** This filter is documented in wp-includes/widgets/class-wp-widget-pages.php */ + $title = apply_filters( 'widget_title', $instance['title'], $instance, $this->id_base ); + + $content = $instance['content']; + + /** + * Filters the content of the HTML Code widget. + * + * @since 4.8.1 + * + * @param string $content The widget content. + * @param array $instance Array of settings for the current widget. + * @param WP_Widget_HTML_Code $this Current HTML Code widget instance. + */ + $content = apply_filters( 'widget_html_code_content', $content, $instance, $this ); + + echo $args['before_widget']; + if ( ! empty( $title ) ) { + echo $args['before_title'] . $title . $args['after_title']; + } + echo $content; + echo $args['after_widget']; + } + + /** + * Handles updating settings for the current HTML Code widget instance. + * + * @since 4.8.1 + * + * @param array $new_instance New settings for this instance as input by the user via + * WP_Widget::form(). + * @param array $old_instance Old settings for this instance. + * @return array Settings to save or bool false to cancel saving. + */ + public function update( $new_instance, $old_instance ) { + $instance = array_merge( $this->default_instance, $old_instance ); + $instance['title'] = sanitize_text_field( $new_instance['title'] ); + if ( current_user_can( 'unfiltered_html' ) ) { + $instance['content'] = $new_instance['content']; + } else { + $instance['content'] = wp_kses_post( $new_instance['content'] ); + } + return $instance; + } + + /** + * Outputs the HTML Code widget settings form. + * + * @since 4.8.1 + * + * @param array $instance Current instance. + * @returns void + */ + public function form( $instance ) { + $instance = wp_parse_args( (array) $instance, $this->default_instance ); + ?> +

+ + +

+ +

+ + +

+ + + + +

+ + , ', $disallowed_html ); ?> +

+ + + 'widget_text', - 'description' => __( 'Arbitrary text or HTML.' ), + 'description' => __( 'Arbitrary text.' ), 'customize_selective_refresh' => true, ); $control_ops = array( diff --git a/tests/phpunit/tests/widgets/html-code-widget.php b/tests/phpunit/tests/widgets/html-code-widget.php new file mode 100644 index 0000000000..9ab5a18b94 --- /dev/null +++ b/tests/phpunit/tests/widgets/html-code-widget.php @@ -0,0 +1,153 @@ +Custom HTML\n\nCODE\nLast line.unclosed"; + + $args = array( + 'before_title' => '

', + 'after_title' => "

\n", + 'before_widget' => '
', + 'after_widget' => "
\n", + ); + $instance = array( + 'title' => 'Foo', + 'content' => $content, + ); + + $this->assertEquals( 10, has_filter( 'widget_html_code_content', 'balanceTags' ) ); + + update_option( 'use_balanceTags', 0 ); + add_filter( 'widget_html_code_content', array( $this, 'filter_widget_html_code_content' ), 5, 3 ); + ob_start(); + $this->widget_html_code_content_args = null; + $widget->widget( $args, $instance ); + $output = ob_get_clean(); + $this->assertNotEmpty( $this->widget_html_code_content_args ); + $this->assertContains( '[filter:widget_html_code_content]', $output ); + $this->assertNotContains( '

', $output ); + $this->assertNotContains( '
', $output ); + $this->assertNotContains( '
', $output ); + $this->assertEquals( $instance, $this->widget_html_code_content_args[1] ); + $this->assertSame( $widget, $this->widget_html_code_content_args[2] ); + remove_filter( 'widget_html_code_content', array( $this, 'filter_widget_html_code_content' ), 5, 3 ); + + update_option( 'use_balanceTags', 1 ); + ob_start(); + $widget->widget( $args, $instance ); + $output = ob_get_clean(); + $this->assertContains( '', $output ); + } + + /** + * Filters the content of the HTML Code widget. + * + * @param string $widget_content The widget content. + * @param array $instance Array of settings for the current widget. + * @param WP_Widget_HTML_Code $widget Current HTML Code widget instance. + * @return string Widget content. + */ + function filter_widget_html_code_content( $widget_content, $instance, $widget ) { + $this->widget_html_code_content_args = func_get_args(); + + $widget_content .= '[filter:widget_html_code_content]'; + return $widget_content; + } + + /** + * Test update method. + * + * @covers WP_Widget_HTML_Code::update + */ + function test_update() { + $widget = new WP_Widget_HTML_Code(); + $instance = array( + 'title' => "The\nTitle", + 'content' => "The\n\nCode", + ); + + wp_set_current_user( $this->factory()->user->create( array( + 'role' => 'administrator', + ) ) ); + + // Should return valid instance. + $expected = array( + 'title' => sanitize_text_field( $instance['title'] ), + 'content' => $instance['content'], + ); + $result = $widget->update( $instance, array() ); + $this->assertEquals( $result, $expected ); + + // Make sure KSES is applying as expected. + add_filter( 'map_meta_cap', array( $this, 'grant_unfiltered_html_cap' ), 10, 2 ); + $this->assertTrue( current_user_can( 'unfiltered_html' ) ); + $instance['content'] = ''; + $expected['content'] = $instance['content']; + $result = $widget->update( $instance, array() ); + $this->assertEquals( $result, $expected ); + remove_filter( 'map_meta_cap', array( $this, 'grant_unfiltered_html_cap' ) ); + + add_filter( 'map_meta_cap', array( $this, 'revoke_unfiltered_html_cap' ), 10, 2 ); + $this->assertFalse( current_user_can( 'unfiltered_html' ) ); + $instance['content'] = ''; + $expected['content'] = wp_kses_post( $instance['content'] ); + $result = $widget->update( $instance, array() ); + $this->assertEquals( $result, $expected ); + remove_filter( 'map_meta_cap', array( $this, 'revoke_unfiltered_html_cap' ), 10 ); + } + + /** + * Grant unfiltered_html cap via map_meta_cap. + * + * @param array $caps Returns the user's actual capabilities. + * @param string $cap Capability name. + * @return array Caps. + */ + function grant_unfiltered_html_cap( $caps, $cap ) { + if ( 'unfiltered_html' === $cap ) { + $caps = array_diff( $caps, array( 'do_not_allow' ) ); + $caps[] = 'unfiltered_html'; + } + return $caps; + } + + /** + * Revoke unfiltered_html cap via map_meta_cap. + * + * @param array $caps Returns the user's actual capabilities. + * @param string $cap Capability name. + * @return array Caps. + */ + function revoke_unfiltered_html_cap( $caps, $cap ) { + if ( 'unfiltered_html' === $cap ) { + $caps = array_diff( $caps, array( 'unfiltered_html' ) ); + $caps[] = 'do_not_allow'; + } + return $caps; + } +}