<!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>[31370] trunk: Customizer: Introduce an API to create WP_Customize_Settings for dynamically-created settings.</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/31370">31370</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/31370","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>ocean90</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2015-02-08 23:10:05 +0000 (Sun, 08 Feb 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: Introduce an API to create WP_Customize_Settings for dynamically-created settings.

* Introduce WP_Customize_Manager::add_dynamic_settings() to register dynamically-created settings.
* Introduce `customize_dynamic_setting_args` filter to pass an array of args to a dynamic setting's constructor.
* Add unit tests for WP_Customize_Manager and WP_Customize_Widgets.
* See WP_Customize_Widgets as an example.

props westonruter.
fixes <a href="https://core.trac.wordpress.org/ticket/30936">#30936</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpincludesclasswpcustomizemanagerphp">trunk/src/wp-includes/class-wp-customize-manager.php</a></li>
<li><a href="#trunksrcwpincludesclasswpcustomizesettingphp">trunk/src/wp-includes/class-wp-customize-setting.php</a></li>
<li><a href="#trunksrcwpincludesclasswpcustomizewidgetsphp">trunk/src/wp-includes/class-wp-customize-widgets.php</a></li>
<li><a href="#trunktestsphpunittestscustomizemanagerphp">trunk/tests/phpunit/tests/customize/manager.php</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#trunktestsphpunittestscustomizewidgetsphp">trunk/tests/phpunit/tests/customize/widgets.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpincludesclasswpcustomizemanagerphp"></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-manager.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/class-wp-customize-manager.php      2015-02-08 16:58:14 UTC (rev 31369)
+++ trunk/src/wp-includes/class-wp-customize-manager.php        2015-02-08 23:10:05 UTC (rev 31370)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -65,7 +65,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="cx" style="display: block; padding: 0 10px">         * Unsanitized values for Customize Settings parsed from $_POST['customized'].
</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 array|false
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @var array
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><span class="cx" style="display: block; padding: 0 10px">        private $_post_values;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -102,6 +102,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                add_action( 'wp_ajax_customize_save', array( $this, 'save' ) );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                add_action( 'customize_register',                 array( $this, 'register_controls' ) );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                add_action( 'customize_register',                 array( $this, 'register_dynamic_settings' ), 11 ); // allow code to create settings first
</ins><span class="cx" style="display: block; padding: 0 10px">                 add_action( 'customize_controls_init',            array( $this, 'prepare_controls' ) );
</span><span class="cx" style="display: block; padding: 0 10px">                add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_control_scripts' ) );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -110,11 +111,23 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * Return true if it's an AJAX request.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 3.4.0
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @since 4.2.0 Added $action param.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @param string|null $action whether the supplied Ajax action is being run.
</ins><span class="cx" style="display: block; padding: 0 10px">          * @return bool
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        public function doing_ajax() {
-               return isset( $_POST['customized'] ) || ( defined( 'DOING_AJAX' ) && DOING_AJAX );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ public function doing_ajax( $action = null ) {
+               $doing_ajax = ( defined( 'DOING_AJAX' ) && DOING_AJAX );
+               if ( ! $doing_ajax ) {
+                       return false;
+               }
+
+               if ( ! $action ) {
+                       return true;
+               } else {
+                       // Note: we can't just use doing_action( "wp_ajax_{$action}" ) because we need to check before admin-ajax.php gets to that point
+                       return isset( $_REQUEST['action'] ) && wp_unslash( $_REQUEST['action'] ) === $action;
+               }
</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">@@ -411,8 +424,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        if ( isset( $_POST['customized'] ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                $this->_post_values = json_decode( wp_unslash( $_POST['customized'] ), true );
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        if ( empty( $this->_post_values ) ) { // if not isset or of JSON error
-                               $this->_post_values = false;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 if ( empty( $this->_post_values ) ) { // if not isset or if JSON error
+                               $this->_post_values = array();
</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 ( empty( $this->_post_values ) ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -442,6 +455,19 @@
</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">+         * Override a setting's (unsanitized) value as found in any incoming $_POST['customized']
+        *
+        * @since 4.2.0
+        *
+        * @param string $setting_id  The ID for the WP_Customize_Setting instance.
+        * @param mixed $value
+        */
+       public function set_post_value( $setting_id, $value ) {
+               $this->unsanitized_post_values();
+               $this->_post_values[ $setting_id ] = $value;
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Print JavaScript settings.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 3.4.0
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -727,6 +753,65 @@
</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">+         * Register any dynamically-created settings, such as those from $_POST['customized'] that have no corresponding setting created.
+        *
+        * This is a mechanism to "wake up" settings that have been dynamically created
+        * on the frontend and have been sent to WordPress in $_POST['customized']. When WP
+        * loads, the dynamically-created settings then will get created and previewed
+        * even though they are not directly created statically with code.
+        *
+        * @since 4.2.0
+        *
+        * @param string[] $setting_ids The setting IDs to add.
+        * @return WP_Customize_Setting[] The settings added.
+        */
+       public function add_dynamic_settings( $setting_ids ) {
+               $new_settings = array();
+               foreach ( $setting_ids as $setting_id ) {
+                       // Skip settings already created
+                       if ( $this->get_setting( $setting_id ) ) {
+                               continue;
+                       }
+
+                       $setting_args = false;
+                       $setting_class = 'WP_Customize_Setting';
+
+                       /**
+                        * Filter a dynamic setting's constructor args.
+                        *
+                        * For a dynamic setting to be registered, this filter must be employed
+                        * to override the default false value with an array of args to pass to
+                        * the WP_Customize_Setting constructor.
+                        *
+                        * @since 4.2.0
+                        *
+                        * @param false|array $setting_args  The arguments to the WP_Customize_Setting constructor.
+                        * @param string      $setting_id    ID for dynamic setting, usually coming from $_POST['customized'].
+                        */
+                       $setting_args = apply_filters( 'customize_dynamic_setting_args', $setting_args, $setting_id );
+                       if ( false === $setting_args ) {
+                               continue;
+                       }
+
+                       /**
+                        * Allow non-statically created settings to be constructed with custom WP_Customize_Setting subclass.
+                        *
+                        * @since 4.2.0
+                        *
+                        * @param string $setting_class  WP_Customize_Setting or a subclass.
+                        * @param string $setting_id     ID for dynamic setting, usually coming from $_POST['customized'].
+                        * @param string $setting_args   WP_Customize_Setting or a subclass.
+                        */
+                       $setting_class = apply_filters( 'customize_dynamic_setting_class', $setting_class, $setting_id, $setting_args );
+
+                       $setting = new $setting_class( $this, $setting_id, $setting_args );
+                       $this->add_setting( $setting );
+                       $new_settings[] = $setting;
+               }
+               return $new_settings;
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Retrieve a customize setting.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 3.4.0
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -735,8 +820,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @return WP_Customize_Setting
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function get_setting( $id ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                if ( isset( $this->settings[ $id ] ) )
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         if ( isset( $this->settings[ $id ] ) ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                         return $this->settings[ $id ];
</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><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">@@ -1275,6 +1361,15 @@
</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">+         * Add settings from the POST data that were not added with code, e.g. dynamically-created settings for Widgets
+        *
+        * @since 4.2.0
+        */
+       public function register_dynamic_settings() {
+               $this->add_dynamic_settings( array_keys( $this->unsanitized_post_values() ) );
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Callback for validating the header_textcolor value.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * Accepts 'blank', and otherwise uses sanitize_hex_color_no_hash().
</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-02-08 16:58:14 UTC (rev 31369)
+++ trunk/src/wp-includes/class-wp-customize-setting.php        2015-02-08 23:10:05 UTC (rev 31370)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -55,14 +55,6 @@
</span><span class="cx" style="display: block; padding: 0 10px">        protected $id_data = array();
</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">-         * Cached and sanitized $_POST value for the setting.
-        *
-        * @access private
-        * @var mixed
-        */
-       private $_post_value;
-
-       /**
</del><span class="cx" style="display: block; padding: 0 10px">          * Constructor.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * Any supplied $args override class property defaults.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -163,7 +155,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function _preview_filter( $original ) {
</span><span class="cx" style="display: block; padding: 0 10px">                $undefined = new stdClass(); // symbol hack
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $post_value = $this->manager->post_value( $this, $undefined );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $post_value = $this->post_value( $undefined );
</ins><span class="cx" style="display: block; padding: 0 10px">                 if ( $undefined === $post_value ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        $value = $this->_original_value;
</span><span class="cx" style="display: block; padding: 0 10px">                } else {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -211,17 +203,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @return mixed The default value on failure, otherwise the sanitized value.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        final public function post_value( $default = null ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                // Check for a cached value
-               if ( isset( $this->_post_value ) )
-                       return $this->_post_value;
-
-               // Call the manager for the post value
-               $result = $this->manager->post_value( $this );
-
-               if ( isset( $result ) )
-                       return $this->_post_value = $result;
-               else
-                       return $default;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         return $this->manager->post_value( $this, $default );
</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></pre></div>
<a id="trunksrcwpincludesclasswpcustomizewidgetsphp"></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-widgets.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/class-wp-customize-widgets.php      2015-02-08 16:58:14 UTC (rev 31369)
+++ trunk/src/wp-includes/class-wp-customize-widgets.php        2015-02-08 23:10:05 UTC (rev 31370)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -35,37 +35,35 @@
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 3.9.0
</span><span class="cx" style="display: block; padding: 0 10px">         * @access protected
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @var
-        */
-       protected $_customized;
-
-       /**
-        * @since 3.9.0
-        * @access protected
</del><span class="cx" style="display: block; padding: 0 10px">          * @var array
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        protected $_prepreview_added_filters = array();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ protected $rendered_sidebars = array();
</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">         * @since 3.9.0
</span><span class="cx" style="display: block; padding: 0 10px">         * @access protected
</span><span class="cx" style="display: block; padding: 0 10px">         * @var array
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        protected $rendered_sidebars = array();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ protected $rendered_widgets = array();
</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">         * @since 3.9.0
</span><span class="cx" style="display: block; padding: 0 10px">         * @access protected
</span><span class="cx" style="display: block; padding: 0 10px">         * @var array
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        protected $rendered_widgets = array();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ protected $old_sidebars_widgets = array();
</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">-         * @since 3.9.0
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * Mapping of setting type to setting ID pattern.
+        *
+        * @since 4.2.0
</ins><span class="cx" style="display: block; padding: 0 10px">          * @access protected
</span><span class="cx" style="display: block; padding: 0 10px">         * @var array
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        protected $old_sidebars_widgets = array();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ protected $setting_id_patterns = array(
+               'widget_instance' => '/^(widget_.+?)(?:\[(\d+)\])?$/',
+               'sidebar_widgets' => '/^sidebars_widgets\[(.+?)\]$/',
+       );
</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">         * Initial loader.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -78,7 +76,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">        public function __construct( $manager ) {
</span><span class="cx" style="display: block; padding: 0 10px">                $this->manager = $manager;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                add_action( 'after_setup_theme',                       array( $this, 'setup_widget_addition_previews' ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         add_filter( 'customize_dynamic_setting_args',          array( $this, 'filter_customize_dynamic_setting_args' ), 10, 2 );
+               add_action( 'after_setup_theme',                       array( $this, 'register_settings' ) );
</ins><span class="cx" style="display: block; padding: 0 10px">                 add_action( 'wp_loaded',                               array( $this, 'override_sidebars_widgets_for_theme_switch' ) );
</span><span class="cx" style="display: block; padding: 0 10px">                add_action( 'customize_controls_init',                 array( $this, 'customize_controls_init' ) );
</span><span class="cx" style="display: block; padding: 0 10px">                add_action( 'customize_register',                      array( $this, 'schedule_customize_register' ), 1 );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -95,196 +94,95 @@
</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">-         * Get an unslashed post value or return a default.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * Get the widget setting type given a setting ID.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @since 3.9.0
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @since 4.2.0
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @access protected
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @param $setting_id
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @param string $name    Post value.
-        * @param mixed  $default Default post value.
-        * @return mixed Unslashed post value or default value.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @return string|null
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        protected function get_post_value( $name, $default = null ) {
-               if ( ! isset( $_POST[ $name ] ) ) {
-                       return $default;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ protected function get_setting_type( $setting_id ) {
+               static $cache = array();
+               if ( isset( $cache[ $setting_id ] ) ) {
+                       return $cache[ $setting_id ];
</ins><span class="cx" style="display: block; padding: 0 10px">                 }
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-
-               return wp_unslash( $_POST[$name] );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         foreach ( $this->setting_id_patterns as $type => $pattern ) {
+                       if ( preg_match( $pattern, $setting_id ) ) {
+                               $cache[ $setting_id ] = $type;
+                               return $type;
+                       }
+               }
+               return null;
</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><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * Set up widget addition previews.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * Inspect the incoming customized data for any widget settings, and dynamically add them up-front so widgets will be initialized properly.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * Since the widgets get registered on 'widgets_init' before the Customizer
-        * settings are set up on 'customize_register', we have to filter the options
-        * similarly to how the setting previewer will filter the options later.
-        *
-        * @since 3.9.0
-        *
-        * @access public
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @since 4.2.0
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        public function setup_widget_addition_previews() {
-               $is_customize_preview = false;
-
-               if ( ! empty( $this->manager ) && ! is_admin() && 'on' === $this->get_post_value( 'wp_customize' ) ) {
-                       $is_customize_preview = check_ajax_referer( 'preview-customize_' . $this->manager->get_stylesheet(), 'nonce', false );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ public function register_settings() {
+               $widget_setting_ids = array();
+               $incoming_setting_ids = array_keys( $this->manager->unsanitized_post_values() );
+               foreach ( $incoming_setting_ids as $setting_id ) {
+                       if ( ! is_null( $this->get_setting_type( $setting_id ) ) ) {
+                               $widget_setting_ids[] = $setting_id;
+                       }
</ins><span class="cx" style="display: block; padding: 0 10px">                 }
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-
-               $is_ajax_widget_update = false;
-               if ( $this->manager->doing_ajax() && 'update-widget' === $this->get_post_value( 'action' ) ) {
-                       $is_ajax_widget_update = check_ajax_referer( 'update-widget', 'nonce', false );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         if ( $this->manager->doing_ajax( 'update-widget' ) && isset( $_REQUEST['widget-id'] ) ) {
+                       $widget_setting_ids[] = $this->get_setting_id( wp_unslash( $_REQUEST['widget-id'] ) );
</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">-                $is_ajax_customize_save = false;
-               if ( $this->manager->doing_ajax() && 'customize_save' === $this->get_post_value( 'action' ) ) {
-                       $is_ajax_customize_save = check_ajax_referer( 'save-customize_' . $this->manager->get_stylesheet(), 'nonce', false );
-               }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $settings = $this->manager->add_dynamic_settings( array_unique( $widget_setting_ids ) );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $is_valid_request = ( $is_ajax_widget_update || $is_customize_preview || $is_ajax_customize_save );
-               if ( ! $is_valid_request ) {
-                       return;
-               }
-
-               // Input from Customizer preview.
-               if ( isset( $_POST['customized'] ) ) {
-                       $this->_customized = json_decode( $this->get_post_value( 'customized' ), true );
-               } else { // Input from ajax widget update request.
-                       $this->_customized = array();
-                       $id_base = $this->get_post_value( 'id_base' );
-                       $widget_number = $this->get_post_value( 'widget_number', false );
-                       $option_name = 'widget_' . $id_base;
-                       $this->_customized[ $option_name ] = array();
-                       if ( preg_match( '/^[0-9]+$/', $widget_number ) ) {
-                               $option_name .= '[' . $widget_number . ']';
-                               $this->_customized[ $option_name ][ $widget_number ] = array();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         /*
+                * Preview settings right away so that widgets and sidebars will get registered properly.
+                * But don't do this if a customize_save because this will cause WP to think there is nothing
+                * changed that needs to be saved.
+                */
+               if ( ! $this->manager->doing_ajax( 'customize_save' ) ) {
+                       foreach ( $settings as $setting ) {
+                               $setting->preview();
</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">-
-               $function = array( $this, 'prepreview_added_sidebars_widgets' );
-
-               $hook = 'option_sidebars_widgets';
-               add_filter( $hook, $function );
-               $this->_prepreview_added_filters[] = compact( 'hook', 'function' );
-
-               $hook = 'default_option_sidebars_widgets';
-               add_filter( $hook, $function );
-               $this->_prepreview_added_filters[] = compact( 'hook', 'function' );
-
-               $function = array( $this, 'prepreview_added_widget_instance' );
-               foreach ( $this->_customized as $setting_id => $value ) {
-                       if ( preg_match( '/^(widget_.+?)(?:\[(\d+)\])?$/', $setting_id, $matches ) ) {
-                               $option = $matches[1];
-
-                               $hook = sprintf( 'option_%s', $option );
-                               if ( ! has_filter( $hook, $function ) ) {
-                                       add_filter( $hook, $function );
-                                       $this->_prepreview_added_filters[] = compact( 'hook', 'function' );
-                               }
-
-                               $hook = sprintf( 'default_option_%s', $option );
-                               if ( ! has_filter( $hook, $function ) ) {
-                                       add_filter( $hook, $function );
-                                       $this->_prepreview_added_filters[] = compact( 'hook', 'function' );
-                               }
-
-                               /*
-                                * Make sure the option is registered so that the update_option()
-                                * won't fail due to the filters providing a default value, which
-                                * causes the update_option() to get confused.
-                                */
-                               add_option( $option, array() );
-                       }
-               }
</del><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">-         * Ensure that newly-added widgets will appear in the widgets_sidebars.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * Determine the arguments for a dynamically-created setting.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * This is necessary because the Customizer's setting preview filters
-        * are added after the widgets_init action, which is too late for the
-        * widgets to be set up properly.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @since 4.2.0
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @since 3.9.0
-        * @access public
-        *
-        * @param array $sidebars_widgets Associative array of sidebars and their widgets.
-        * @return array Filtered array of sidebars and their widgets.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @param false|array $args
+        * @param string $setting_id
+        * @return false|array
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        public function prepreview_added_sidebars_widgets( $sidebars_widgets ) {
-               foreach ( $this->_customized as $setting_id => $value ) {
-                       if ( preg_match( '/^sidebars_widgets\[(.+?)\]$/', $setting_id, $matches ) ) {
-                               $sidebar_id = $matches[1];
-                               $sidebars_widgets[ $sidebar_id ] = $value;
-                       }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ public function filter_customize_dynamic_setting_args( $args, $setting_id ) {
+               if ( $this->get_setting_type( $setting_id ) ) {
+                       $args = $this->get_setting_args( $setting_id );
</ins><span class="cx" style="display: block; padding: 0 10px">                 }
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                return $sidebars_widgets;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         return $args;
</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><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * Ensure newly-added widgets have empty instances so they
-        * will be recognized.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * Get an unslashed post value or return a default.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * This is necessary because the Customizer's setting preview
-        * filters are added after the widgets_init action, which is
-        * too late for the widgets to be set up properly.
-        *
</del><span class="cx" style="display: block; padding: 0 10px">          * @since 3.9.0
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @access public
</del><span class="cx" style="display: block; padding: 0 10px">          *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @param array|bool|mixed $value Widget instance(s), false if open was empty.
-        * @return array|mixed Widget instance(s) with additions.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @access protected
+        *
+        * @param string $name    Post value.
+        * @param mixed  $default Default post value.
+        * @return mixed Unslashed post value or default value.
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        public function prepreview_added_widget_instance( $value = false ) {
-               if ( ! preg_match( '/^(?:default_)?option_(widget_(.+))/', current_filter(), $matches ) ) {
-                       return $value;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ protected function get_post_value( $name, $default = null ) {
+               if ( ! isset( $_POST[ $name ] ) ) {
+                       return $default;
</ins><span class="cx" style="display: block; padding: 0 10px">                 }
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $id_base = $matches[2];
</del><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                foreach ( $this->_customized as $setting_id => $setting ) {
-                       $parsed_setting_id = $this->parse_widget_setting_id( $setting_id );
-                       if ( is_wp_error( $parsed_setting_id ) || $id_base !== $parsed_setting_id['id_base'] ) {
-                               continue;
-                       }
-                       $widget_number = $parsed_setting_id['number'];
-
-                       if ( is_null( $widget_number ) ) {
-                               // Single widget.
-                               if ( false === $value ) {
-                                       $value = array();
-                               }
-                       } else {
-                               // Multi widget.
-                               if ( empty( $value ) ) {
-                                       $value = array( '_multiwidget' => 1 );
-                               }
-                               if ( ! isset( $value[ $widget_number ] ) ) {
-                                       $value[ $widget_number ] = array();
-                               }
-                       }
-               }
-
-               return $value;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         return wp_unslash( $_POST[ $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><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * Remove pre-preview filters.
-        *
-        * Removes filters added in setup_widget_addition_previews()
-        * to ensure widgets are populating the options during
-        * 'widgets_init'.
-        *
-        * @since 3.9.0
-        * @access public
-        */
-       public function remove_prepreview_filters() {
-               foreach ( $this->_prepreview_added_filters as $prepreview_added_filter ) {
-                       remove_filter( $prepreview_added_filter['hook'], $prepreview_added_filter['function'] );
-               }
-               $this->_prepreview_added_filters = array();
-       }
-
-       /**
</del><span class="cx" style="display: block; padding: 0 10px">          * Override sidebars_widgets for theme switch.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * When switching a theme via the Customizer, supply any previously-configured
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -380,7 +278,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 schedule_customize_register() {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                if ( is_admin() ) { // @todo for some reason, $wp_customize->is_preview() is true here?
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         if ( is_admin() ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                         $this->customize_register();
</span><span class="cx" style="display: block; padding: 0 10px">                } else {
</span><span class="cx" style="display: block; padding: 0 10px">                        add_action( 'wp', array( $this, 'customize_register' ) );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -412,12 +310,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">                foreach ( array_keys( $wp_registered_widgets ) as $widget_id ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        $setting_id   = $this->get_setting_id( $widget_id );
</span><span class="cx" style="display: block; padding: 0 10px">                        $setting_args = $this->get_setting_args( $setting_id );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-
-                       $setting_args['sanitize_callback']    = array( $this, 'sanitize_widget_instance' );
-                       $setting_args['sanitize_js_callback'] = array( $this, 'sanitize_widget_js_instance' );
-
-                       $this->manager->add_setting( $setting_id, $setting_args );
-
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 if ( ! $this->manager->get_setting( $setting_id ) ) {
+                               $this->manager->add_setting( $setting_id, $setting_args );
+                       }
</ins><span class="cx" style="display: block; padding: 0 10px">                         $new_setting_ids[] = $setting_id;
</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">@@ -452,11 +347,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        if ( $is_registered_sidebar || $is_inactive_widgets ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                $setting_id   = sprintf( 'sidebars_widgets[%s]', $sidebar_id );
</span><span class="cx" style="display: block; padding: 0 10px">                                $setting_args = $this->get_setting_args( $setting_id );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-
-                               $setting_args['sanitize_callback']    = array( $this, 'sanitize_sidebar_widgets' );
-                               $setting_args['sanitize_js_callback'] = array( $this, 'sanitize_sidebar_widgets_js_instance' );
-
-                               $this->manager->add_setting( $setting_id, $setting_args );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         if ( ! $this->manager->get_setting( $setting_id ) ) {
+                                       $this->manager->add_setting( $setting_id, $setting_args );
+                               }
</ins><span class="cx" style="display: block; padding: 0 10px">                                 $new_setting_ids[] = $setting_id;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                // Add section to contain controls.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -523,16 +416,13 @@
</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">-                /*
-                * We have to register these settings later than customize_preview_init
-                * so that other filters have had a chance to run.
-                */
-               if ( did_action( 'customize_preview_init' ) ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         if ( ! $this->manager->doing_ajax( 'customize_save' ) ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                         foreach ( $new_setting_ids as $new_setting_id ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                $this->manager->get_setting( $new_setting_id )->preview();
</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">-                $this->remove_prepreview_filters();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+               add_filter( 'sidebars_widgets', array( $this, 'preview_sidebars_widgets' ), 1 );
</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">@@ -804,6 +694,15 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        'transport'  => 'refresh',
</span><span class="cx" style="display: block; padding: 0 10px">                        'default'    => array(),
</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 ( preg_match( $this->setting_id_patterns['sidebar_widgets'], $id, $matches ) ) {
+                       $args['sanitize_callback'] = array( $this, 'sanitize_sidebar_widgets' );
+                       $args['sanitize_js_callback'] = array( $this, 'sanitize_sidebar_widgets_js_instance' );
+               } else if ( preg_match( $this->setting_id_patterns['widget_instance'], $id, $matches ) ) {
+                       $args['sanitize_callback'] = array( $this, 'sanitize_widget_instance' );
+                       $args['sanitize_js_callback'] = array( $this, 'sanitize_widget_js_instance' );
+               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 $args = array_merge( $args, $overrides );
</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">@@ -831,15 +730,10 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @return array Array of sanitized widget IDs.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function sanitize_sidebar_widgets( $widget_ids ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                global $wp_registered_widgets;
-
-               $widget_ids           = array_map( 'strval', (array) $widget_ids );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $widget_ids = array_map( 'strval', (array) $widget_ids );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $sanitized_widget_ids = array();
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-
</del><span class="cx" style="display: block; padding: 0 10px">                 foreach ( $widget_ids as $widget_id ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        if ( array_key_exists( $widget_id, $wp_registered_widgets ) ) {
-                               $sanitized_widget_ids[] = $widget_id;
-                       }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 $sanitized_widget_ids[] = preg_replace( '/[^a-z0-9_\-]/', '', $widget_id );
</ins><span class="cx" style="display: block; padding: 0 10px">                 }
</span><span class="cx" style="display: block; padding: 0 10px">                return $sanitized_widget_ids;
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -974,7 +868,6 @@
</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 customize_preview_init() {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                add_filter( 'sidebars_widgets',   array( $this, 'preview_sidebars_widgets' ), 1 );
</del><span class="cx" style="display: block; padding: 0 10px">                 add_action( 'wp_enqueue_scripts', array( $this, 'customize_preview_enqueue' ) );
</span><span class="cx" style="display: block; padding: 0 10px">                add_action( 'wp_print_styles',    array( $this, 'print_preview_css' ), 1 );
</span><span class="cx" style="display: block; padding: 0 10px">                add_action( 'wp_footer',          array( $this, 'export_preview_data' ), 20 );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1315,8 +1208,8 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // Clean up any input vars that were manually added
</span><span class="cx" style="display: block; padding: 0 10px">                foreach ( $added_input_vars as $key ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        unset( $_POST[$key] );
-                       unset( $_REQUEST[$key] );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 unset( $_POST[ $key ] );
+                       unset( $_REQUEST[ $key ] );
</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">                // Make sure the expected option was updated.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1333,25 +1226,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">+                // Obtain the widget instance.
+               $option = $this->get_captured_option( $option_name );
+               if ( null !== $parsed_id['number'] ) {
+                       $instance = $option[ $parsed_id['number'] ];
+               } else {
+                       $instance = $option;
+               }
+
+               /*
+                * Override the incoming $_POST['customized'] for a newly-created widget's
+                * setting with the new $instance so that the preview filter currently
+                * in place from WP_Customize_Setting::preview() will use this value
+                * instead of the default widget instance value (an empty array).
+                */
+               $setting_id = $this->get_setting_id( $widget_id );
+               $this->manager->set_post_value( $setting_id, $instance );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 // Obtain the widget control with the updated instance in place.
</span><span class="cx" style="display: block; padding: 0 10px">                ob_start();
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-
-               $form = $wp_registered_widget_controls[$widget_id];
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $form = $wp_registered_widget_controls[ $widget_id ];
</ins><span class="cx" style="display: block; padding: 0 10px">                 if ( $form ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        call_user_func_array( $form['callback'], $form['params'] );
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-
</del><span class="cx" style="display: block; padding: 0 10px">                 $form = ob_get_clean();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                // Obtain the widget instance.
-               $option = get_option( $option_name );
-
-               if ( null !== $parsed_id['number'] ) {
-                       $instance = $option[$parsed_id['number']];
-               } else {
-                       $instance = $option;
-               }
-
</del><span class="cx" style="display: block; padding: 0 10px">                 $this->stop_capturing_option_updates();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                return compact( 'instance', 'form' );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1383,8 +1282,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        wp_die( -1 );
</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">-                if ( ! isset( $_POST['widget-id'] ) ) {
-                       wp_send_json_error();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         if ( empty( $_POST['widget-id'] ) ) {
+                       wp_send_json_error( 'missing_widget-id' );
</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">                /** This action is documented in wp-admin/includes/ajax-actions.php */
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1398,15 +1297,22 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $widget_id = $this->get_post_value( 'widget-id' );
</span><span class="cx" style="display: block; padding: 0 10px">                $parsed_id = $this->parse_widget_id( $widget_id );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $id_base   = $parsed_id['id_base'];
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $id_base = $parsed_id['id_base'];
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                if ( isset( $_POST['widget-' . $id_base] ) && is_array( $_POST['widget-' . $id_base] ) && preg_match( '/__i__|%i%/', key( $_POST['widget-' . $id_base] ) ) ) {
-                       wp_send_json_error();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $is_updating_widget_template = (
+                       isset( $_POST[ 'widget-' . $id_base ] )
+                       &&
+                       is_array( $_POST[ 'widget-' . $id_base ] )
+                       &&
+                       preg_match( '/__i__|%i%/', key( $_POST[ 'widget-' . $id_base ] ) )
+               );
+               if ( $is_updating_widget_template ) {
+                       wp_send_json_error( 'template_widget_not_updatable' );
</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">                $updated_widget = $this->call_widget_update( $widget_id ); // => {instance,form}
</span><span class="cx" style="display: block; padding: 0 10px">                if ( is_wp_error( $updated_widget ) ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        wp_send_json_error();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 wp_send_json_error( $updated_widget->get_error_message() );
</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">                $form = $updated_widget['form'];
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1463,6 +1369,25 @@
</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">+         * Get the option that was captured from being saved.
+        *
+        * @since 4.2.0
+        * @access protected
+        *
+        * @param string $option_name Option name.
+        * @param mixed  $default     Optional. Default value to return if the option does not exist.
+        * @return mixed Value set for the option.
+        */
+       protected function get_captured_option( $option_name, $default = false ) {
+               if ( array_key_exists( $option_name, $this->_captured_options ) ) {
+                       $value = $this->_captured_options[ $option_name ];
+               } else {
+                       $value = $default;
+               }
+               return $value;
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Get the number of captured widget option updates.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 3.9.0
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1496,21 +1421,21 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 3.9.0
</span><span class="cx" style="display: block; padding: 0 10px">         * @access public
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @param mixed $new_value
-        * @param string $option_name
-        * @param mixed $old_value
-        * @return mixed
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @param mixed  $new_value   The new option value.
+        * @param string $option_name Name of the option.
+        * @param mixed  $old_value   The old option value.
+        * @return mixed Filtered option value.
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><span class="cx" style="display: block; padding: 0 10px">        public function capture_filter_pre_update_option( $new_value, $option_name, $old_value ) {
</span><span class="cx" style="display: block; padding: 0 10px">                if ( $this->is_option_capture_ignored( $option_name ) ) {
</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">-                if ( ! isset( $this->_captured_options[$option_name] ) ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         if ( ! isset( $this->_captured_options[ $option_name ] ) ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                         add_filter( "pre_option_{$option_name}", array( $this, 'capture_filter_pre_get_option' ) );
</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">-                $this->_captured_options[$option_name] = $new_value;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $this->_captured_options[ $option_name ] = $new_value;
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                return $old_value;
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1521,14 +1446,14 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 3.9.0
</span><span class="cx" style="display: block; padding: 0 10px">         * @access public
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @param mixed $value Option
-        * @return mixed
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @param mixed $value Value to return instead of the option value.
+        * @return mixed Filtered option value.
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><span class="cx" style="display: block; padding: 0 10px">        public function capture_filter_pre_get_option( $value ) {
</span><span class="cx" style="display: block; padding: 0 10px">                $option_name = preg_replace( '/^pre_option_/', '', current_filter() );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                if ( isset( $this->_captured_options[$option_name] ) ) {
-                       $value = $this->_captured_options[$option_name];
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         if ( isset( $this->_captured_options[ $option_name ] ) ) {
+                       $value = $this->_captured_options[ $option_name ];
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        /** This filter is documented in wp-includes/option.php */
</span><span class="cx" style="display: block; padding: 0 10px">                        $value = apply_filters( 'option_' . $option_name, $value );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1557,4 +1482,36 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->_captured_options = array();
</span><span class="cx" style="display: block; padding: 0 10px">                $this->_is_capturing_option_updates = false;
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+       /**
+        * @since 3.9.0
+        * @deprecated 4.2.0 Deprecated in favor of customize_dynamic_setting_args filter.
+        */
+       public function setup_widget_addition_previews() {
+               _deprecated_function( __METHOD__, '4.2.0' );
+       }
+
+       /**
+        * @since 3.9.0
+        * @deprecated 4.2.0 Deprecated in favor of customize_dynamic_setting_args filter.
+        */
+       public function prepreview_added_sidebars_widgets() {
+               _deprecated_function( __METHOD__, '4.2.0' );
+       }
+
+       /**
+        * @since 3.9.0
+        * @deprecated 4.2.0 Deprecated in favor of customize_dynamic_setting_args filter.
+        */
+       public function prepreview_added_widget_instance() {
+               _deprecated_function( __METHOD__, '4.2.0' );
+       }
+
+       /**
+        * @since 3.9.0
+        * @deprecated 4.2.0 Deprecated in favor of customize_dynamic_setting_args filter.
+        */
+       public function remove_prepreview_filters() {
+               _deprecated_function( __METHOD__, '4.2.0' );
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span></span></pre></div>
<a id="trunktestsphpunittestscustomizemanagerphp"></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/manager.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/customize/manager.php   2015-02-08 16:58:14 UTC (rev 31369)
+++ trunk/tests/phpunit/tests/customize/manager.php     2015-02-08 23:10:05 UTC (rev 31370)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -32,8 +32,38 @@
</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">-         * Test WP_Customize_Manager::unsanitized_post_values()
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * Test WP_Customize_Manager::doing_ajax().
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @group ajax
+        */
+       function test_doing_ajax() {
+               if ( ! defined( 'DOING_AJAX' ) ) {
+                       define( 'DOING_AJAX', true );
+               }
+
+               $manager = $this->instantiate();
+               $this->assertTrue( $manager->doing_ajax() );
+
+               $_REQUEST['action'] = 'customize_save';
+               $this->assertTrue( $manager->doing_ajax( 'customize_save' ) );
+               $this->assertFalse( $manager->doing_ajax( 'update-widget' ) );
+       }
+
+       /**
+        * Test ! WP_Customize_Manager::doing_ajax().
+        */
+       function test_not_doing_ajax() {
+               if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
+                       $this->markTestSkipped( 'Cannot test when DOING_AJAX' );
+               }
+
+               $manager = $this->instantiate();
+               $this->assertFalse( $manager->doing_ajax() );
+       }
+
+       /**
+        * Test WP_Customize_Manager::unsanitized_post_values().
+        *
</ins><span class="cx" style="display: block; padding: 0 10px">          * @ticket 30988
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        function test_unsanitized_post_values() {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -49,7 +79,7 @@
</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">-         * Test the WP_Customize_Manager::post_value() method
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * Test the WP_Customize_Manager::post_value() method.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @ticket 30988
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -71,5 +101,78 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( 'post_value_bar_default', $manager->post_value( $bar_setting, 'post_value_bar_default' ), 'Expected post_value($bar_setting, $default) to return $default since no value supplied in $_POST[customized][bar]' );
</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 the WP_Customize_Manager::add_dynamic_settings() method.
+        *
+        * @ticket 30936
+        */
+       function test_add_dynamic_settings() {
+               $manager = $this->instantiate();
+               $setting_ids = array( 'foo', 'bar' );
+               $manager->add_setting( 'foo', array( 'default' => 'foo_default' ) );
+               $this->assertEmpty( $manager->get_setting( 'bar' ), 'Expected there to not be a bar setting up front.' );
+               $manager->add_dynamic_settings( $setting_ids );
+               $this->assertEmpty( $manager->get_setting( 'bar' ), 'Expected the bar setting to remain absent since filters not added.' );
+
+               $this->action_customize_register_for_dynamic_settings();
+               $manager->add_dynamic_settings( $setting_ids );
+               $this->assertNotEmpty( $manager->get_setting( 'bar' ), 'Expected bar setting to be created since filters were added.' );
+               $this->assertEquals( 'foo_default', $manager->get_setting( 'foo' )->default, 'Expected static foo setting to not get overridden by dynamic setting.' );
+               $this->assertEquals( 'dynamic_bar_default', $manager->get_setting( 'bar' )->default, 'Expected dynamic setting bar to have default providd by filter.' );
+       }
+
+       /**
+        * Test the WP_Customize_Manager::register_dynamic_settings() method.
+        *
+        * This is similar to test_add_dynamic_settings, except the settings are passed via $_POST['customized'].
+        *
+        * @ticket 30936
+        */
+       function test_register_dynamic_settings() {
+               $posted_settings = array(
+                       'foo' => 'OOF',
+                       'bar' => 'RAB',
+               );
+               $_POST['customized'] = wp_slash( wp_json_encode( $posted_settings ) );
+
+               add_action( 'customize_register', array( $this, 'action_customize_register_for_dynamic_settings' ) );
+
+               $manager = $this->instantiate();
+               $manager->add_setting( 'foo', array( 'default' => 'foo_default' ) );
+
+               $this->assertEmpty( $manager->get_setting( 'bar' ), 'Expected dynamic setting "bar" to not be registered.' );
+               do_action( 'customize_register', $manager );
+               $this->assertNotEmpty( $manager->get_setting( 'bar' ), 'Expected dynamic setting "bar" to be automatically registered after customize_register action.' );
+               $this->assertEmpty( $manager->get_setting( 'baz' ), 'Expected unrecognized dynamic setting "baz" to remain unregistered.' );
+       }
+
+       /**
+        * In lieu of closures, callback for customize_register action added in test_register_dynamic_settings().
+        */
+       function action_customize_register_for_dynamic_settings() {
+               add_filter( 'customize_dynamic_setting_args', array( $this, 'filter_customize_dynamic_setting_args_for_test_dynamic_settings' ), 10, 2 );
+               add_filter( 'customize_dynamic_setting_class', array( $this, 'filter_customize_dynamic_setting_class_for_test_dynamic_settings' ), 10, 3 );
+       }
+
+       /**
+        * In lieu of closures, callback for customize_dynamic_setting_args filter added for test_register_dynamic_settings().
+        */
+       function filter_customize_dynamic_setting_args_for_test_dynamic_settings( $setting_args, $setting_id ) {
+               $this->assertEquals( false, $setting_args, 'Expected $setting_args to be false by default.' );
+               $this->assertInternalType( 'string', $setting_id );
+               if ( in_array( $setting_id, array( 'foo', 'bar' ) ) ) {
+                       $setting_args = array( 'default' => "dynamic_{$setting_id}_default" );
+               }
+               return $setting_args;
+       }
+
+       /**
+        * In lieu of closures, callback for customize_dynamic_setting_class filter added for test_register_dynamic_settings().
+        */
+       function filter_customize_dynamic_setting_class_for_test_dynamic_settings( $setting_class, $setting_id, $setting_args ) {
+               $this->assertEquals( 'WP_Customize_Setting', $setting_class );
+               $this->assertInternalType( 'string', $setting_id );
+               $this->assertInternalType( 'array', $setting_args );
+               return $setting_class;
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-
</del></span></pre></div>
<a id="trunktestsphpunittestscustomizewidgetsphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/customize/widgets.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/customize/widgets.php                           (rev 0)
+++ trunk/tests/phpunit/tests/customize/widgets.php     2015-02-08 23:10:05 UTC (rev 31370)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,197 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+/**
+ * Tests for the WP_Customize_Widgets class.
+ *
+ * @group customize
+ */
+class Tests_WP_Customize_Widgets extends WP_UnitTestCase {
+
+       /**
+        * @var WP_Customize_Manager
+        */
+       protected $manager;
+
+       function setUp() {
+               parent::setUp();
+               require_once( ABSPATH . WPINC . '/class-wp-customize-manager.php' );
+               $GLOBALS['wp_customize'] = new WP_Customize_Manager();
+               $this->manager = $GLOBALS['wp_customize'];
+
+               unset( $GLOBALS['_wp_sidebars_widgets'] ); // clear out cache set by wp_get_sidebars_widgets()
+               $sidebars_widgets = wp_get_sidebars_widgets();
+               $this->assertEqualSets( array( 'wp_inactive_widgets', 'sidebar-1' ), array_keys( wp_get_sidebars_widgets() ) );
+               $this->assertContains( 'search-2', $sidebars_widgets['sidebar-1'] );
+               $this->assertContains( 'categories-2', $sidebars_widgets['sidebar-1'] );
+               $this->assertArrayHasKey( 2, get_option( 'widget_search' ) );
+               $widget_categories = get_option( 'widget_categories' );
+               $this->assertArrayHasKey( 2, $widget_categories );
+               $this->assertEquals( '', $widget_categories[2]['title'] );
+
+               remove_action( 'after_setup_theme', 'twentyfifteen_setup' ); // @todo We should not be including a theme anyway
+
+               $user_id = $this->factory->user->create( array( 'role' => 'administrator' ) );
+               wp_set_current_user( $user_id );
+       }
+
+       function tearDown() {
+               parent::tearDown();
+               $this->manager = null;
+               unset( $GLOBALS['wp_customize'] );
+       }
+
+       function set_customized_post_data( $customized ) {
+               $_POST['customized'] = wp_slash( wp_json_encode( $customized ) );
+       }
+
+       function do_customize_boot_actions() {
+               $_SERVER['REQUEST_METHOD'] = 'POST';
+               do_action( 'setup_theme' );
+               $_REQUEST['nonce'] = wp_create_nonce( 'preview-customize_' . $this->manager->theme()->get_stylesheet() );
+               do_action( 'after_setup_theme' );
+               do_action( 'init' );
+               do_action( 'wp_loaded' );
+               do_action( 'wp', $GLOBALS['wp'] );
+       }
+
+       /**
+        * Test WP_Customize_Widgets::__construct()
+        */
+       function test_construct() {
+               $this->assertInstanceOf( 'WP_Customize_Widgets', $this->manager->widgets );
+               $this->assertEquals( $this->manager, $this->manager->widgets->manager );
+       }
+
+       /**
+        * Test WP_Customize_Widgets::register_settings()
+        *
+        * @ticket 30988
+        */
+       function test_register_settings() {
+
+               $raw_widget_customized = array(
+                       'widget_categories[2]' => array(
+                               'title' => 'Taxonomies Brand New Value',
+                               'count' => 0,
+                               'hierarchical' => 0,
+                               'dropdown' => 0,
+                       ),
+                       'widget_search[3]' => array(
+                               'title' => 'Not as good as Google!',
+                       ),
+               );
+               $customized = array();
+               foreach ( $raw_widget_customized as $setting_id => $instance ) {
+                       $customized[ $setting_id ] = $this->manager->widgets->sanitize_widget_js_instance( $instance );
+               }
+
+               $this->set_customized_post_data( $customized );
+               $this->do_customize_boot_actions();
+               $this->assertTrue( is_customize_preview() );
+
+               $this->assertNotEmpty( $this->manager->get_setting( 'widget_categories[2]' ), 'Expected setting for pre-existing widget category-2, being customized.' );
+               $this->assertNotEmpty( $this->manager->get_setting( 'widget_search[2]' ), 'Expected setting for pre-existing widget search-2, not being customized.' );
+               $this->assertNotEmpty( $this->manager->get_setting( 'widget_search[3]' ), 'Expected dynamic setting for non-existing widget search-3, being customized.' );
+
+               $widget_categories = get_option( 'widget_categories' );
+               $this->assertEquals( $raw_widget_customized['widget_categories[2]'], $widget_categories[2], 'Expected $wp_customize->get_setting(widget_categories[2])->preview() to have been called.' );
+       }
+
+       /**
+        * Test WP_Customize_Widgets::get_setting_args()
+        */
+       function test_get_setting_args() {
+
+               add_filter( 'widget_customizer_setting_args', array( $this, 'filter_widget_customizer_setting_args' ), 10, 2 );
+
+               $default_args = array(
+                       'type' => 'option',
+                       'capability' => 'edit_theme_options',
+                       'transport' => 'refresh',
+                       'default' => array(),
+                       'sanitize_callback' => array( $this->manager->widgets, 'sanitize_widget_instance' ),
+                       'sanitize_js_callback' => array( $this->manager->widgets, 'sanitize_widget_js_instance' ),
+               );
+
+               $args = $this->manager->widgets->get_setting_args( 'widget_foo[2]' );
+               foreach ( $default_args as $key => $default_value ) {
+                       $this->assertEquals( $default_value, $args[ $key ] );
+               }
+               $this->assertEquals( 'WIDGET_FOO[2]', $args['uppercase_id_set_by_filter'] );
+
+               $override_args = array(
+                       'type' => 'theme_mod',
+                       'capability' => 'edit_posts',
+                       'transport' => 'postMessage',
+                       'default' => array( 'title' => 'asd' ),
+                       'sanitize_callback' => '__return_empty_array',
+                       'sanitize_js_callback' => '__return_empty_array',
+               );
+               $args = $this->manager->widgets->get_setting_args( 'widget_bar[3]', $override_args );
+               foreach ( $override_args as $key => $override_value ) {
+                       $this->assertEquals( $override_value, $args[ $key ] );
+               }
+               $this->assertEquals( 'WIDGET_BAR[3]', $args['uppercase_id_set_by_filter'] );
+
+               $default_args = array(
+                       'type' => 'option',
+                       'capability' => 'edit_theme_options',
+                       'transport' => 'refresh',
+                       'default' => array(),
+                       'sanitize_callback' => array( $this->manager->widgets, 'sanitize_sidebar_widgets' ),
+                       'sanitize_js_callback' => array( $this->manager->widgets, 'sanitize_sidebar_widgets_js_instance' ),
+               );
+               $args = $this->manager->widgets->get_setting_args( 'sidebars_widgets[sidebar-1]' );
+               foreach ( $default_args as $key => $default_value ) {
+                       $this->assertEquals( $default_value, $args[ $key ] );
+               }
+               $this->assertEquals( 'SIDEBARS_WIDGETS[SIDEBAR-1]', $args['uppercase_id_set_by_filter'] );
+
+               $override_args = array(
+                       'type' => 'theme_mod',
+                       'capability' => 'edit_posts',
+                       'transport' => 'postMessage',
+                       'default' => array( 'title' => 'asd' ),
+                       'sanitize_callback' => '__return_empty_array',
+                       'sanitize_js_callback' => '__return_empty_array',
+               );
+               $args = $this->manager->widgets->get_setting_args( 'sidebars_widgets[sidebar-2]', $override_args );
+               foreach ( $override_args as $key => $override_value ) {
+                       $this->assertEquals( $override_value, $args[ $key ] );
+               }
+               $this->assertEquals( 'SIDEBARS_WIDGETS[SIDEBAR-2]', $args['uppercase_id_set_by_filter'] );
+       }
+
+       function filter_widget_customizer_setting_args( $args, $id ) {
+               $args['uppercase_id_set_by_filter'] = strtoupper( $id );
+               return $args;
+       }
+
+       /**
+        * Test WP_Customize_Widgets::sanitize_widget_js_instance() and WP_Customize_Widgets::sanitize_widget_instance()
+        */
+       function test_sanitize_widget_js_instance() {
+               $this->do_customize_boot_actions();
+
+               $new_categories_instance = array(
+                       'title' => 'Taxonomies Brand New Value',
+                       'count' => '1',
+                       'hierarchical' => '1',
+                       'dropdown' => '1',
+               );
+
+               $sanitized_for_js = $this->manager->widgets->sanitize_widget_js_instance( $new_categories_instance );
+               $this->assertArrayHasKey( 'encoded_serialized_instance', $sanitized_for_js );
+               $this->assertTrue( is_serialized( base64_decode( $sanitized_for_js['encoded_serialized_instance'] ), true ) );
+               $this->assertEquals( $new_categories_instance['title'], $sanitized_for_js['title'] );
+               $this->assertTrue( $sanitized_for_js['is_widget_customizer_js_value'] );
+               $this->assertArrayHasKey( 'instance_hash_key', $sanitized_for_js );
+
+               $corrupted_sanitized_for_js = $sanitized_for_js;
+               $corrupted_sanitized_for_js['encoded_serialized_instance'] = base64_encode( serialize( array( 'title' => 'EVIL' ) ) );
+               $this->assertNull( $this->manager->widgets->sanitize_widget_instance( $corrupted_sanitized_for_js ), 'Expected sanitize_widget_instance to reject corrupted data.' );
+
+               $unsanitized_from_js = $this->manager->widgets->sanitize_widget_instance( $sanitized_for_js );
+               $this->assertEquals( $unsanitized_from_js, $new_categories_instance );
+       }
+}
</ins></span></pre>
</div>
</div>

</body>
</html>