WordPress 4.5
[autoinstalls/wordpress.git] / wp-includes / class-wp-customize-nav-menus.php
index f6be2094061935a4fcf4869bb08c93082a7dff54..778456727c731c2b63b1332c82e850622ca51043 100644 (file)
@@ -48,6 +48,12 @@ final class WP_Customize_Nav_Menus {
                $this->previewed_menus = array();
                $this->manager         = $manager;
 
+               // Skip useless hooks when the user can't manage nav menus anyway.
+               if ( ! current_user_can( 'edit_theme_options' ) ) {
+                       return;
+               }
+
+               add_filter( 'customize_refresh_nonces', array( $this, 'filter_nonces' ) );
                add_action( 'wp_ajax_load-available-menu-items-customizer', array( $this, 'ajax_load_available_items' ) );
                add_action( 'wp_ajax_search-available-menu-items-customizer', array( $this, 'ajax_search_available_items' ) );
                add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
@@ -60,6 +66,23 @@ final class WP_Customize_Nav_Menus {
                add_action( 'customize_controls_print_footer_scripts', array( $this, 'print_templates' ) );
                add_action( 'customize_controls_print_footer_scripts', array( $this, 'available_items_template' ) );
                add_action( 'customize_preview_init', array( $this, 'customize_preview_init' ) );
+
+               // Selective Refresh partials.
+               add_filter( 'customize_dynamic_partial_args', array( $this, 'customize_dynamic_partial_args' ), 10, 2 );
+       }
+
+       /**
+        * Adds a nonce for customizing menus.
+        *
+        * @since 4.5.0
+        * @access public
+        *
+        * @param array $nonces Array of nonces.
+        * @return array $nonces Modified array of nonces.
+        */
+       public function filter_nonces( $nonces ) {
+               $nonces['customize-menus'] = wp_create_nonce( 'customize-menus' );
+               return $nonces;
        }
 
        /**
@@ -311,6 +334,16 @@ final class WP_Customize_Nav_Menus {
                        }
                }
 
+               /**
+                * Filter the available menu items during a search request.
+                *
+                * @since 4.5.0
+                *
+                * @param array $items The array of menu items.
+                * @param array $args  Includes 'pagenum' and 's' (search) arguments.
+                */
+               $items = apply_filters( 'customize_nav_menu_searched_items', $items, $args );
+
                return $items;
        }
 
