[ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Widget API: WP_Widget_Text class 4 * 5 * @package WordPress 6 * @subpackage Widgets 7 * @since 4.4.0 8 */ 9 10 /** 11 * Core class used to implement a Text widget. 12 * 13 * @since 2.8.0 14 * 15 * @see WP_Widget 16 */ 17 class WP_Widget_Text extends WP_Widget { 18 19 /** 20 * Whether or not the widget has been registered yet. 21 * 22 * @since 4.8.1 23 * @var bool 24 */ 25 protected $registered = false; 26 27 /** 28 * Sets up a new Text widget instance. 29 * 30 * @since 2.8.0 31 */ 32 public function __construct() { 33 $widget_ops = array( 34 'classname' => 'widget_text', 35 'description' => __( 'Arbitrary text.' ), 36 'customize_selective_refresh' => true, 37 'show_instance_in_rest' => true, 38 ); 39 $control_ops = array( 40 'width' => 400, 41 'height' => 350, 42 ); 43 parent::__construct( 'text', __( 'Text' ), $widget_ops, $control_ops ); 44 } 45 46 /** 47 * Adds hooks for enqueueing assets when registering all widget instances of this widget class. 48 * 49 * @param int $number Optional. The unique order number of this widget instance 50 * compared to other instances of the same class. Default -1. 51 */ 52 public function _register_one( $number = -1 ) { 53 parent::_register_one( $number ); 54 if ( $this->registered ) { 55 return; 56 } 57 $this->registered = true; 58 59 if ( $this->is_preview() ) { 60 add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_preview_scripts' ) ); 61 } 62 63 /* 64 * Note that the widgets component in the customizer will also do 65 * the 'admin_print_scripts-widgets.php' action in WP_Customize_Widgets::print_scripts(). 66 */ 67 add_action( 'admin_print_scripts-widgets.php', array( $this, 'enqueue_admin_scripts' ) ); 68 69 /* 70 * Note that the widgets component in the customizer will also do 71 * the 'admin_footer-widgets.php' action in WP_Customize_Widgets::print_footer_scripts(). 72 */ 73 add_action( 'admin_footer-widgets.php', array( 'WP_Widget_Text', 'render_control_template_scripts' ) ); 74 } 75 76 /** 77 * Determines whether a given instance is legacy and should bypass using TinyMCE. 78 * 79 * @since 4.8.1 80 * 81 * @param array $instance { 82 * Instance data. 83 * 84 * @type string $text Content. 85 * @type bool|string $filter Whether autop or content filters should apply. 86 * @type bool $legacy Whether widget is in legacy mode. 87 * } 88 * @return bool Whether Text widget instance contains legacy data. 89 */ 90 public function is_legacy_instance( $instance ) { 91 92 // Legacy mode when not in visual mode. 93 if ( isset( $instance['visual'] ) ) { 94 return ! $instance['visual']; 95 } 96 97 // Or, the widget has been added/updated in 4.8.0 then filter prop is 'content' and it is no longer legacy. 98 if ( isset( $instance['filter'] ) && 'content' === $instance['filter'] ) { 99 return false; 100 } 101 102 // If the text is empty, then nothing is preventing migration to TinyMCE. 103 if ( empty( $instance['text'] ) ) { 104 return false; 105 } 106 107 $wpautop = ! empty( $instance['filter'] ); 108 $has_line_breaks = ( str_contains( trim( $instance['text'] ), "\n" ) ); 109 110 // If auto-paragraphs are not enabled and there are line breaks, then ensure legacy mode. 111 if ( ! $wpautop && $has_line_breaks ) { 112 return true; 113 } 114 115 // If an HTML comment is present, assume legacy mode. 116 if ( str_contains( $instance['text'], '<!--' ) ) { 117 return true; 118 } 119 120 // In the rare case that DOMDocument is not available we cannot reliably sniff content and so we assume legacy. 121 if ( ! class_exists( 'DOMDocument' ) ) { 122 // @codeCoverageIgnoreStart 123 return true; 124 // @codeCoverageIgnoreEnd 125 } 126 127 $doc = new DOMDocument(); 128 129 // Suppress warnings generated by loadHTML. 130 $errors = libxml_use_internal_errors( true ); 131 // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged 132 @$doc->loadHTML( 133 sprintf( 134 '<!DOCTYPE html><html><head><meta charset="%s"></head><body>%s</body></html>', 135 esc_attr( get_bloginfo( 'charset' ) ), 136 $instance['text'] 137 ) 138 ); 139 libxml_use_internal_errors( $errors ); 140 141 $body = $doc->getElementsByTagName( 'body' )->item( 0 ); 142 143 // See $allowedposttags. 144 $safe_elements_attributes = array( 145 'strong' => array(), 146 'em' => array(), 147 'b' => array(), 148 'i' => array(), 149 'u' => array(), 150 's' => array(), 151 'ul' => array(), 152 'ol' => array(), 153 'li' => array(), 154 'hr' => array(), 155 'abbr' => array(), 156 'acronym' => array(), 157 'code' => array(), 158 'dfn' => array(), 159 'a' => array( 160 'href' => true, 161 ), 162 'img' => array( 163 'src' => true, 164 'alt' => true, 165 ), 166 ); 167 $safe_empty_elements = array( 'img', 'hr', 'iframe' ); 168 169 foreach ( $body->getElementsByTagName( '*' ) as $element ) { 170 /** @var DOMElement $element */ 171 $tag_name = strtolower( $element->nodeName ); 172 173 // If the element is not safe, then the instance is legacy. 174 if ( ! isset( $safe_elements_attributes[ $tag_name ] ) ) { 175 return true; 176 } 177 178 // If the element is not safely empty and it has empty contents, then legacy mode. 179 if ( ! in_array( $tag_name, $safe_empty_elements, true ) && '' === trim( $element->textContent ) ) { 180 return true; 181 } 182 183 // If an attribute is not recognized as safe, then the instance is legacy. 184 foreach ( $element->attributes as $attribute ) { 185 /** @var DOMAttr $attribute */ 186 $attribute_name = strtolower( $attribute->nodeName ); 187 188 if ( ! isset( $safe_elements_attributes[ $tag_name ][ $attribute_name ] ) ) { 189 return true; 190 } 191 } 192 } 193 194 // Otherwise, the text contains no elements/attributes that TinyMCE could drop, and therefore the widget does not need legacy mode. 195 return false; 196 } 197 198 /** 199 * Filters gallery shortcode attributes. 200 * 201 * Prevents all of a site's attachments from being shown in a gallery displayed on a 202 * non-singular template where a $post context is not available. 203 * 204 * @since 4.9.0 205 * 206 * @param array $attrs Attributes. 207 * @return array Attributes. 208 */ 209 public function _filter_gallery_shortcode_attrs( $attrs ) { 210 if ( ! is_singular() && empty( $attrs['id'] ) && empty( $attrs['include'] ) ) { 211 $attrs['id'] = -1; 212 } 213 return $attrs; 214 } 215 216 /** 217 * Outputs the content for the current Text widget instance. 218 * 219 * @since 2.8.0 220 * 221 * @global WP_Post $post Global post object. 222 * 223 * @param array $args Display arguments including 'before_title', 'after_title', 224 * 'before_widget', and 'after_widget'. 225 * @param array $instance Settings for the current Text widget instance. 226 */ 227 public function widget( $args, $instance ) { 228 global $post; 229 230 $title = ! empty( $instance['title'] ) ? $instance['title'] : ''; 231 232 /** This filter is documented in wp-includes/widgets/class-wp-widget-pages.php */ 233 $title = apply_filters( 'widget_title', $title, $instance, $this->id_base ); 234 235 $text = ! empty( $instance['text'] ) ? $instance['text'] : ''; 236 $is_visual_text_widget = ( ! empty( $instance['visual'] ) && ! empty( $instance['filter'] ) ); 237 238 // In 4.8.0 only, visual Text widgets get filter=content, without visual prop; upgrade instance props just-in-time. 239 if ( ! $is_visual_text_widget ) { 240 $is_visual_text_widget = ( isset( $instance['filter'] ) && 'content' === $instance['filter'] ); 241 } 242 if ( $is_visual_text_widget ) { 243 $instance['filter'] = true; 244 $instance['visual'] = true; 245 } 246 247 /* 248 * Suspend legacy plugin-supplied do_shortcode() for 'widget_text' filter for the visual Text widget to prevent 249 * shortcodes being processed twice. Now do_shortcode() is added to the 'widget_text_content' filter in core itself 250 * and it applies after wpautop() to prevent corrupting HTML output added by the shortcode. When do_shortcode() is 251 * added to 'widget_text_content' then do_shortcode() will be manually called when in legacy mode as well. 252 */ 253 $widget_text_do_shortcode_priority = has_filter( 'widget_text', 'do_shortcode' ); 254 $should_suspend_legacy_shortcode_support = ( $is_visual_text_widget && false !== $widget_text_do_shortcode_priority ); 255 if ( $should_suspend_legacy_shortcode_support ) { 256 remove_filter( 'widget_text', 'do_shortcode', $widget_text_do_shortcode_priority ); 257 } 258 259 // Override global $post so filters (and shortcodes) apply in a consistent context. 260 $original_post = $post; 261 if ( is_singular() ) { 262 // Make sure post is always the queried object on singular queries (not from another sub-query that failed to clean up the global $post). 263 $post = get_queried_object(); 264 } else { 265 // Nullify the $post global during widget rendering to prevent shortcodes from running with the unexpected context on archive queries. 266 $post = null; 267 } 268 269 // Prevent dumping out all attachments from the media library. 270 add_filter( 'shortcode_atts_gallery', array( $this, '_filter_gallery_shortcode_attrs' ) ); 271 272 /** 273 * Filters the content of the Text widget. 274 * 275 * @since 2.3.0 276 * @since 4.4.0 Added the `$widget` parameter. 277 * @since 4.8.1 The `$widget` param may now be a `WP_Widget_Custom_HTML` object in addition to a `WP_Widget_Text` object. 278 * 279 * @param string $text The widget content. 280 * @param array $instance Array of settings for the current widget. 281 * @param WP_Widget_Text|WP_Widget_Custom_HTML $widget Current text or HTML widget instance. 282 */ 283 $text = apply_filters( 'widget_text', $text, $instance, $this ); 284 285 if ( $is_visual_text_widget ) { 286 287 /** 288 * Filters the content of the Text widget to apply changes expected from the visual (TinyMCE) editor. 289 * 290 * By default a subset of the_content filters are applied, including wpautop and wptexturize. 291 * 292 * @since 4.8.0 293 * 294 * @param string $text The widget content. 295 * @param array $instance Array of settings for the current widget. 296 * @param WP_Widget_Text $widget Current Text widget instance. 297 */ 298 $text = apply_filters( 'widget_text_content', $text, $instance, $this ); 299 } else { 300 // Now in legacy mode, add paragraphs and line breaks when checkbox is checked. 301 if ( ! empty( $instance['filter'] ) ) { 302 $text = wpautop( $text ); 303 } 304 305 /* 306 * Manually do shortcodes on the content when the core-added filter is present. It is added by default 307 * in core by adding do_shortcode() to the 'widget_text_content' filter to apply after wpautop(). 308 * Since the legacy Text widget runs wpautop() after 'widget_text' filters are applied, the widget in 309 * legacy mode here manually applies do_shortcode() on the content unless the default 310 * core filter for 'widget_text_content' has been removed, or if do_shortcode() has already 311 * been applied via a plugin adding do_shortcode() to 'widget_text' filters. 312 */ 313 if ( has_filter( 'widget_text_content', 'do_shortcode' ) && ! $widget_text_do_shortcode_priority ) { 314 if ( ! empty( $instance['filter'] ) ) { 315 $text = shortcode_unautop( $text ); 316 } 317 $text = do_shortcode( $text ); 318 } 319 } 320 321 // Restore post global. 322 $post = $original_post; 323 remove_filter( 'shortcode_atts_gallery', array( $this, '_filter_gallery_shortcode_attrs' ) ); 324 325 // Undo suspension of legacy plugin-supplied shortcode handling. 326 if ( $should_suspend_legacy_shortcode_support ) { 327 add_filter( 'widget_text', 'do_shortcode', $widget_text_do_shortcode_priority ); 328 } 329 330 echo $args['before_widget']; 331 if ( ! empty( $title ) ) { 332 echo $args['before_title'] . $title . $args['after_title']; 333 } 334 335 $text = preg_replace_callback( '#<(video|iframe|object|embed)\s[^>]*>#i', array( $this, 'inject_video_max_width_style' ), $text ); 336 337 // Adds 'noopener' relationship, without duplicating values, to all HTML A elements that have a target. 338 $text = wp_targeted_link_rel( $text ); 339 340 ?> 341 <div class="textwidget"><?php echo $text; ?></div> 342 <?php 343 echo $args['after_widget']; 344 } 345 346 /** 347 * Injects max-width and removes height for videos too constrained to fit inside sidebars on frontend. 348 * 349 * @since 4.9.0 350 * 351 * @see WP_Widget_Media_Video::inject_video_max_width_style() 352 * 353 * @param array $matches Pattern matches from preg_replace_callback. 354 * @return string HTML Output. 355 */ 356 public function inject_video_max_width_style( $matches ) { 357 $html = $matches[0]; 358 $html = preg_replace( '/\sheight="\d+"/', '', $html ); 359 $html = preg_replace( '/\swidth="\d+"/', '', $html ); 360 $html = preg_replace( '/(?<=width:)\s*\d+px(?=;?)/', '100%', $html ); 361 return $html; 362 } 363 364 /** 365 * Handles updating settings for the current Text widget instance. 366 * 367 * @since 2.8.0 368 * 369 * @param array $new_instance New settings for this instance as input by the user via 370 * WP_Widget::form(). 371 * @param array $old_instance Old settings for this instance. 372 * @return array Settings to save or bool false to cancel saving. 373 */ 374 public function update( $new_instance, $old_instance ) { 375 $new_instance = wp_parse_args( 376 $new_instance, 377 array( 378 'title' => '', 379 'text' => '', 380 'filter' => false, // For back-compat. 381 'visual' => null, // Must be explicitly defined. 382 ) 383 ); 384 385 $instance = $old_instance; 386 387 $instance['title'] = sanitize_text_field( $new_instance['title'] ); 388 if ( current_user_can( 'unfiltered_html' ) ) { 389 $instance['text'] = $new_instance['text']; 390 } else { 391 $instance['text'] = wp_kses_post( $new_instance['text'] ); 392 } 393 394 $instance['filter'] = ! empty( $new_instance['filter'] ); 395 396 // Upgrade 4.8.0 format. 397 if ( isset( $old_instance['filter'] ) && 'content' === $old_instance['filter'] ) { 398 $instance['visual'] = true; 399 } 400 if ( 'content' === $new_instance['filter'] ) { 401 $instance['visual'] = true; 402 } 403 404 if ( isset( $new_instance['visual'] ) ) { 405 $instance['visual'] = ! empty( $new_instance['visual'] ); 406 } 407 408 // Filter is always true in visual mode. 409 if ( ! empty( $instance['visual'] ) ) { 410 $instance['filter'] = true; 411 } 412 413 return $instance; 414 } 415 416 /** 417 * Enqueues preview scripts. 418 * 419 * These scripts normally are enqueued just-in-time when a playlist shortcode is used. 420 * However, in the customizer, a playlist shortcode may be used in a text widget and 421 * dynamically added via selective refresh, so it is important to unconditionally enqueue them. 422 * 423 * @since 4.9.3 424 */ 425 public function enqueue_preview_scripts() { 426 require_once dirname( __DIR__ ) . '/media.php'; 427 428 wp_playlist_scripts( 'audio' ); 429 wp_playlist_scripts( 'video' ); 430 } 431 432 /** 433 * Loads the required scripts and styles for the widget control. 434 * 435 * @since 4.8.0 436 */ 437 public function enqueue_admin_scripts() { 438 wp_enqueue_editor(); 439 wp_enqueue_media(); 440 wp_enqueue_script( 'text-widgets' ); 441 wp_add_inline_script( 'text-widgets', sprintf( 'wp.textWidgets.idBases.push( %s );', wp_json_encode( $this->id_base ) ) ); 442 wp_add_inline_script( 'text-widgets', 'wp.textWidgets.init();', 'after' ); 443 } 444 445 /** 446 * Outputs the Text widget settings form. 447 * 448 * @since 2.8.0 449 * @since 4.8.0 Form only contains hidden inputs which are synced with JS template. 450 * @since 4.8.1 Restored original form to be displayed when in legacy mode. 451 * 452 * @see WP_Widget_Text::render_control_template_scripts() 453 * @see _WP_Editors::editor() 454 * 455 * @param array $instance Current settings. 456 */ 457 public function form( $instance ) { 458 $instance = wp_parse_args( 459 (array) $instance, 460 array( 461 'title' => '', 462 'text' => '', 463 ) 464 ); 465 ?> 466 <?php if ( ! $this->is_legacy_instance( $instance ) ) : ?> 467 <?php 468 469 if ( user_can_richedit() ) { 470 add_filter( 'the_editor_content', 'format_for_editor', 10, 2 ); 471 $default_editor = 'tinymce'; 472 } else { 473 $default_editor = 'html'; 474 } 475 476 /** This filter is documented in wp-includes/class-wp-editor.php */ 477 $text = apply_filters( 'the_editor_content', $instance['text'], $default_editor ); 478 479 // Reset filter addition. 480 if ( user_can_richedit() ) { 481 remove_filter( 'the_editor_content', 'format_for_editor' ); 482 } 483 484 // Prevent premature closing of textarea in case format_for_editor() didn't apply or the_editor_content filter did a wrong thing. 485 $escaped_text = preg_replace( '#</textarea#i', '</textarea', $text ); 486 487 ?> 488 <input id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" class="title sync-input" type="hidden" value="<?php echo esc_attr( $instance['title'] ); ?>"> 489 <textarea id="<?php echo $this->get_field_id( 'text' ); ?>" name="<?php echo $this->get_field_name( 'text' ); ?>" class="text sync-input" hidden><?php echo $escaped_text; ?></textarea> 490 <input id="<?php echo $this->get_field_id( 'filter' ); ?>" name="<?php echo $this->get_field_name( 'filter' ); ?>" class="filter sync-input" type="hidden" value="on"> 491 <input id="<?php echo $this->get_field_id( 'visual' ); ?>" name="<?php echo $this->get_field_name( 'visual' ); ?>" class="visual sync-input" type="hidden" value="on"> 492 <?php else : ?> 493 <input id="<?php echo $this->get_field_id( 'visual' ); ?>" name="<?php echo $this->get_field_name( 'visual' ); ?>" class="visual" type="hidden" value=""> 494 <p> 495 <label for="<?php echo $this->get_field_id( 'title' ); ?>"><?php _e( 'Title:' ); ?></label> 496 <input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" type="text" value="<?php echo esc_attr( $instance['title'] ); ?>" /> 497 </p> 498 <?php 499 if ( ! isset( $instance['visual'] ) ) { 500 $widget_info_message = __( 'This widget may contain code that may work better in the “Custom HTML” widget. How about trying that widget instead?' ); 501 } else { 502 $widget_info_message = __( 'This widget may have contained code that may work better in the “Custom HTML” widget. If you have not yet, how about trying that widget instead?' ); 503 } 504 505 wp_admin_notice( 506 $widget_info_message, 507 array( 508 'type' => 'info', 509 'additional_classes' => array( 'notice-alt', 'inline' ), 510 ) 511 ); 512 ?> 513 <p> 514 <label for="<?php echo $this->get_field_id( 'text' ); ?>"><?php _e( 'Content:' ); ?></label> 515 <textarea class="widefat" rows="16" cols="20" id="<?php echo $this->get_field_id( 'text' ); ?>" name="<?php echo $this->get_field_name( 'text' ); ?>"><?php echo esc_textarea( $instance['text'] ); ?></textarea> 516 </p> 517 <p> 518 <input id="<?php echo $this->get_field_id( 'filter' ); ?>" name="<?php echo $this->get_field_name( 'filter' ); ?>" type="checkbox"<?php checked( ! empty( $instance['filter'] ) ); ?> /> <label for="<?php echo $this->get_field_id( 'filter' ); ?>"><?php _e( 'Automatically add paragraphs' ); ?></label> 519 </p> 520 <?php 521 endif; 522 } 523 524 /** 525 * Renders form template scripts. 526 * 527 * @since 4.8.0 528 * @since 4.9.0 The method is now static. 529 */ 530 public static function render_control_template_scripts() { 531 $dismissed_pointers = explode( ',', (string) get_user_meta( get_current_user_id(), 'dismissed_wp_pointers', true ) ); 532 ?> 533 <script type="text/html" id="tmpl-widget-text-control-fields"> 534 <# var elementIdPrefix = 'el' + String( Math.random() ).replace( /\D/g, '' ) + '_' #> 535 <p> 536 <label for="{{ elementIdPrefix }}title"><?php esc_html_e( 'Title:' ); ?></label> 537 <input id="{{ elementIdPrefix }}title" type="text" class="widefat title"> 538 </p> 539 540 <?php if ( ! in_array( 'text_widget_custom_html', $dismissed_pointers, true ) ) : ?> 541 <div hidden class="wp-pointer custom-html-widget-pointer wp-pointer-top"> 542 <div class="wp-pointer-content"> 543 <h3><?php _e( 'New Custom HTML Widget' ); ?></h3> 544 <?php if ( is_customize_preview() ) : ?> 545 <p><?php _e( 'Did you know there is a “Custom HTML” widget now? You can find it by pressing the “<a class="add-widget" href="#">Add a Widget</a>” button and searching for “HTML”. Check it out to add some custom code to your site!' ); ?></p> 546 <?php else : ?> 547 <p><?php _e( 'Did you know there is a “Custom HTML” widget now? You can find it by scanning the list of available widgets on this screen. Check it out to add some custom code to your site!' ); ?></p> 548 <?php endif; ?> 549 <div class="wp-pointer-buttons"> 550 <a class="close" href="#"><?php _e( 'Dismiss' ); ?></a> 551 </div> 552 </div> 553 <div class="wp-pointer-arrow"> 554 <div class="wp-pointer-arrow-inner"></div> 555 </div> 556 </div> 557 <?php endif; ?> 558 559 <?php if ( ! in_array( 'text_widget_paste_html', $dismissed_pointers, true ) ) : ?> 560 <div hidden class="wp-pointer paste-html-pointer wp-pointer-top"> 561 <div class="wp-pointer-content"> 562 <h3><?php _e( 'Did you just paste HTML?' ); ?></h3> 563 <p><?php _e( 'Hey there, looks like you just pasted HTML into the “Visual” tab of the Text widget. You may want to paste your code into the “Text” tab instead. Alternately, try out the new “Custom HTML” widget!' ); ?></p> 564 <div class="wp-pointer-buttons"> 565 <a class="close" href="#"><?php _e( 'Dismiss' ); ?></a> 566 </div> 567 </div> 568 <div class="wp-pointer-arrow"> 569 <div class="wp-pointer-arrow-inner"></div> 570 </div> 571 </div> 572 <?php endif; ?> 573 574 <p> 575 <label for="{{ elementIdPrefix }}text" class="screen-reader-text"><?php /* translators: Hidden accessibility text. */ esc_html_e( 'Content:' ); ?></label> 576 <textarea id="{{ elementIdPrefix }}text" class="widefat text wp-editor-area" style="height: 200px" rows="16" cols="20"></textarea> 577 </p> 578 </script> 579 <?php 580 } 581 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated : Sat Apr 20 08:20:01 2024 | Cross-referenced by PHPXref |