<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[41050] trunk: Widgets: Add legacy mode for Text widget and add usage pointers to default visual mode.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/41050">41050</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/41050","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>westonruter</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2017-07-14 17:08:20 +0000 (Fri, 14 Jul 2017)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>Widgets: Add legacy mode for Text widget and add usage pointers to default visual mode.

The Text widget in legacy mode omits TinyMCE and retains old behavior for matching pre-existing Text widgets. Usage pointers added to default visual mode appear when attempting to paste HTML code into the Visual tab and when clicking on the Text tab, informing users of the new Custom HTML widget.

Props westonruter, melchoyce, gitlost for testing, obenland for testing, dougal for testing, afercia for testing.
See <a href="https://core.trac.wordpress.org/ticket/35243">#35243</a>.
Fixes <a href="https://core.trac.wordpress.org/ticket/40951">#40951</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpadmincsswidgetscss">trunk/src/wp-admin/css/widgets.css</a></li>
<li><a href="#trunksrcwpadminjswidgetstextwidgetsjs">trunk/src/wp-admin/js/widgets/text-widgets.js</a></li>
<li><a href="#trunksrcwpincludesscriptloaderphp">trunk/src/wp-includes/script-loader.php</a></li>
<li><a href="#trunksrcwpincludeswidgetsclasswpwidgettextphp">trunk/src/wp-includes/widgets/class-wp-widget-text.php</a></li>
<li><a href="#trunktestsphpunittestswidgetstextwidgetphp">trunk/tests/phpunit/tests/widgets/text-widget.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpadmincsswidgetscss"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/css/widgets.css</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/css/widgets.css        2017-07-14 16:18:42 UTC (rev 41049)
+++ trunk/src/wp-admin/css/widgets.css  2017-07-14 17:08:20 UTC (rev 41050)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -619,6 +619,29 @@
</span><span class="cx" style="display: block; padding: 0 10px">        cursor: move;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+/* =Specific widget styling
+-------------------------------------------------------------- */
+.text-widget-fields {
+       position: relative;
+}
+.text-widget-fields [hidden] {
+       display: none;
+}
+.text-widget-fields .wp-pointer.wp-pointer-top {
+       position: absolute;
+       z-index: 3;
+       top: 100px;
+       right: 10px;
+       left: 10px;
+}
+.text-widget-fields .wp-pointer .wp-pointer-arrow {
+       left: auto;
+       right: 15px;
+}
+.text-widget-fields .wp-pointer .wp-pointer-buttons {
+       line-height: 1.4em;
+}
+
</ins><span class="cx" style="display: block; padding: 0 10px"> /* =Media Queries
</span><span class="cx" style="display: block; padding: 0 10px"> -------------------------------------------------------------- */
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span></span></pre></div>
<a id="trunksrcwpadminjswidgetstextwidgetsjs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/js/widgets/text-widgets.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/js/widgets/text-widgets.js     2017-07-14 16:18:42 UTC (rev 41049)
+++ trunk/src/wp-admin/js/widgets/text-widgets.js       2017-07-14 17:08:20 UTC (rev 41050)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3,7 +3,9 @@
</span><span class="cx" style="display: block; padding: 0 10px"> wp.textWidgets = ( function( $ ) {
</span><span class="cx" style="display: block; padding: 0 10px">        'use strict';
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        var component = {};
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ var component = {
+               dismissedPointers: []
+       };
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="cx" style="display: block; padding: 0 10px">         * Text widget control.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -45,6 +47,31 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        control.$el.addClass( 'text-widget-fields' );
</span><span class="cx" style="display: block; padding: 0 10px">                        control.$el.html( wp.template( 'widget-text-control-fields' ) );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        control.customHtmlWidgetPointer = control.$el.find( '.wp-pointer.custom-html-widget-pointer' );
+                       if ( control.customHtmlWidgetPointer.length ) {
+                               control.customHtmlWidgetPointer.find( '.close' ).on( 'click', function( event ) {
+                                       event.preventDefault();
+                                       control.customHtmlWidgetPointer.hide();
+                                       $( '#' + control.fields.text.attr( 'id' ) + '-html' ).focus();
+                                       control.dismissPointers( [ 'text_widget_custom_html' ] );
+                               });
+                               control.customHtmlWidgetPointer.find( '.add-widget' ).on( 'click', function( event ) {
+                                       event.preventDefault();
+                                       control.customHtmlWidgetPointer.hide();
+                                       control.openAvailableWidgetsPanel();
+                               });
+                       }
+
+                       control.pasteHtmlPointer = control.$el.find( '.wp-pointer.paste-html-pointer' );
+                       if ( control.pasteHtmlPointer.length ) {
+                               control.pasteHtmlPointer.find( '.close' ).on( 'click', function( event ) {
+                                       event.preventDefault();
+                                       control.pasteHtmlPointer.hide();
+                                       control.editor.focus();
+                                       control.dismissPointers( [ 'text_widget_custom_html', 'text_widget_paste_html' ] );
+                               });
+                       }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                         control.fields = {
</span><span class="cx" style="display: block; padding: 0 10px">                                title: control.$el.find( '.title' ),
</span><span class="cx" style="display: block; padding: 0 10px">                                text: control.$el.find( '.text' )
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -66,6 +93,45 @@
</span><span class="cx" style="display: block; padding: 0 10px">                },
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 * Dismiss pointers for Custom HTML widget.
+                *
+                * @since 4.8.1
+                *
+                * @param {Array} pointers Pointer IDs to dismiss.
+                * @returns {void}
+                */
+               dismissPointers: function dismissPointers( pointers ) {
+                       _.each( pointers, function( pointer ) {
+                               wp.ajax.post( 'dismiss-wp-pointer', {
+                                       pointer: pointer
+                               });
+                               component.dismissedPointers.push( pointer );
+                       });
+               },
+
+               /**
+                * Open available widgets panel.
+                *
+                * @since 4.8.1
+                * @returns {void}
+                */
+               openAvailableWidgetsPanel: function openAvailableWidgetsPanel() {
+                       var sidebarControl;
+                       wp.customize.section.each( function( section ) {
+                               if ( section.extended( wp.customize.Widgets.SidebarSection ) && section.expanded() ) {
+                                       sidebarControl = wp.customize.control( 'sidebars_widgets[' + section.params.sidebarId + ']' );
+                               }
+                       });
+                       if ( ! sidebarControl ) {
+                               return;
+                       }
+                       setTimeout( function() { // Timeout to prevent click event from causing panel to immediately collapse.
+                               wp.customize.Widgets.availableWidgetsPanel.open( sidebarControl );
+                               wp.customize.Widgets.availableWidgetsPanel.$search.val( 'HTML' ).trigger( 'keyup' );
+                       });
+               },
+
+               /**
</ins><span class="cx" style="display: block; padding: 0 10px">                  * Update input fields from the sync fields.
</span><span class="cx" style="display: block; padding: 0 10px">                 *
</span><span class="cx" style="display: block; padding: 0 10px">                 * This function is called at the widget-updated and widget-synced events.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -108,7 +174,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                         * @returns {void}
</span><span class="cx" style="display: block; padding: 0 10px">                         */
</span><span class="cx" style="display: block; padding: 0 10px">                        function buildEditor() {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                var editor, triggerChangeIfDirty, onInit;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         var editor, triggerChangeIfDirty, onInit, showPointerElement;
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                // Abort building if the textarea is gone, likely due to the widget having been deleted entirely.
</span><span class="cx" style="display: block; padding: 0 10px">                                if ( ! document.getElementById( id ) ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -137,6 +203,20 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                        quicktags: true
</span><span class="cx" style="display: block; padding: 0 10px">                                });
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                /**
+                                * Show a pointer, focus on dismiss, and speak the contents for a11y.
+                                *
+                                * @param {jQuery} pointerElement Pointer element.
+                                * @returns {void}
+                                */
+                               showPointerElement = function( pointerElement ) {
+                                       pointerElement.show();
+                                       pointerElement.find( '.close' ).focus();
+                                       wp.a11y.speak( pointerElement.find( 'h3, p' ).map( function() {
+                                               return $( this ).text();
+                                       } ).get().join( '\n\n' ) );
+                               };
+
</ins><span class="cx" style="display: block; padding: 0 10px">                                 editor = window.tinymce.get( id );
</span><span class="cx" style="display: block; padding: 0 10px">                                if ( ! editor ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                        throw new Error( 'Failed to initialize editor' );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -152,6 +232,34 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                        if ( restoreTextMode ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                                switchEditors.go( id, 'toggle' );
</span><span class="cx" style="display: block; padding: 0 10px">                                        }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+                                       // Show the pointer.
+                                       $( '#' + id + '-html' ).on( 'click', function() {
+                                               control.pasteHtmlPointer.hide(); // Hide the HTML pasting pointer.
+
+                                               if ( -1 !== component.dismissedPointers.indexOf( 'text_widget_custom_html' ) ) {
+                                                       return;
+                                               }
+                                               showPointerElement( control.customHtmlWidgetPointer );
+                                       });
+
+                                       // Hide the pointer when switching tabs.
+                                       $( '#' + id + '-tmce' ).on( 'click', function() {
+                                               control.customHtmlWidgetPointer.hide();
+                                       });
+
+                                       // Show pointer when pasting HTML.
+                                       editor.on( 'pastepreprocess', function( event ) {
+                                               var content = event.content;
+                                               if ( -1 !== component.dismissedPointers.indexOf( 'text_widget_paste_html' ) || ! content || ! /&lt;\w+.*?&gt;/.test( content ) ) {
+                                                       return;
+                                               }
+
+                                               // Show the pointer after a slight delay so the user sees what they pasted.
+                                               _.delay( function() {
+                                                       showPointerElement( control.pasteHtmlPointer );
+                                               }, 250 );
+                                       });
</ins><span class="cx" style="display: block; padding: 0 10px">                                 };
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                if ( editor.initialized ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -233,6 +341,11 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        return;
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                // Bypass using TinyMCE when widget is in legacy mode.
+               if ( widgetForm.find( '.legacy' ).length > 0 ) {
+                       return;
+               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 /*
</span><span class="cx" style="display: block; padding: 0 10px">                 * Create a container element for the widget control fields.
</span><span class="cx" style="display: block; padding: 0 10px">                 * This is inserted into the DOM immediately before the the .widget-content
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -289,6 +402,11 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        return;
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                // Bypass using TinyMCE when widget is in legacy mode.
+               if ( widgetForm.find( '.legacy' ).length > 0 ) {
+                       return;
+               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 fieldContainer = $( '<div></div>' );
</span><span class="cx" style="display: block; padding: 0 10px">                syncContainer = widgetForm.find( '> .widget-inside' );
</span><span class="cx" style="display: block; padding: 0 10px">                syncContainer.before( fieldContainer );
</span></span></pre></div>
<a id="trunksrcwpincludesscriptloaderphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/script-loader.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/script-loader.php   2017-07-14 16:18:42 UTC (rev 41049)
+++ trunk/src/wp-includes/script-loader.php     2017-07-14 17:08:20 UTC (rev 41050)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -608,7 +608,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $scripts->add( 'media-audio-widget', "/wp-admin/js/widgets/media-audio-widget$suffix.js", array( 'media-widgets', 'media-audiovideo' ) );
</span><span class="cx" style="display: block; padding: 0 10px">                $scripts->add( 'media-image-widget', "/wp-admin/js/widgets/media-image-widget$suffix.js", array( 'media-widgets' ) );
</span><span class="cx" style="display: block; padding: 0 10px">                $scripts->add( 'media-video-widget', "/wp-admin/js/widgets/media-video-widget$suffix.js", array( 'media-widgets', 'media-audiovideo' ) );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $scripts->add( 'text-widgets', "/wp-admin/js/widgets/text-widgets$suffix.js", array( 'jquery', 'backbone', 'editor', 'wp-util' ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $scripts->add( 'text-widgets', "/wp-admin/js/widgets/text-widgets$suffix.js", array( 'jquery', 'backbone', 'editor', 'wp-util', 'wp-a11y' ) );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $scripts->add_inline_script( 'text-widgets', 'wp.textWidgets.init();', 'after' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $scripts->add( 'theme', "/wp-admin/js/theme$suffix.js", array( 'wp-backbone', 'wp-a11y' ), false, 1 );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -845,7 +845,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">        $styles->add( 'themes',              "/wp-admin/css/themes$suffix.css" );
</span><span class="cx" style="display: block; padding: 0 10px">        $styles->add( 'about',               "/wp-admin/css/about$suffix.css" );
</span><span class="cx" style="display: block; padding: 0 10px">        $styles->add( 'nav-menus',           "/wp-admin/css/nav-menus$suffix.css" );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        $styles->add( 'widgets',             "/wp-admin/css/widgets$suffix.css" );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $styles->add( 'widgets',             "/wp-admin/css/widgets$suffix.css", array( 'wp-pointer' ) );
</ins><span class="cx" style="display: block; padding: 0 10px">         $styles->add( 'site-icon',           "/wp-admin/css/site-icon$suffix.css" );
</span><span class="cx" style="display: block; padding: 0 10px">        $styles->add( 'l10n',                "/wp-admin/css/l10n$suffix.css" );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span></span></pre></div>
<a id="trunksrcwpincludeswidgetsclasswpwidgettextphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/widgets/class-wp-widget-text.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/widgets/class-wp-widget-text.php    2017-07-14 16:18:42 UTC (rev 41049)
+++ trunk/src/wp-includes/widgets/class-wp-widget-text.php      2017-07-14 17:08:20 UTC (rev 41050)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -64,6 +64,129 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Determines whether a given instance is legacy and should bypass using TinyMCE.
+        *
+        * @since 4.8.1
+        *
+        * @param array $instance {
+        *     Instance data.
+        *
+        *     @type string      $text   Content.
+        *     @type bool|string $filter Whether autop or content filters should apply.
+        *     @type bool        $legacy Whether widget is in legacy mode.
+        * }
+        * @return bool Whether Text widget instance contains legacy data.
+        */
+       public function is_legacy_instance( $instance ) {
+
+               // If the widget has been updated while in legacy mode, it stays in legacy mode.
+               if ( ! empty( $instance['legacy'] ) ) {
+                       return true;
+               }
+
+               // If the widget has been added/updated in 4.8 then filter prop is 'content' and it is no longer legacy.
+               if ( isset( $instance['filter'] ) && 'content' === $instance['filter'] ) {
+                       return false;
+               }
+
+               // If the text is empty, then nothing is preventing migration to TinyMCE.
+               if ( empty( $instance['text'] ) ) {
+                       return false;
+               }
+
+               $wpautop = ! empty( $instance['filter'] );
+               $has_line_breaks = ( false !== strpos( $instance['text'], "\n" ) );
+
+               // If auto-paragraphs are not enabled and there are line breaks, then ensure legacy mode.
+               if ( ! $wpautop && $has_line_breaks ) {
+                       return true;
+               }
+
+               // If an HTML comment is present, assume legacy mode.
+               if ( false !== strpos( $instance['text'], '<!--' ) ) {
+                       return true;
+               }
+
+               /*
+                * If a shortcode is present (with support added by a plugin), assume legacy mode
+                * since shortcodes would apply at the widget_text filter and thus be applied
+                * before wpautop runs at the widget_text_content filter.
+                */
+               if ( preg_match( '/' . get_shortcode_regex() . '/', $instance['text'] ) ) {
+                       return true;
+               }
+
+               // In the rare case that DOMDocument is not available we cannot reliably sniff content and so we assume legacy.
+               if ( ! class_exists( 'DOMDocument' ) ) {
+                       // @codeCoverageIgnoreStart
+                       return true;
+                       // @codeCoverageIgnoreEnd
+               }
+
+               $doc = new DOMDocument();
+               $doc->loadHTML( sprintf(
+                       '<html><head><meta charset="%s"></head><body>%s</body></html>',
+                       esc_attr( get_bloginfo( 'charset' ) ),
+                       $instance['text']
+               ) );
+               $body = $doc->getElementsByTagName( 'body' )->item( 0 );
+
+               // See $allowedposttags.
+               $safe_elements_attributes = array(
+                       'strong' => array(),
+                       'em' => array(),
+                       'b' => array(),
+                       'i' => array(),
+                       'u' => array(),
+                       's' => array(),
+                       'ul' => array(),
+                       'ol' => array(),
+                       'li' => array(),
+                       'hr' => array(),
+                       'abbr' => array(),
+                       'acronym' => array(),
+                       'code' => array(),
+                       'dfn' => array(),
+                       'a' => array(
+                               'href' => true,
+                       ),
+                       'img' => array(
+                               'src' => true,
+                               'alt' => true,
+                       ),
+               );
+               $safe_empty_elements = array( 'img', 'hr', 'iframe' );
+
+               foreach ( $body->getElementsByTagName( '*' ) as $element ) {
+                       /** @var DOMElement $element */
+                       $tag_name = strtolower( $element->nodeName );
+
+                       // If the element is not safe, then the instance is legacy.
+                       if ( ! isset( $safe_elements_attributes[ $tag_name ] ) ) {
+                               return true;
+                       }
+
+                       // If the element is not safely empty and it has empty contents, then legacy mode.
+                       if ( ! in_array( $tag_name, $safe_empty_elements, true ) && '' === trim( $element->textContent ) ) {
+                               return true;
+                       }
+
+                       // If an attribute is not recognized as safe, then the instance is legacy.
+                       foreach ( $element->attributes as $attribute ) {
+                               /** @var DOMAttr $attribute */
+                               $attribute_name = strtolower( $attribute->nodeName );
+
+                               if ( ! isset( $safe_elements_attributes[ $tag_name ][ $attribute_name ] ) ) {
+                                       return true;
+                               }
+                       }
+               }
+
+               // Otherwise, the text contains no elements/attributes that TinyMCE could drop, and therefore the widget does not need legacy mode.
+               return false;
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Outputs the content for the current Text widget instance.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 2.8.0
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -79,7 +202,21 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $title = apply_filters( 'widget_title', empty( $instance['title'] ) ? '' : $instance['title'], $instance, $this->id_base );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $text = ! empty( $instance['text'] ) ? $instance['text'] : '';
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $is_visual_text_widget = ( isset( $instance['filter'] ) && 'content' === $instance['filter'] );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                /*
+                * Just-in-time temporarily upgrade Visual Text widget shortcode handling
+                * (with support added by plugin) from the widget_text filter to
+                * widget_text_content:11 to prevent wpautop from corrupting HTML output
+                * added by the shortcode.
+                */
+               $widget_text_do_shortcode_priority = has_filter( 'widget_text', 'do_shortcode' );
+               $should_upgrade_shortcode_handling = ( $is_visual_text_widget && false !== $widget_text_do_shortcode_priority );
+               if ( $should_upgrade_shortcode_handling ) {
+                       remove_filter( 'widget_text', 'do_shortcode', $widget_text_do_shortcode_priority );
+                       add_filter( 'widget_text_content', 'do_shortcode', 11 );
+               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 /**
</span><span class="cx" style="display: block; padding: 0 10px">                 * Filters the content of the Text widget.
</span><span class="cx" style="display: block; padding: 0 10px">                 *
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -113,6 +250,12 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                // Undo temporary upgrade of the plugin-supplied shortcode handling.
+               if ( $should_upgrade_shortcode_handling ) {
+                       remove_filter( 'widget_text_content', 'do_shortcode', 11 );
+                       add_filter( 'widget_text', 'do_shortcode', $widget_text_do_shortcode_priority );
+               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 echo $args['before_widget'];
</span><span class="cx" style="display: block; padding: 0 10px">                if ( ! empty( $title ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        echo $args['before_title'] . $title . $args['after_title'];
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -145,12 +288,20 @@
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                /*
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                 * Re-use legacy 'filter' (wpautop) property to now indicate content filters will always apply.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+          * If the Text widget is in legacy mode, then a hidden input will indicate this
+                * and the new content value for the filter prop will by bypassed. Otherwise,
+                * re-use legacy 'filter' (wpautop) property to now indicate content filters will always apply.
</ins><span class="cx" style="display: block; padding: 0 10px">                  * Prior to 4.8, this is a boolean value used to indicate whether or not wpautop should be
</span><span class="cx" style="display: block; padding: 0 10px">                 * applied. By re-using this property, downgrading WordPress from 4.8 to 4.7 will ensure
</span><span class="cx" style="display: block; padding: 0 10px">                 * that the content for Text widgets created with TinyMCE will continue to get wpautop.
</span><span class="cx" style="display: block; padding: 0 10px">                 */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $instance['filter'] = 'content';
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         if ( isset( $new_instance['legacy'] ) || isset( $old_instance['legacy'] ) || ( isset( $new_instance['filter'] ) && 'content' !== $new_instance['filter'] ) ) {
+                       $instance['filter'] = ! empty( $new_instance['filter'] );
+                       $instance['legacy'] = true;
+               } else {
+                       $instance['filter'] = 'content';
+                       unset( $instance['legacy'] );
+               }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                return $instance;
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -171,6 +322,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 2.8.0
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 4.8.0 Form only contains hidden inputs which are synced with JS template.
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @since 4.8.1 Restored original form to be displayed when in legacy mode.
</ins><span class="cx" style="display: block; padding: 0 10px">          * @access public
</span><span class="cx" style="display: block; padding: 0 10px">         * @see WP_Widget_Visual_Text::render_control_template_scripts()
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -186,9 +338,27 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        )
</span><span class="cx" style="display: block; padding: 0 10px">                );
</span><span class="cx" style="display: block; padding: 0 10px">                ?>
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                <input id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" class="title" type="hidden" value="<?php echo esc_attr( $instance['title'] ); ?>">
-               <input id="<?php echo $this->get_field_id( 'text' ); ?>" name="<?php echo $this->get_field_name( 'text' ); ?>" class="text" type="hidden" value="<?php echo esc_attr( $instance['text'] ); ?>">
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         <?php if ( ! $this->is_legacy_instance( $instance ) ) : ?>
+                       <input id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" class="title" type="hidden" value="<?php echo esc_attr( $instance['title'] ); ?>">
+                       <input id="<?php echo $this->get_field_id( 'text' ); ?>" name="<?php echo $this->get_field_name( 'text' ); ?>" class="text" type="hidden" value="<?php echo esc_attr( $instance['text'] ); ?>">
+               <?php else : ?>
+                       <input name="<?php echo $this->get_field_name( 'legacy' ); ?>" type="hidden" class="legacy" value="true">
+                       <p>
+                               <label for="<?php echo $this->get_field_id( 'title' ); ?>"><?php _e( 'Title:' ); ?></label>
+                               <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'] ); ?>"/>
+                       </p>
+                       <div class="notice inline notice-info notice-alt">
+                               <p><?php _e( 'This widget contains code that may work better in the new &#8220;Custom HTML&#8221; widget. How about trying that widget instead?' ); ?></p>
+                       </div>
+                       <p>
+                               <label for="<?php echo $this->get_field_id( 'text' ); ?>"><?php _e( 'Content:' ); ?></label>
+                               <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>
+                       </p>
+                       <p>
+                               <input id="<?php echo $this->get_field_id( 'filter' ); ?>" name="<?php echo $this->get_field_name( 'filter' ); ?>" type="checkbox"<?php checked( ! empty( $instance['filter'] ) ); ?> />&nbsp;<label for="<?php echo $this->get_field_id( 'filter' ); ?>"><?php _e( 'Automatically add paragraphs' ); ?></label>
+                       </p>
</ins><span class="cx" style="display: block; padding: 0 10px">                 <?php
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                endif;
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -198,6 +368,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @access public
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function render_control_template_scripts() {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $dismissed_pointers = explode( ',', (string) get_user_meta( get_current_user_id(), 'dismissed_wp_pointers', true ) );
</ins><span class="cx" style="display: block; padding: 0 10px">                 ?>
</span><span class="cx" style="display: block; padding: 0 10px">                <script type="text/html" id="tmpl-widget-text-control-fields">
</span><span class="cx" style="display: block; padding: 0 10px">                        <# var elementIdPrefix = 'el' + String( Math.random() ).replace( /\D/g, '' ) + '_' #>
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -205,6 +376,41 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                <label for="{{ elementIdPrefix }}title"><?php esc_html_e( 'Title:' ); ?></label>
</span><span class="cx" style="display: block; padding: 0 10px">                                <input id="{{ elementIdPrefix }}title" type="text" class="widefat title">
</span><span class="cx" style="display: block; padding: 0 10px">                        </p>
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+                       <?php if ( ! in_array( 'text_widget_custom_html', $dismissed_pointers, true ) ) : ?>
+                               <div hidden class="wp-pointer custom-html-widget-pointer wp-pointer-top">
+                                       <div class="wp-pointer-content">
+                                               <h3><?php _e( 'New Custom HTML Widget' ); ?></h3>
+                                               <?php if ( is_customize_preview() ) : ?>
+                                                       <p><?php _e( 'Hey, did you hear we have a &#8220;Custom HTML&#8221; widget now? You can find it by pressing the &#8220;<a class="add-widget" href="#">Add a Widget</a>&#8221; button and searching for &#8220;HTML&#8221;. Check it out to add some custom code to your site!' ); ?></p>
+                                               <?php else : ?>
+                                                       <p><?php _e( 'Hey, did you hear we have a &#8220;Custom HTML&#8221; 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>
+                                               <?php endif; ?>
+                                               <div class="wp-pointer-buttons">
+                                                       <a class="close" href="#"><?php _e( 'Dismiss' ); ?></a>
+                                               </div>
+                                       </div>
+                                       <div class="wp-pointer-arrow">
+                                               <div class="wp-pointer-arrow-inner"></div>
+                                       </div>
+                               </div>
+                       <?php endif; ?>
+
+                       <?php if ( ! in_array( 'text_widget_paste_html', $dismissed_pointers, true ) ) : ?>
+                               <div hidden class="wp-pointer paste-html-pointer wp-pointer-top">
+                                       <div class="wp-pointer-content">
+                                               <h3><?php _e( 'Did you just paste HTML?' ); ?></h3>
+                                               <p><?php _e( 'Hey there, looks like you just pasted HTML into the &#8220;Visual&#8221; tab of the Text widget. You may want to paste your code into the &#8220;Text&#8221; tab instead. Alternately, try out the new &#8220;Custom HTML&#8221; widget!' ); ?></p>
+                                               <div class="wp-pointer-buttons">
+                                                       <a class="close" href="#"><?php _e( 'Dismiss' ); ?></a>
+                                               </div>
+                                       </div>
+                                       <div class="wp-pointer-arrow">
+                                               <div class="wp-pointer-arrow-inner"></div>
+                                       </div>
+                               </div>
+                       <?php endif; ?>
+
</ins><span class="cx" style="display: block; padding: 0 10px">                         <p>
</span><span class="cx" style="display: block; padding: 0 10px">                                <label for="{{ elementIdPrefix }}text" class="screen-reader-text"><?php esc_html_e( 'Content:' ); ?></label>
</span><span class="cx" style="display: block; padding: 0 10px">                                <textarea id="{{ elementIdPrefix }}text" class="widefat text wp-editor-area" style="height: 200px" rows="16" cols="20"></textarea>
</span></span></pre></div>
<a id="trunktestsphpunittestswidgetstextwidgetphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/widgets/text-widget.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/widgets/text-widget.php 2017-07-14 16:18:42 UTC (rev 41049)
+++ trunk/tests/phpunit/tests/widgets/text-widget.php   2017-07-14 17:08:20 UTC (rev 41050)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -40,6 +40,20 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Test constructor method.
+        *
+        * @covers WP_Widget_Text::__construct
+        */
+       function test_construct() {
+               $widget = new WP_Widget_Text();
+               $this->assertEquals( 'text', $widget->id_base );
+               $this->assertEquals( 'widget_text', $widget->widget_options['classname'] );
+               $this->assertTrue( $widget->widget_options['customize_selective_refresh'] );
+               $this->assertEquals( 400, $widget->control_options['width'] );
+               $this->assertEquals( 350, $widget->control_options['height'] );
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Test enqueue_admin_scripts method.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @covers WP_Widget_Text::_register
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -122,6 +136,78 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Example shortcode content to test for wpautop corruption.
+        *
+        * @var string
+        */
+       protected $example_shortcode_content = "<p>One\nTwo\n\nThree</p>\n<script>\ndocument.write('Test1');\n\ndocument.write('Test2');\n</script>";
+
+       /**
+        * Do example shortcode.
+        *
+        * @return string Shortcode content.
+        */
+       function do_example_shortcode() {
+               return $this->example_shortcode_content;
+       }
+
+       /**
+        * Test widget method when a plugin has added shortcode support.
+        *
+        * @covers WP_Widget_Text::widget
+        */
+       function test_widget_shortcodes() {
+               $args = array(
+                       'before_title'  => '<h2>',
+                       'after_title'   => "</h2>\n",
+                       'before_widget' => '<section>',
+                       'after_widget'  => "</section>\n",
+               );
+               $widget = new WP_Widget_Text();
+               add_filter( 'widget_text', 'do_shortcode' );
+               add_shortcode( 'example', array( $this, 'do_example_shortcode' ) );
+
+               $base_instance = array(
+                       'title' => 'Example',
+                       'text' => "This is an example:\n\n[example]",
+                       'filter' => false,
+               );
+
+               // Legacy Text Widget.
+               $instance = array_merge( $base_instance, array(
+                       'filter' => false,
+               ) );
+               ob_start();
+               $widget->widget( $args, $instance );
+               $output = ob_get_clean();
+               $this->assertContains( $this->example_shortcode_content, $output, 'Shortcode was applied without wpautop corrupting it.' );
+               $this->assertEquals( 10, has_filter( 'widget_text', 'do_shortcode' ), 'Filter was restored.' );
+
+               // Visual Text Widget.
+               $instance = array_merge( $base_instance, array(
+                       'filter' => 'content',
+               ) );
+               ob_start();
+               $widget->widget( $args, $instance );
+               $output = ob_get_clean();
+               $this->assertContains( $this->example_shortcode_content, $output, 'Shortcode was applied without wpautop corrupting it.' );
+               $this->assertEquals( 10, has_filter( 'widget_text', 'do_shortcode' ), 'Filter was restored.' );
+               $this->assertFalse( has_filter( 'widget_text_content', 'do_shortcode' ), 'Filter was removed.' );
+
+               // Visual Text Widget with properly-used widget_text_content filter.
+               remove_filter( 'widget_text', 'do_shortcode' );
+               add_filter( 'widget_text_content', 'do_shortcode', 11 );
+               $instance = array_merge( $base_instance, array(
+                       'filter' => 'content',
+               ) );
+               ob_start();
+               $widget->widget( $args, $instance );
+               $output = ob_get_clean();
+               $this->assertContains( $this->example_shortcode_content, $output, 'Shortcode was applied without wpautop corrupting it.' );
+               $this->assertFalse( has_filter( 'widget_text', 'do_shortcode' ), 'Filter was not erroneously restored.' );
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Filters the content of the Text widget.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @param string         $widget_text The widget content.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -152,8 +238,149 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Test is_legacy_instance method.
+        *
+        * @covers WP_Widget_Text::is_legacy_instance
+        */
+       function test_is_legacy_instance() {
+               $widget = new WP_Widget_Text();
+               $base_instance = array(
+                       'title' => 'Title',
+                       'text' => "Hello\n\nWorld",
+               );
+
+               $instance = array_merge( $base_instance, array(
+                       'legacy' => true,
+               ) );
+               $this->assertTrue( $widget->is_legacy_instance( $instance ), 'Legacy when legacy prop is present.' );
+
+               $instance = array_merge( $base_instance, array(
+                       'filter' => 'content',
+               ) );
+               $this->assertFalse( $widget->is_legacy_instance( $instance ), 'Not legacy when filter is explicitly content.' );
+
+               $instance = array_merge( $base_instance, array(
+                       'text' => '',
+                       'filter' => true,
+               ) );
+               $this->assertFalse( $widget->is_legacy_instance( $instance ), 'Not legacy when text is empty.' );
+
+               $instance = array_merge( $base_instance, array(
+                       'text' => "One\nTwo",
+                       'filter' => false,
+               ) );
+               $this->assertTrue( $widget->is_legacy_instance( $instance ), 'Legacy when not-wpautop and there are line breaks.' );
+
+               $instance = array_merge( $base_instance, array(
+                       'text' => "One\n\nTwo",
+                       'filter' => false,
+               ) );
+               $this->assertTrue( $widget->is_legacy_instance( $instance ), 'Legacy when not-wpautop and there are paragraph breaks.' );
+
+               $instance = array_merge( $base_instance, array(
+                       'text' => "One\nTwo",
+                       'filter' => true,
+               ) );
+               $this->assertFalse( $widget->is_legacy_instance( $instance ), 'Not automatically legacy when wpautop and there are line breaks.' );
+
+               $instance = array_merge( $base_instance, array(
+                       'text' => "One\n\nTwo",
+                       'filter' => true,
+               ) );
+               $this->assertFalse( $widget->is_legacy_instance( $instance ), 'Not automatically legacy when wpautop and there are paragraph breaks.' );
+
+               $instance = array_merge( $base_instance, array(
+                       'text' => 'Test<!-- comment -->',
+                       'filter' => true,
+               ) );
+               $this->assertTrue( $widget->is_legacy_instance( $instance ), 'Legacy when HTML comment is present.' );
+
+               $instance = array_merge( $base_instance, array(
+                       'text' => 'Here is a [gallery]',
+                       'filter' => true,
+               ) );
+               $this->assertTrue( $widget->is_legacy_instance( $instance ), 'Legacy mode when a shortcode is present.' );
+
+               // Check text examples that will not migrate to TinyMCE.
+               $legacy_text_examples = array(
+                       '<span class="hello"></span>',
+                       '<span></span>',
+                       "<ul>\n<li><a href=\"#\" class=\"location\"></a>List Item 1</li>\n<li><a href=\"#\" class=\"location\"></a>List Item 2</li>\n</ul>",
+                       '<a href="#" class="map"></a>',
+                       "<script>\n\\Line one\n\n\\Line two</script>",
+                       "<style>body {\ncolor:red;\n}</style>",
+                       '<span class="fa fa-cc-discover fa-2x" aria-hidden="true"></span>',
+                       "<p>\nStay updated with our latest news and specials. We never sell your information and you can unsubscribe at any time.\n</p>\n\n<div class=\"custom-form-class\">\n\t<form action=\"#\" method=\"post\" name=\"mc-embedded-subscribe-form\">\n\n\t\t<label class=\"screen-reader-text\" for=\"mce-EMAIL-b\">Email </label>\n\t\t<input id=\"mce-EMAIL-b\" class=\"required email\" name=\"EMAIL\" required=\"\" type=\"email\" value=\"\" placeholder=\"Email Address*\" />\n\n\t\t<input class=\"button\" name=\"subscribe\" type=\"submit\" value=\"Go!\" />\n\n\t</form>\n</div>",
+                       '<span class="sectiondown"><a href="#front-page-3"><i class="fa fa-chevron-circle-down"></i></a></span>',
+               );
+               foreach ( $legacy_text_examples as $legacy_text_example ) {
+                       $instance = array_merge( $base_instance, array(
+                               'text' => $legacy_text_example,
+                               'filter' => true,
+                       ) );
+                       $this->assertTrue( $widget->is_legacy_instance( $instance ), 'Legacy when wpautop and there is HTML that is not liable to be mutated.' );
+
+                       $instance = array_merge( $base_instance, array(
+                               'text' => $legacy_text_example,
+                               'filter' => false,
+                       ) );
+                       $this->assertTrue( $widget->is_legacy_instance( $instance ), 'Legacy when not-wpautop and there is HTML that is not liable to be mutated.' );
+               }
+
+               // Check text examples that will migrate to TinyMCE, where elements and attributes are not in whitelist.
+               $migratable_text_examples = array(
+                       'Check out <a href="http://example.com">Example</a>',
+                       '<img src="http://example.com/img.jpg" alt="Img">',
+                       '<strong><em>Hello</em></strong>',
+                       '<b><i><u><s>Hello</s></u></i></b>',
+                       "<ul>\n<li>One</li>\n<li>One</li>\n<li>One</li>\n</ul>",
+                       "<ol>\n<li>One</li>\n<li>One</li>\n<li>One</li>\n</ol>",
+                       "Text\n<hr>\nAddendum",
+                       "Look at this code:\n\n<code>echo 'Hello World!';</code>",
+               );
+               foreach ( $migratable_text_examples as $migratable_text_example ) {
+                       $instance = array_merge( $base_instance, array(
+                               'text' => $migratable_text_example,
+                               'filter' => true,
+                       ) );
+                       $this->assertFalse( $widget->is_legacy_instance( $instance ), 'Legacy when wpautop and there is HTML that is not liable to be mutated.' );
+               }
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Test update method.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @covers WP_Widget_Text::form
+        */
+       function test_form() {
+               $widget = new WP_Widget_Text();
+               $instance = array(
+                       'title' => 'Title',
+                       'text' => 'Text',
+                       'filter' => false,
+                       'legacy' => true,
+               );
+               $this->assertTrue( $widget->is_legacy_instance( $instance ) );
+               ob_start();
+               $widget->form( $instance );
+               $form = ob_get_clean();
+               $this->assertContains( 'class="legacy"', $form );
+
+               $instance = array(
+                       'title' => 'Title',
+                       'text' => 'Text',
+                       'filter' => 'content',
+               );
+               $this->assertFalse( $widget->is_legacy_instance( $instance ) );
+               ob_start();
+               $widget->form( $instance );
+               $form = ob_get_clean();
+               $this->assertNotContains( 'class="legacy"', $form );
+       }
+
+       /**
+        * Test update method.
+        *
</ins><span class="cx" style="display: block; padding: 0 10px">          * @covers WP_Widget_Text::update
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        function test_update() {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -161,21 +388,21 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $instance = array(
</span><span class="cx" style="display: block; padding: 0 10px">                        'title' => "The\nTitle",
</span><span class="cx" style="display: block; padding: 0 10px">                        'text'  => "The\n\nText",
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        'filter' => false,
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 'filter' => 'content',
</ins><span class="cx" style="display: block; padding: 0 10px">                 );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                wp_set_current_user( $this->factory()->user->create( array(
</span><span class="cx" style="display: block; padding: 0 10px">                        'role' => 'administrator',
</span><span class="cx" style="display: block; padding: 0 10px">                ) ) );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                // Should return valid instance.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         // Should return valid instance in legacy mode since filter=false and there are line breaks.
</ins><span class="cx" style="display: block; padding: 0 10px">                 $expected = array(
</span><span class="cx" style="display: block; padding: 0 10px">                        'title'  => sanitize_text_field( $instance['title'] ),
</span><span class="cx" style="display: block; padding: 0 10px">                        'text'   => $instance['text'],
</span><span class="cx" style="display: block; padding: 0 10px">                        'filter' => 'content',
</span><span class="cx" style="display: block; padding: 0 10px">                );
</span><span class="cx" style="display: block; padding: 0 10px">                $result = $widget->update( $instance, array() );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->assertEquals( $result, $expected );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $this->assertEquals( $expected, $result );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->assertTrue( ! empty( $expected['filter'] ), 'Expected filter prop to be truthy, to handle case where 4.8 is downgraded to 4.7.' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // Make sure KSES is applying as expected.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -184,7 +411,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $instance['text'] = '<script>alert( "Howdy!" );</script>';
</span><span class="cx" style="display: block; padding: 0 10px">                $expected['text'] = $instance['text'];
</span><span class="cx" style="display: block; padding: 0 10px">                $result = $widget->update( $instance, array() );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->assertEquals( $result, $expected );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $this->assertEquals( $expected, $result );
</ins><span class="cx" style="display: block; padding: 0 10px">                 remove_filter( 'map_meta_cap', array( $this, 'grant_unfiltered_html_cap' ) );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                add_filter( 'map_meta_cap', array( $this, 'revoke_unfiltered_html_cap' ), 10, 2 );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -192,11 +419,63 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $instance['text'] = '<script>alert( "Howdy!" );</script>';
</span><span class="cx" style="display: block; padding: 0 10px">                $expected['text'] = wp_kses_post( $instance['text'] );
</span><span class="cx" style="display: block; padding: 0 10px">                $result = $widget->update( $instance, array() );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->assertEquals( $result, $expected );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $this->assertEquals( $expected, $result );
</ins><span class="cx" style="display: block; padding: 0 10px">                 remove_filter( 'map_meta_cap', array( $this, 'revoke_unfiltered_html_cap' ), 10 );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Test update for legacy widgets.
+        *
+        * @covers WP_Widget_Text::update
+        */
+       function test_update_legacy() {
+               $widget = new WP_Widget_Text();
+
+               // Updating a widget with explicit filter=true persists with legacy mode.
+               $instance = array(
+                       'title' => 'Legacy',
+                       'text' => 'Text',
+                       'filter' => true,
+               );
+               $result = $widget->update( $instance, array() );
+               $expected = array_merge( $instance, array(
+                       'legacy' => true,
+                       'filter' => true,
+               ) );
+               $this->assertEquals( $expected, $result );
+
+               // Updating a widget with explicit filter=false persists with legacy mode.
+               $instance['filter'] = false;
+               $result = $widget->update( $instance, array() );
+               $expected = array_merge( $instance, array(
+                       'legacy' => true,
+                       'filter' => false,
+               ) );
+               $this->assertEquals( $expected, $result );
+
+               // Updating a widget in legacy form results in filter=false when checkbox not checked.
+               $instance['filter'] = true;
+               $result = $widget->update( $instance, array() );
+               $expected = array_merge( $instance, array(
+                       'legacy' => true,
+                       'filter' => true,
+               ) );
+               $this->assertEquals( $expected, $result );
+
+               // Updating a widget that previously had legacy form results in filter persisting.
+               unset( $instance['legacy'] );
+               $instance['filter'] = true;
+               $result = $widget->update( $instance, array(
+                       'legacy' => true,
+               ) );
+               $expected = array_merge( $instance, array(
+                       'legacy' => true,
+                       'filter' => true,
+               ) );
+               $this->assertEquals( $expected, $result );
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Grant unfiltered_html cap via map_meta_cap.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @param array  $caps    Returns the user's actual capabilities.
</span></span></pre>
</div>
</div>

</body>
</html>