@@ -329,14 +362,13 @@ final class WP_Customize_Nav_Menus {
 
                // Pass data to JS.
                $settings = array(
-                       'nonce'                => wp_create_nonce( 'customize-menus' ),
                        'allMenus'             => wp_get_nav_menus(),
                        'itemTypes'            => $this->available_item_types(),
                        'l10n'                 => array(
                                'untitled'          => _x( '(no label)', 'missing menu item navigation label' ),
                                'unnamed'           => _x( '(unnamed)', 'Missing menu name.' ),
                                'custom_label'      => __( 'Custom Link' ),
-                               /* translators: %s: menu location slug */
+                               /* translators: %s: menu location */
                                'menuLocation'      => _x( '(Currently set to: %s)', 'menu' ),
                                'menuNameLabel'     => __( 'Menu Name' ),
                                'itemAdded'         => __( 'Menu item added' ),
@@ -361,12 +393,13 @@ final class WP_Customize_Nav_Menus {
                                'reorderLabelOn'    => esc_attr__( 'Reorder menu items' ),
                                'reorderLabelOff'   => esc_attr__( 'Close reorder mode' ),
                        ),
-                       'menuItemTransport'    => 'postMessage',
+                       'settingTransport'     => 'postMessage',
                        'phpIntMax'            => PHP_INT_MAX,
                        'defaultSettingValues' => array(
                                'nav_menu'      => $temp_nav_menu_setting->default,
                                'nav_menu_item' => $temp_nav_menu_item_setting->default,
                        ),
+                       'locationSlugMappedToName' => get_registered_nav_menus(),
                );
 
                $data = sprintf( 'var _wpCustomizeNavMenusSettings = %s;', wp_json_encode( $settings ) );
@@ -411,11 +444,13 @@ final class WP_Customize_Nav_Menus {
        public function filter_dynamic_setting_args( $setting_args, $setting_id ) {
                if ( preg_match( WP_Customize_Nav_Menu_Setting::ID_PATTERN, $setting_id ) ) {
                        $setting_args = array(
-                               'type' => WP_Customize_Nav_Menu_Setting::TYPE,
+                               'type'      => WP_Customize_Nav_Menu_Setting::TYPE,
+                               'transport' => 'postMessage',
                        );
                } elseif ( preg_match( WP_Customize_Nav_Menu_Item_Setting::ID_PATTERN, $setting_id ) ) {
                        $setting_args = array(
-                               'type' => WP_Customize_Nav_Menu_Item_Setting::TYPE,
+                               'type'      => WP_Customize_Nav_Menu_Item_Setting::TYPE,
+                               'transport' => 'postMessage',
                        );
                }
                return $setting_args;
@@ -534,7 +569,9 @@ final class WP_Customize_Nav_Menus {
                        ) ) );
 
                        $nav_menu_setting_id = 'nav_menu[' . $menu_id . ']';
-                       $this->manager->add_setting( new WP_Customize_Nav_Menu_Setting( $this->manager, $nav_menu_setting_id ) );
+                       $this->manager->add_setting( new WP_Customize_Nav_Menu_Setting( $this->manager, $nav_menu_setting_id, array(
+                               'transport' => 'postMessage',
+                       ) ) );
 
                        // Add the menu contents.
                        $menu_items = (array) wp_get_nav_menu_items( $menu_id );
@@ -547,7 +584,8 @@ final class WP_Customize_Nav_Menus {
                                $value = (array) $item;
                                $value['nav_menu_term_id'] = $menu_id;
                                $this->manager->add_setting( new WP_Customize_Nav_Menu_Item_Setting( $this->manager, $menu_item_setting_id, array(
-                                       'value' => $value,
+                                       'value'     => $value,
+                                       'transport' => 'postMessage',
                                ) ) );
 
                                // Create a control for each menu item.
@@ -568,28 +606,20 @@ final class WP_Customize_Nav_Menus {
                        'priority' => 999,
                ) ) );
 
-               $this->manager->add_setting( 'new_menu_name', array(
-                       'type'      => 'new_menu',
-                       'default'   => '',
-                       'transport' => 'postMessage',
-               ) );
-
                $this->manager->add_control( 'new_menu_name', array(
                        'label'       => '',
                        'section'     => 'add_menu',
                        'type'        => 'text',
+                       'settings'    => array(),
                        'input_attrs' => array(
                                'class'       => 'menu-name-field',
                                'placeholder' => __( 'New menu name' ),
                        ),
                ) );
 
-               $this->manager->add_setting( 'create_new_menu', array(
-                       'type' => 'new_menu',
-               ) );
-
                $this->manager->add_control( new WP_Customize_New_Menu_Control( $this->manager, 'create_new_menu', array(
-                       'section' => 'add_menu',
+                       'section'  => 'add_menu',
+                       'settings' => array(),
                ) ) );
        }
 
