<!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>[33071] trunk: Customizer: Fix saving menus with empty names or names that are already used.</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/33071">33071</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/33071","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>2015-07-03 20:46:48 +0000 (Fri, 03 Jul 2015)</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'>Customizer: Fix saving menus with empty names or names that are already used.

Adds validation for initially-supplied nav menu name, blocking empty names from being supplied. If later an empty name is supplied and the nav menu is saved, the name "(unnamed)" will be supplied instead and supplied back to the client. If a name is supplied for the menu which is currently used by another menu, then the name conflict is resolved by adding a numerical counter similar to how `post_name` conflicts are resolved. Includes unit tests.

Fixes <a href="https://core.trac.wordpress.org/ticket/32760">#32760</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpadmincsscustomizenavmenuscss">trunk/src/wp-admin/css/customize-nav-menus.css</a></li>
<li><a href="#trunksrcwpadminjscustomizenavmenusjs">trunk/src/wp-admin/js/customize-nav-menus.js</a></li>
<li><a href="#trunksrcwpincludesclasswpcustomizenavmenusphp">trunk/src/wp-includes/class-wp-customize-nav-menus.php</a></li>
<li><a href="#trunksrcwpincludesclasswpcustomizesettingphp">trunk/src/wp-includes/class-wp-customize-setting.php</a></li>
<li><a href="#trunktestsphpunittestscustomizenavmenusettingphp">trunk/tests/phpunit/tests/customize/nav-menu-setting.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpadmincsscustomizenavmenuscss"></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/customize-nav-menus.css</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/css/customize-nav-menus.css    2015-07-03 20:36:59 UTC (rev 33070)
+++ trunk/src/wp-admin/css/customize-nav-menus.css      2015-07-03 20:46:48 UTC (rev 33071)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -654,7 +654,9 @@
</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"> #custom-menu-item-name.invalid,
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-#custom-menu-item-url.invalid {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+#custom-menu-item-url.invalid,
+.menu-name-field.invalid,
+.menu-name-field.invalid:focus {
</ins><span class="cx" style="display: block; padding: 0 10px">         border: 1px solid #f00;
</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="trunksrcwpadminjscustomizenavmenusjs"></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/customize-nav-menus.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/js/customize-nav-menus.js      2015-07-03 20:36:59 UTC (rev 33070)
+++ trunk/src/wp-admin/js/customize-nav-menus.js        2015-07-03 20:46:48 UTC (rev 33071)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -833,6 +833,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                button.removeClass( 'open' );
</span><span class="cx" style="display: block; padding: 0 10px">                                button.attr( 'aria-expanded', 'false' );
</span><span class="cx" style="display: block; padding: 0 10px">                                content.slideUp( 'fast' );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                content.find( '.menu-name-field' ).removeClass( 'invalid' );
</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">@@ -869,7 +870,7 @@
</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">                                menuId = matches[1];
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                option = new Option( setting().name, menuId );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         option = new Option( displayNavMenuName( setting().name ), menuId );
</ins><span class="cx" style="display: block; padding: 0 10px">                                 control.container.find( 'select' ).append( option );
</span><span class="cx" style="display: block; padding: 0 10px">                        });
</span><span class="cx" style="display: block; padding: 0 10px">                        api.bind( 'remove', function( setting ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -895,7 +896,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                        }
</span><span class="cx" style="display: block; padding: 0 10px">                                        control.container.find( 'option[value=' + menuId + ']' ).remove();
</span><span class="cx" style="display: block; padding: 0 10px">                                } else {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        control.container.find( 'option[value=' + menuId + ']' ).text( setting().name );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 control.container.find( 'option[value=' + menuId + ']' ).text( displayNavMenuName( setting().name ) );
</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">@@ -1605,7 +1606,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">                 */
</span><span class="cx" style="display: block; padding: 0 10px">                ready: function() {
</span><span class="cx" style="display: block; padding: 0 10px">                        var control = this,
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                menuId = control.params.menu_id;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         menuId = control.params.menu_id,
+                               menu = control.setting(),
+                               name;
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        if ( 'undefined' === typeof this.params.menu_id ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                throw new Error( 'params.menu_id was not defined' );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1636,17 +1639,19 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        this._setupTitle();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        // Add menu to Custom Menu widgets.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        if ( control.setting() ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 if ( menu ) {
+                               name = displayNavMenuName( menu.name );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                                 api.control.each( function( widgetControl ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                        if ( ! widgetControl.extended( api.controlConstructor.widget_form ) || 'nav_menu' !== widgetControl.params.widget_id_base ) {
</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">                                        var select = widgetControl.container.find( 'select' );
</span><span class="cx" style="display: block; padding: 0 10px">                                        if ( select.find( 'option[value=' + String( menuId ) + ']' ).length === 0 ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                                select.append( new Option( control.setting().name, menuId ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                         select.append( new Option( name, menuId ) );
</ins><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">-                                $( '#available-widgets-list .widget-inside:has(input.id_base[value=nav_menu]) select:first' ).append( new Option( control.setting().name, menuId ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         $( '#available-widgets-list .widget-inside:has(input.id_base[value=nav_menu]) select:first' ).append( new Option( name, menuId ) );
</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">@@ -1677,18 +1682,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">                        control.setting.bind( function( to ) {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                var name;
</ins><span class="cx" style="display: block; padding: 0 10px">                                 if ( false === to ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                        control._handleDeletion();
</span><span class="cx" style="display: block; padding: 0 10px">                                } else {
</span><span class="cx" style="display: block; padding: 0 10px">                                        // Update names in the Custom Menu widgets.
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                        name = displayNavMenuName( to.name );
</ins><span class="cx" style="display: block; padding: 0 10px">                                         api.control.each( function( widgetControl ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                                if ( ! widgetControl.extended( api.controlConstructor.widget_form ) || 'nav_menu' !== widgetControl.params.widget_id_base ) {
</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">                                                var select = widgetControl.container.find( 'select' );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                                select.find( 'option[value=' + String( menuId ) + ']' ).text( to.name );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                         select.find( 'option[value=' + String( menuId ) + ']' ).text( name );
</ins><span class="cx" style="display: block; padding: 0 10px">                                         });
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        $( '#available-widgets-list .widget-inside:has(input.id_base[value=nav_menu]) select:first option[value=' + String( menuId ) + ']' ).text( to.name );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 $( '#available-widgets-list .widget-inside:has(input.id_base[value=nav_menu]) select:first option[value=' + String( menuId ) + ']' ).text( name );
</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">@@ -1847,7 +1854,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                        if ( ! selectedMenuId || ! menuSetting || ! menuSetting() ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                                container.find( '.theme-location-set' ).hide();
</span><span class="cx" style="display: block; padding: 0 10px">                                        } else {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                                container.find( '.theme-location-set' ).show().find( 'span' ).text( menuSetting().name );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                         container.find( '.theme-location-set' ).show().find( 'span' ).text( displayNavMenuName( menuSetting().name ) );
</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">@@ -1879,41 +1886,39 @@
</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><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                // Empty names are not allowed (will not be saved), don't update to one.
-                               if ( menu.name ) {
-                                       var section = control.container.closest( '.accordion-section' ),
-                                               menuId = control.params.menu_id,
-                                               controlTitle = section.find( '.accordion-section-title' ),
-                                               sectionTitle = section.find( '.customize-section-title h3' ),
-                                               location = section.find( '.menu-in-location' ),
-                                               action = sectionTitle.find( '.customize-action' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         var section = control.container.closest( '.accordion-section' ),
+                                       menuId = control.params.menu_id,
+                                       controlTitle = section.find( '.accordion-section-title' ),
+                                       sectionTitle = section.find( '.customize-section-title h3' ),
+                                       location = section.find( '.menu-in-location' ),
+                                       action = sectionTitle.find( '.customize-action' ),
+                                       name = displayNavMenuName( menu.name );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        // Update the control title
-                                       controlTitle.text( menu.name );
-                                       if ( location.length ) {
-                                               location.appendTo( controlTitle );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         // Update the control title
+                               controlTitle.text( name );
+                               if ( location.length ) {
+                                       location.appendTo( controlTitle );
+                               }
+
+                               // Update the section title
+                               sectionTitle.text( name );
+                               if ( action.length ) {
+                                       action.prependTo( sectionTitle );
+                               }
+
+                               // Update the nav menu name in location selects.
+                               api.control.each( function( control ) {
+                                       if ( /^nav_menu_locations\[/.test( control.id ) ) {
+                                               control.container.find( 'option[value=' + menuId + ']' ).text( name );
</ins><span class="cx" style="display: block; padding: 0 10px">                                         }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                } );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        // Update the section title
-                                       sectionTitle.text( menu.name );
-                                       if ( action.length ) {
-                                               action.prependTo( sectionTitle );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         // Update the nav menu name in all location checkboxes.
+                               section.find( '.customize-control-checkbox input' ).each( function() {
+                                       if ( $( this ).prop( 'checked' ) ) {
+                                               $( '.current-menu-location-name-' + $( this ).data( 'location-id' ) ).text( name );
</ins><span class="cx" style="display: block; padding: 0 10px">                                         }
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-
-                                       // Update the nav menu name in location selects.
-                                       api.control.each( function( control ) {
-                                               if ( /^nav_menu_locations\[/.test( control.id ) ) {
-                                                       control.container.find( 'option[value=' + menuId + ']' ).text( menu.name );
-                                               }
-                                       } );
-
-                                       // Update the nav menu name in all location checkboxes.
-                                       section.find( '.customize-control-checkbox input' ).each( function() {
-                                               if ( $( this ).prop( 'checked' ) ) {
-                                                       $( '.current-menu-location-name-' + $( this ).data( 'location-id' ) ).text( menu.name );
-                                               }
-                                       } );
-                               }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         } );
</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">@@ -2151,8 +2156,6 @@
</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">                 * Create the new menu with the name supplied.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                 *
-                * @returns {boolean}
</del><span class="cx" style="display: block; padding: 0 10px">                  */
</span><span class="cx" style="display: block; padding: 0 10px">                submit: function() {
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2164,6 +2167,12 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                customizeId,
</span><span class="cx" style="display: block; padding: 0 10px">                                placeholderId = api.Menus.generatePlaceholderAutoIncrementId();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        if ( ! name ) {
+                               nameInput.addClass( 'invalid' );
+                               nameInput.focus();
+                               return;
+                       }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                         customizeId = 'nav_menu[' + String( placeholderId ) + ']';
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        // Register the menu control setting.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2189,7 +2198,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                params: {
</span><span class="cx" style="display: block; padding: 0 10px">                                        id: customizeId,
</span><span class="cx" style="display: block; padding: 0 10px">                                        panel: 'nav_menus',
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        title: name,
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 title: displayNavMenuName( name ),
</ins><span class="cx" style="display: block; padding: 0 10px">                                         customizeAction: api.Menus.data.l10n.customizingMenus,
</span><span class="cx" style="display: block; padding: 0 10px">                                        type: 'nav_menu',
</span><span class="cx" style="display: block; padding: 0 10px">                                        priority: 10,
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2200,6 +2209,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        // Clear name field.
</span><span class="cx" style="display: block; padding: 0 10px">                        nameInput.val( '' );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        nameInput.removeClass( 'invalid' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        wp.a11y.speak( api.Menus.data.l10n.menuAdded );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2269,7 +2279,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                var insertedMenuIdMapping = {};
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                _( data.nav_menu_updates ).each(function( update ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        var oldCustomizeId, newCustomizeId, oldSetting, newSetting, settingValue, oldSection, newSection;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 var oldCustomizeId, newCustomizeId, customizeId, oldSetting, newSetting, setting, settingValue, oldSection, newSection, wasSaved;
</ins><span class="cx" style="display: block; padding: 0 10px">                         if ( 'inserted' === update.status ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                if ( ! update.previous_term_id ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                        throw new Error( 'Expected previous_term_id' );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2291,7 +2301,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                if ( ! settingValue ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                        throw new Error( 'Did not expect setting to be empty (deleted).' );
</span><span class="cx" style="display: block; padding: 0 10px">                                }
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                settingValue = _.clone( settingValue );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         settingValue = $.extend( _.clone( settingValue ), update.saved_value );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                insertedMenuIdMapping[ update.previous_term_id ] = update.term_id;
</span><span class="cx" style="display: block; padding: 0 10px">                                newCustomizeId = 'nav_menu[' + String( update.term_id ) + ']';
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2349,6 +2359,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">                                // @todo Update the Custom Menu selects, ensuring the newly-inserted IDs are used for any that have selected a placeholder menu.
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        } else if ( 'updated' === update.status ) {
+                               customizeId = 'nav_menu[' + String( update.term_id ) + ']';
+                               if ( ! api.has( customizeId ) ) {
+                                       throw new Error( 'Expected setting to exist: ' + customizeId );
+                               }
+
+                               // Make sure the setting gets updated with its sanitized server value (specifically the conflict-resolved name).
+                               setting = api( customizeId );
+                               if ( ! _.isEqual( update.saved_value, setting.get() ) ) {
+                                       wasSaved = api.state( 'saved' ).get();
+                                       setting.set( update.saved_value );
+                                       setting._dirty = false;
+                                       api.state( 'saved' ).set( wasSaved );
+                               }
</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">@@ -2496,4 +2520,17 @@
</span><span class="cx" style="display: block; padding: 0 10px">                return 'nav_menu_item[' + menuItemId + ']';
</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">+        /**
+        * Apply sanitize_text_field()-like logic to the supplied name, returning a
+        * "unnammed" fallback string if the name is then empty.
+        *
+        * @param {string} name
+        * @returns {string}
+        */
+       function displayNavMenuName( name ) {
+               name = $( '<div>' ).text( name ).html(); // Emulate esc_html() which is used in wp-admin/nav-menus.php.
+               name = $.trim( name );
+               return name || api.Menus.data.l10n.unnamed;
+       }
+
</ins><span class="cx" style="display: block; padding: 0 10px"> })( wp.customize, wp, jQuery );
</span></span></pre></div>
<a id="trunksrcwpincludesclasswpcustomizenavmenusphp"></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/class-wp-customize-nav-menus.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/class-wp-customize-nav-menus.php    2015-07-03 20:36:59 UTC (rev 33070)
+++ trunk/src/wp-includes/class-wp-customize-nav-menus.php      2015-07-03 20:46:48 UTC (rev 33071)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -281,6 +281,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        'itemTypes'            => $this->available_item_types(),
</span><span class="cx" style="display: block; padding: 0 10px">                        'l10n'                 => array(
</span><span class="cx" style="display: block; padding: 0 10px">                                'untitled'          => _x( '(no label)', 'missing menu item navigation label' ),
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                'unnamed'           => _x( '(unnamed)', 'Missing menu name.' ),
</ins><span class="cx" style="display: block; padding: 0 10px">                                 'custom_label'      => __( 'Custom Link' ),
</span><span class="cx" style="display: block; padding: 0 10px">                                /* translators: %s: Current menu location */
</span><span class="cx" style="display: block; padding: 0 10px">                                'menuLocation'      => __( '(Currently set to: %s)' ),
</span></span></pre></div>
<a id="trunksrcwpincludesclasswpcustomizesettingphp"></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/class-wp-customize-setting.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/class-wp-customize-setting.php      2015-07-03 20:36:59 UTC (rev 33070)
+++ trunk/src/wp-includes/class-wp-customize-setting.php        2015-07-03 20:46:48 UTC (rev 33071)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1182,6 +1182,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                if ( false === $nav_menu_setting->save() ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                        $this->update_status = 'error';
</span><span class="cx" style="display: block; padding: 0 10px">                                        $this->update_error  = new WP_Error( 'nav_menu_setting_failure' );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                        return;
</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 ( $nav_menu_setting->previous_term_id !== intval( $value['nav_menu_term_id'] ) ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1207,6 +1208,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                if ( false === $parent_nav_menu_item_setting->save() ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                        $this->update_status = 'error';
</span><span class="cx" style="display: block; padding: 0 10px">                                        $this->update_error  = new WP_Error( 'nav_menu_item_setting_failure' );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                        return;
</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 ( $parent_nav_menu_item_setting->previous_post_id !== intval( $value['menu_item_parent'] ) ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1611,6 +1613,10 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $value['parent']      = max( 0, intval( $value['parent'] ) );
</span><span class="cx" style="display: block; padding: 0 10px">                $value['auto_add']    = ! empty( $value['auto_add'] );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                if ( '' === $value['name'] ) {
+                       $value['name'] = _x( '(unnamed)', 'Missing menu name.' );
+               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 /** This filter is documented in wp-includes/class-wp-customize-setting.php */
</span><span class="cx" style="display: block; padding: 0 10px">                return apply_filters( "customize_sanitize_{$this->id}", $value, $this );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1669,11 +1675,19 @@
</span><span class="cx" style="display: block; padding: 0 10px">                } else {
</span><span class="cx" style="display: block; padding: 0 10px">                        // Insert or update menu.
</span><span class="cx" style="display: block; padding: 0 10px">                        $menu_data = wp_array_slice_assoc( $value, array( 'description', 'parent' ) );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        if ( isset( $value['name'] ) ) {
-                               $menu_data['menu-name'] = $value['name'];
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 $menu_data['menu-name'] = $value['name'];
+
+                       $menu_id = $is_placeholder ? 0 : $this->term_id;
+                       $r = wp_update_nav_menu_object( $menu_id, $menu_data );
+                       $original_name = $menu_data['menu-name'];
+                       $name_conflict_suffix = 1;
+                       while ( is_wp_error( $r ) && 'menu_exists' === $r->get_error_code() ) {
+                               $name_conflict_suffix += 1;
+                               /* translators: 1: original menu name, 2: duplicate count */
+                               $menu_data['menu-name'] = sprintf( __( '%1$s (%2$d)' ), $original_name, $name_conflict_suffix );
+                               $r = wp_update_nav_menu_object( $menu_id, $menu_data );
</ins><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">-                        $r = wp_update_nav_menu_object( $is_placeholder ? 0 : $this->term_id, $menu_data );
</del><span class="cx" style="display: block; padding: 0 10px">                         if ( is_wp_error( $r ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                $this->update_status = 'error';
</span><span class="cx" style="display: block; padding: 0 10px">                                $this->update_error  = $r;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1764,6 +1778,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        'previous_term_id' => $this->previous_term_id,
</span><span class="cx" style="display: block; padding: 0 10px">                        'error'            => $this->update_error ? $this->update_error->get_error_code() : null,
</span><span class="cx" style="display: block; padding: 0 10px">                        'status'           => $this->update_status,
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        'saved_value'      => 'deleted' === $this->update_status ? null : $this->value(),
</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">                return $data;
</span></span></pre></div>
<a id="trunktestsphpunittestscustomizenavmenusettingphp"></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/customize/nav-menu-setting.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/customize/nav-menu-setting.php  2015-07-03 20:36:59 UTC (rev 33070)
+++ trunk/tests/phpunit/tests/customize/nav-menu-setting.php    2015-07-03 20:46:48 UTC (rev 33071)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -94,7 +94,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">        function test_construct_empty_menus() {
</span><span class="cx" style="display: block; padding: 0 10px">                do_action( 'customize_register', $this->wp_customize );
</span><span class="cx" style="display: block; padding: 0 10px">                $_wp_customize = $this->wp_customize;
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                unset($_wp_customize->nav_menus);
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         unset( $_wp_customize->nav_menus );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $exception = null;
</span><span class="cx" style="display: block; padding: 0 10px">                try {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -310,6 +310,10 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( 0, $sanitized['parent'] );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( true, $sanitized['auto_add'] );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEqualSets( array( 'name', 'description', 'parent', 'auto_add' ), array_keys( $sanitized ) );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+               $value['name'] = '    '; // Blank spaces.
+               $sanitized = $setting->sanitize( $value );
+               $this->assertEquals( '(unnamed)', $sanitized['name'] );
</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">@@ -360,6 +364,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertArrayHasKey( 'previous_term_id', $update_result );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertArrayHasKey( 'error', $update_result );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertArrayHasKey( 'status', $update_result );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $this->assertArrayHasKey( 'saved_value', $update_result );
+               $this->assertEquals( $new_value, $update_result['saved_value'] );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( $menu_id, $update_result['term_id'] );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertNull( $update_result['previous_term_id'] );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -410,6 +416,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertArrayHasKey( 'previous_term_id', $update_result );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertArrayHasKey( 'error', $update_result );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertArrayHasKey( 'status', $update_result );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $this->assertArrayHasKey( 'saved_value', $update_result );
+               $this->assertEquals( $setting->value(), $update_result['saved_value'] );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( $menu->term_id, $update_result['term_id'] );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( $menu_id, $update_result['previous_term_id'] );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -418,6 +426,31 @@
</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 saving a new name that conflicts with an existing nav menu's name.
+        *
+        * @see WP_Customize_Nav_Menu_Setting::update()
+        */
+       function test_save_inserted_conflicted_name() {
+               do_action( 'customize_register', $this->wp_customize );
+
+               $menu_name = 'Foo';
+               wp_update_nav_menu_object( 0, array( 'menu-name' => $menu_name ) );
+
+               $menu_id = -123;
+               $setting_id = "nav_menu[$menu_id]";
+               $setting = new WP_Customize_Nav_Menu_Setting( $this->wp_customize, $setting_id );
+               $this->wp_customize->set_post_value( $setting->id, array( 'name' => $menu_name ) );
+               $setting->save();
+
+               $expected_resolved_menu_name = "$menu_name (2)";
+               $new_menu = wp_get_nav_menu_object( $setting->term_id );
+               $this->assertEquals( $expected_resolved_menu_name, $new_menu->name );
+
+               $save_response = apply_filters( 'customize_save_response', array() );
+               $this->assertEquals( $expected_resolved_menu_name, $save_response['nav_menu_updates'][0]['saved_value']['name'] );
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Test protected update() method via the save() method, for deleted menu.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @see WP_Customize_Nav_Menu_Setting::update()
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -448,6 +481,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertArrayHasKey( 'previous_term_id', $update_result );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertArrayHasKey( 'error', $update_result );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertArrayHasKey( 'status', $update_result );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $this->assertArrayHasKey( 'saved_value', $update_result );
+               $this->assertNull( $update_result['saved_value'] );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( $menu_id, $update_result['term_id'] );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertNull( $update_result['previous_term_id'] );
</span></span></pre>
</div>
</div>

</body>
</html>