- +
+ +
-- +
+ +
@@ -791,28 +817,49 @@ final class WP_Customize_Nav_Menus { '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( '', 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; } }