@@ -743,19 +773,15 @@ final class WP_Customize_Nav_Menus {
                                                <span class="toggle-indicator" aria-hidden="true"></span>
                                        </button>
                                </h4>
-                               <div class="accordion-section-content">
+                               <div class="accordion-section-content customlinkdiv">
                                        <input type="hidden" value="custom" id="custom-menu-item-type" name="menu-item[-1][menu-item-type]" />
-                                       <p id="menu-item-url-wrap">
-                                               <label class="howto" for="custom-menu-item-url">
-                                                       <span><?php _e( 'URL' ); ?></span>
-                                                       <input id="custom-menu-item-url" name="menu-item[-1][menu-item-url]" type="text" class="code menu-item-textbox" value="http://">
-                                               </label>
+                                       <p id="menu-item-url-wrap" class="wp-clearfix">
+                                               <label class="howto" for="custom-menu-item-url"><?php _e( 'URL' ); ?></label>
+                                               <input id="custom-menu-item-url" name="menu-item[-1][menu-item-url]" type="text" class="code menu-item-textbox" value="http://">
                                        </p>
-                                       <p id="menu-item-name-wrap">
-                                               <label class="howto" for="custom-menu-item-name">
-                                                       <span><?php _e( 'Link Text' ); ?></span>
-                                                       <input id="custom-menu-item-name" name="menu-item[-1][menu-item-title]" type="text" class="regular-text menu-item-textbox">
-                                               </label>
+                                       <p id="menu-item-name-wrap" class="wp-clearfix">
+                                               <label class="howto" for="custom-menu-item-name"><?php _e( 'Link Text' ); ?></label>
+                                               <input id="custom-menu-item-name" name="menu-item[-1][menu-item-title]" type="text" class="regular-text menu-item-textbox">
                                        </p>
                                        <p class="button-controls">
                                                <span class="add-to-menu">
@@ -791,28 +817,49 @@ final class WP_Customize_Nav_Menus {
        <?php
        }
 
+       //
        // Start functionality specific to partial-refresh of menu changes in Customizer preview.
-       const RENDER_AJAX_ACTION = 'customize_render_menu_partial';
-       const RENDER_NONCE_POST_KEY = 'render-menu-nonce';
-       const RENDER_QUERY_VAR = 'wp_customize_menu_render';
+       //
 
        /**
-        * The number of wp_nav_menu() calls which have happened in the preview.
+        * Nav menu args used for each instance, keyed by the args HMAC.
         *
         * @since 4.3.0
         * @access public
-        * @var int
+        * @var array
         */
-       public $preview_nav_menu_instance_number = 0;
+       public $preview_nav_menu_instance_args = array();
 
        /**
-        * Nav menu args used for each instance.
+        * Filter arguments for dynamic nav_menu selective refresh partials.
         *
-        * @since 4.3.0
+        * @since 4.5.0
         * @access public
-        * @var array
+        *
+        * @param array|false $partial_args Partial args.
+        * @param string      $partial_id   Partial ID.
+        * @return array Partial args.
         */
-       public $preview_nav_menu_instance_args = array();
+       public function customize_dynamic_partial_args( $partial_args, $partial_id ) {
+
+               if ( preg_match( '/^nav_menu_instance\[[0-9a-f]{32}\]$/', $partial_id ) ) {
+                       if ( false === $partial_args ) {
+                               $partial_args = array();
+                       }
+                       $partial_args = array_merge(
+                               $partial_args,
+                               array(
+                                       'type'                => 'nav_menu_instance',
+                                       'render_callback'     => array( $this, 'render_nav_menu_partial' ),
+                                       'container_inclusive' => true,
+                                       'settings'            => array(), // Empty because the nav menu instance may relate to a menu or a location.
+                                       'capability'          => 'edit_theme_options',
+                               )
+                       );
+               }
+
+               return $partial_args;
+       }
 
        /**
         * Add hooks for the Customizer preview.
@@ -821,13 +868,11 @@ final class WP_Customize_Nav_Menus {
         * @access public
         */
        public function customize_preview_init() {
-               add_action( 'template_redirect', array( $this, 'render_menu' ) );
                add_action( 'wp_enqueue_scripts', array( $this, 'customize_preview_enqueue_deps' ) );
-
-               if ( ! isset( $_REQUEST[ self::RENDER_QUERY_VAR ] ) ) {
-                       add_filter( 'wp_nav_menu_args', array( $this, 'filter_wp_nav_menu_args' ), 1000 );
-                       add_filter( 'wp_nav_menu', array( $this, 'filter_wp_nav_menu' ), 10, 2 );
-               }
+               add_filter( 'wp_nav_menu_args', array( $this, 'filter_wp_nav_menu_args' ), 1000 );
+               add_filter( 'wp_nav_menu', array( $this, 'filter_wp_nav_menu' ), 10, 2 );
+               add_filter( 'wp_footer', array( $this, 'export_preview_data' ), 1 );
+               add_filter( 'customize_render_partials_response', array( $this, 'export_partial_rendered_nav_menu_instances' ) );
        }
 
        /**
@@ -835,52 +880,71 @@ final class WP_Customize_Nav_Menus {
         *
         * @since 4.3.0
         * @access public
-        *
         * @see wp_nav_menu()
+        * @see WP_Customize_Widgets_Partial_Refresh::filter_dynamic_sidebar_params()
         *
         * @param array $args An array containing wp_nav_menu() arguments.
         * @return array Arguments.
         */
        public function filter_wp_nav_menu_args( $args ) {
-               $this->preview_nav_menu_instance_number += 1;
-               $args['instance_number'] = $this->preview_nav_menu_instance_number;
-
+               /*
+                * The following conditions determine whether or not this instance of
+                * wp_nav_menu() can use selective refreshed. A wp_nav_menu() can be
+                * selective refreshed if...
+                */
                $can_partial_refresh = (
+                       // ...if wp_nav_menu() is directly echoing out the menu (and thus isn't manipulating the string after generated),
                        ! empty( $args['echo'] )
                        &&
+                       // ...and if the fallback_cb can be serialized to JSON, since it will be included in the placement context data,
                        ( empty( $args['fallback_cb'] ) || is_string( $args['fallback_cb'] ) )
                        &&
+                       // ...and if the walker can also be serialized to JSON, since it will be included in the placement context data as well,
                        ( empty( $args['walker'] ) || is_string( $args['walker'] ) )
-                       &&
-                       (
+                       // ...and if it has a theme location assigned or an assigned menu to display,
+                       && (
                                ! empty( $args['theme_location'] )
                                ||
                                ( ! empty( $args['menu'] ) && ( is_numeric( $args['menu'] ) || is_object( $args['menu'] ) ) )
                        )
+                       &&
+                       // ...and if the nav menu would be rendered with a wrapper container element (upon which to attach data-* attributes).
+                       (
+                               ! empty( $args['container'] )
+                               ||
+                               ( isset( $args['items_wrap'] ) && '<' === substr( $args['items_wrap'], 0, 1 ) )
+                       )
                );
                $args['can_partial_refresh'] = $can_partial_refresh;
 
-               $hashed_args = $args;
+               $exported_args = $args;
 
+               // Empty out args which may not be JSON-serializable.
                if ( ! $can_partial_refresh ) {
-                       $hashed_args['fallback_cb'] = '';
-                       $hashed_args['walker'] = '';
+                       $exported_args['fallback_cb'] = '';
+                       $exported_args['walker'] = '';
                }
 
-               // Replace object menu arg with a term_id menu arg, as this exports better to JS and is easier to compare hashes.
-               if ( ! empty( $hashed_args['menu'] ) && is_object( $hashed_args['menu'] ) ) {
-                       $hashed_args['menu'] = $hashed_args['menu']->term_id;
+               /*
+                * Replace object menu arg with a term_id menu arg, as this exports better
+                * to JS and is easier to compare hashes.
+                */
+               if ( ! empty( $exported_args['menu'] ) && is_object( $exported_args['menu'] ) ) {
+                       $exported_args['menu'] = $exported_args['menu']->term_id;
                }
 
-               ksort( $hashed_args );
-               $hashed_args['args_hash'] = $this->hash_nav_menu_args( $hashed_args );
+               ksort( $exported_args );
+               $exported_args['args_hmac'] = $this->hash_nav_menu_args( $exported_args );
 
-               $this->preview_nav_menu_instance_args[ $this->preview_nav_menu_instance_number ] = $hashed_args;
+               $args['customize_preview_nav_menus_args'] = $exported_args;
+               $this->preview_nav_menu_instance_args[ $exported_args['args_hmac'] ] = $exported_args;
                return $args;
        }
 
        /**
-        * Prepare wp_nav_menu() calls for partial refresh. Wraps output in container for refreshing.
+        * Prepares wp_nav_menu() calls for partial refresh.
+        *
+        * Injects attributes into container element.
         *
         * @since 4.3.0
         * @access public
@@ -892,29 +956,29 @@ final class WP_Customize_Nav_Menus {
         * @return null
         */
        public function filter_wp_nav_menu( $nav_menu_content, $args ) {
-               if ( ! empty( $args->can_partial_refresh ) && ! empty( $args->instance_number ) ) {
-                       $nav_menu_content = preg_replace(
-                               '/(?<=class=")/',
-                               sprintf( 'partial-refreshable-nav-menu partial-refreshable-nav-menu-%1$d ', $args->instance_number ),
-                               $nav_menu_content,
-                               1 // Only update the class on the first element found, the menu container.
-                       );
+               if ( isset( $args->customize_preview_nav_menus_args['can_partial_refresh'] ) && $args->customize_preview_nav_menus_args['can_partial_refresh'] ) {
+                       $attributes = sprintf( ' data-customize-partial-id="%s"', esc_attr( 'nav_menu_instance[' . $args->customize_preview_nav_menus_args['args_hmac'] . ']' ) );
+                       $attributes .= ' data-customize-partial-type="nav_menu_instance"';
+                       $attributes .= sprintf( ' data-customize-partial-placement-context="%s"', esc_attr( wp_json_encode( $args->customize_preview_nav_menus_args ) ) );
+                       $nav_menu_content = preg_replace( '#^(<\w+)#', '$1 ' . $attributes, $nav_menu_content, 1 );
                }
                return $nav_menu_content;
        }
 
        /**
-        * Hash (hmac) the arguments with the nonce and secret auth key to ensure they
-        * are not tampered with when submitted in the Ajax request.
+        * Hashes (hmac) the nav menu arguments to ensure they are not tampered with when
+        * submitted in the Ajax request.
+        *
+        * Note that the array is expected to be pre-sorted.
         *
         * @since 4.3.0
         * @access public
         *
         * @param array $args The arguments to hash.
-        * @return string
+        * @return string Hashed nav menu arguments.
         */
        public function hash_nav_menu_args( $args ) {
-               return wp_hash( wp_create_nonce( self::RENDER_AJAX_ACTION ) . serialize( $args ) );
+               return wp_hash( serialize( $args ) );
        }
 
        /**
@@ -924,14 +988,12 @@ final class WP_Customize_Nav_Menus {
         * @access public
         */
        public function customize_preview_enqueue_deps() {
-               wp_enqueue_script( 'customize-preview-nav-menus' );
+               wp_enqueue_script( 'customize-preview-nav-menus' ); // Note that we have overridden this.
                wp_enqueue_style( 'customize-preview' );
-
-               add_action( 'wp_print_footer_scripts', array( $this, 'export_preview_data' ) );
        }
 
        /**
-        * Export data from PHP to JS.
+        * Exports data from PHP to JS.
         *
         * @since 4.3.0
         * @access public
@@ -940,21 +1002,25 @@ final class WP_Customize_Nav_Menus {
 
                // Why not wp_localize_script? Because we're not localizing, and it forces values into strings.
                $exports = array(
-                       'renderQueryVar'        => self::RENDER_QUERY_VAR,
-                       'renderNonceValue'      => wp_create_nonce( self::RENDER_AJAX_ACTION ),
-                       'renderNoncePostKey'    => self::RENDER_NONCE_POST_KEY,
-                       'requestUri'            => empty( $_SERVER['REQUEST_URI'] ) ? home_url( '/' ) : esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ),
-                       'theme'                 => array(
-                               'stylesheet' => $this->manager->get_stylesheet(),
-                               'active'     => $this->manager->is_theme_active(),
-                       ),
-                       'previewCustomizeNonce' => wp_create_nonce( 'preview-customize_' . $this->manager->get_stylesheet() ),
-                       'navMenuInstanceArgs'   => $this->preview_nav_menu_instance_args,
+                       'navMenuInstanceArgs' => $this->preview_nav_menu_instance_args,
                );
-
                printf( '<script>var _wpCustomizePreviewNavMenusExports = %s;</script>', wp_json_encode( $exports ) );
        }
 
+       /**
+        * Export any wp_nav_menu() calls during the rendering of any partials.
+        *
+        * @since 4.5.0
+        * @access public
+        *
+        * @param array $response Response.
+        * @return array Response.
+        */
+       public function export_partial_rendered_nav_menu_instances( $response ) {
+               $response['nav_menu_instance_args'] = $this->preview_nav_menu_instance_args;
+               return $response;
+       }
+
        /**
         * Render a specific menu via wp_nav_menu() using the supplied arguments.
         *
@@ -962,49 +1028,32 @@ final class WP_Customize_Nav_Menus {
         * @access public
         *
         * @see wp_nav_menu()
+        *
+        * @param WP_Customize_Partial $partial       Partial.
+        * @param array                $nav_menu_args Nav menu args supplied as container context.
+        * @return string|false
         */
-       public function render_menu() {
-               if ( empty( $_POST[ self::RENDER_QUERY_VAR ] ) ) {
-                       return;
-               }
-
-               $this->manager->remove_preview_signature();
-
-               if ( empty( $_POST[ self::RENDER_NONCE_POST_KEY ] ) ) {
-                       wp_send_json_error( 'missing_nonce_param' );
-               }
+       public function render_nav_menu_partial( $partial, $nav_menu_args ) {
+               unset( $partial );
 
-               if ( ! is_customize_preview() ) {
-                       wp_send_json_error( 'expected_customize_preview' );
+               if ( ! isset( $nav_menu_args['args_hmac'] ) ) {
+                       // Error: missing_args_hmac.
+                       return false;
                }
 
-               if ( ! check_ajax_referer( self::RENDER_AJAX_ACTION, self::RENDER_NONCE_POST_KEY, false ) ) {
-                       wp_send_json_error( 'nonce_check_fail' );
-               }
+               $nav_menu_args_hmac = $nav_menu_args['args_hmac'];
+               unset( $nav_menu_args['args_hmac'] );
 
-               if ( ! current_user_can( 'edit_theme_options' ) ) {
-                       wp_send_json_error( 'unauthorized' );
+               ksort( $nav_menu_args );
+               if ( ! hash_equals( $this->hash_nav_menu_args( $nav_menu_args ), $nav_menu_args_hmac ) ) {
+                       // Error: args_hmac_mismatch.
+                       return false;
                }
 
-               if ( ! isset( $_POST['wp_nav_menu_args'] ) ) {
-                       wp_send_json_error( 'missing_param' );
-               }
-
-               if ( ! isset( $_POST['wp_nav_menu_args_hash'] ) ) {
-                       wp_send_json_error( 'missing_param' );
-               }
-
-               $wp_nav_menu_args = json_decode( wp_unslash( $_POST['wp_nav_menu_args'] ), true );
-               if ( ! is_array( $wp_nav_menu_args ) ) {
-                       wp_send_json_error( 'wp_nav_menu_args_not_array' );
-               }
-
-               $wp_nav_menu_args_hash = sanitize_text_field( wp_unslash( $_POST['wp_nav_menu_args_hash'] ) );
-               if ( ! hash_equals( $this->hash_nav_menu_args( $wp_nav_menu_args ), $wp_nav_menu_args_hash ) ) {
-                       wp_send_json_error( 'wp_nav_menu_args_hash_mismatch' );
-               }
+               ob_start();
+               wp_nav_menu( $nav_menu_args );
+               $content = ob_get_clean();
 
-               $wp_nav_menu_args['echo'] = false;
-               wp_send_json_success( wp_nav_menu( $wp_nav_menu_args ) );
+               return $content;
        }
 }