Make WordPress Core


Ignore:
Timestamp:
02/19/2016 06:40:06 PM (9 years ago)
Author:
westonruter
Message:

Customize: Add selective refresh framework with implementation for widgets and re-implementation for nav menus.

See https://make.wordpress.org/core/2016/02/16/selective-refresh-in-the-customizer/.

Props westonruter, valendesigns, DrewAPicture, ocean90.
Fixes #27355.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp-customize-nav-menus.php

    r36573 r36586  
    6262        add_action( 'customize_controls_print_footer_scripts', array( $this, 'available_items_template' ) );
    6363        add_action( 'customize_preview_init', array( $this, 'customize_preview_init' ) );
     64
     65        // Selective Refresh partials.
     66        add_filter( 'customize_dynamic_partial_args', array( $this, 'customize_dynamic_partial_args' ), 10, 2 );
    6467    }
    6568
     
    376379                'reorderLabelOff'   => esc_attr__( 'Close reorder mode' ),
    377380            ),
    378             'menuItemTransport'    => 'postMessage',
     381            'settingTransport'     => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh',
    379382            'phpIntMax'            => PHP_INT_MAX,
    380383            'defaultSettingValues' => array(
     
    427430        if ( preg_match( WP_Customize_Nav_Menu_Setting::ID_PATTERN, $setting_id ) ) {
    428431            $setting_args = array(
    429                 'type' => WP_Customize_Nav_Menu_Setting::TYPE,
     432                'type'      => WP_Customize_Nav_Menu_Setting::TYPE,
     433                'transport' => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh',
    430434            );
    431435        } elseif ( preg_match( WP_Customize_Nav_Menu_Item_Setting::ID_PATTERN, $setting_id ) ) {
    432436            $setting_args = array(
    433                 'type' => WP_Customize_Nav_Menu_Item_Setting::TYPE,
     437                'type'      => WP_Customize_Nav_Menu_Item_Setting::TYPE,
     438                'transport' => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh',
    434439            );
    435440        }
     
    516521            $setting = $this->manager->get_setting( $setting_id );
    517522            if ( $setting ) {
    518                 $setting->transport = 'postMessage';
     523                $setting->transport = isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh';
    519524                remove_filter( "customize_sanitize_{$setting_id}", 'absint' );
    520525                add_filter( "customize_sanitize_{$setting_id}", array( $this, 'intval_base10' ) );
     
    524529                    'theme_supports'    => 'menus',
    525530                    'type'              => 'theme_mod',
    526                     'transport'         => 'postMessage',
     531                    'transport'         => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh',
    527532                    'default'           => 0,
    528533                ) );
     
    550555
    551556            $nav_menu_setting_id = 'nav_menu[' . $menu_id . ']';
    552             $this->manager->add_setting( new WP_Customize_Nav_Menu_Setting( $this->manager, $nav_menu_setting_id ) );
     557            $this->manager->add_setting( new WP_Customize_Nav_Menu_Setting( $this->manager, $nav_menu_setting_id, array(
     558                'transport' => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh',
     559            ) ) );
    553560
    554561            // Add the menu contents.
     
    563570                $value['nav_menu_term_id'] = $menu_id;
    564571                $this->manager->add_setting( new WP_Customize_Nav_Menu_Item_Setting( $this->manager, $menu_item_setting_id, array(
    565                     'value' => $value,
     572                    'value'     => $value,
     573                    'transport' => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh',
    566574                ) ) );
    567575
     
    587595            'type'      => 'new_menu',
    588596            'default'   => '',
    589             'transport' => 'postMessage',
     597            'transport' => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh',
    590598        ) );
    591599
     
    803811    }
    804812
     813    //
    805814    // Start functionality specific to partial-refresh of menu changes in Customizer preview.
    806     const RENDER_AJAX_ACTION = 'customize_render_menu_partial';
    807     const RENDER_NONCE_POST_KEY = 'render-menu-nonce';
    808     const RENDER_QUERY_VAR = 'wp_customize_menu_render';
    809 
    810     /**
    811      * The number of wp_nav_menu() calls which have happened in the preview.
    812      *
    813      * @since 4.3.0
    814      * @access public
    815      * @var int
    816      */
    817     public $preview_nav_menu_instance_number = 0;
    818 
    819     /**
    820      * Nav menu args used for each instance.
    821      *
    822      * @since 4.3.0
    823      * @access public
    824      * @var array
    825      */
    826     public $preview_nav_menu_instance_args = array();
     815    //
     816
     817    /**
     818     * Filters arguments for dynamic nav_menu selective refresh partials.
     819     *
     820     * @since 4.5.0
     821     * @access public
     822     *
     823     * @param array|false $partial_args Partial args.
     824     * @param string      $partial_id   Partial ID.
     825     * @return array Partial args
     826     */
     827    public function customize_dynamic_partial_args( $partial_args, $partial_id ) {
     828
     829        if ( preg_match( '/^nav_menu_instance\[[0-9a-f]{32}\]$/', $partial_id ) ) {
     830            if ( false === $partial_args ) {
     831                $partial_args = array();
     832            }
     833            $partial_args = array_merge(
     834                $partial_args,
     835                array(
     836                    'type'                => 'nav_menu_instance',
     837                    'render_callback'     => array( $this, 'render_nav_menu_partial' ),
     838                    'container_inclusive' => true,
     839                )
     840            );
     841        }
     842
     843        return $partial_args;
     844    }
    827845
    828846    /**
     
    833851     */
    834852    public function customize_preview_init() {
    835         add_action( 'template_redirect', array( $this, 'render_menu' ) );
    836853        add_action( 'wp_enqueue_scripts', array( $this, 'customize_preview_enqueue_deps' ) );
    837 
    838         if ( ! isset( $_REQUEST[ self::RENDER_QUERY_VAR ] ) ) {
    839             add_filter( 'wp_nav_menu_args', array( $this, 'filter_wp_nav_menu_args' ), 1000 );
    840             add_filter( 'wp_nav_menu', array( $this, 'filter_wp_nav_menu' ), 10, 2 );
    841         }
     854        add_filter( 'wp_nav_menu_args', array( $this, 'filter_wp_nav_menu_args' ), 1000 );
     855        add_filter( 'wp_nav_menu', array( $this, 'filter_wp_nav_menu' ), 10, 2 );
    842856    }
    843857
     
    847861     * @since 4.3.0
    848862     * @access public
    849      *
    850863     * @see wp_nav_menu()
     864     * @see WP_Customize_Widgets_Partial_Refresh::filter_dynamic_sidebar_params()
    851865     *
    852866     * @param array $args An array containing wp_nav_menu() arguments.
     
    854868     */
    855869    public function filter_wp_nav_menu_args( $args ) {
    856         $this->preview_nav_menu_instance_number += 1;
    857         $args['instance_number'] = $this->preview_nav_menu_instance_number;
    858 
    859         $can_partial_refresh = (
     870        /*
     871         * The following conditions determine whether or not this instance of
     872         * wp_nav_menu() can use selective refreshed. A wp_nav_menu() can be
     873         * selective refreshed if...
     874         */
     875        $can_selective_refresh = (
     876            // ...if wp_nav_menu() is directly echoing out the menu (and thus isn't manipulating the string after generated),
    860877            ! empty( $args['echo'] )
    861878            &&
     879            // ...and if the fallback_cb can be serialized to JSON, since it will be included in the placement context data,
    862880            ( empty( $args['fallback_cb'] ) || is_string( $args['fallback_cb'] ) )
    863881            &&
     882            // ...and if the walker can also be serialized to JSON, since it will be included in the placement context data as well,
    864883            ( empty( $args['walker'] ) || is_string( $args['walker'] ) )
    865             &&
    866             (
     884            // ...and if it has a theme location assigned or an assigned menu to display,
     885            && (
    867886                ! empty( $args['theme_location'] )
    868887                ||
    869888                ( ! empty( $args['menu'] ) && ( is_numeric( $args['menu'] ) || is_object( $args['menu'] ) ) )
    870889            )
     890            &&
     891            // ...and if the nav menu would be rendered with a wrapper container element (upon which to attach data-* attributes).
     892            (
     893                ! empty( $args['container'] )
     894                ||
     895                ( isset( $args['items_wrap'] ) && '<' === substr( $args['items_wrap'], 0, 1 ) )
     896            )
    871897        );
    872         $args['can_partial_refresh'] = $can_partial_refresh;
    873 
    874         $hashed_args = $args;
    875 
    876         if ( ! $can_partial_refresh ) {
    877             $hashed_args['fallback_cb'] = '';
    878             $hashed_args['walker'] = '';
    879         }
    880 
    881         // Replace object menu arg with a term_id menu arg, as this exports better to JS and is easier to compare hashes.
    882         if ( ! empty( $hashed_args['menu'] ) && is_object( $hashed_args['menu'] ) ) {
    883             $hashed_args['menu'] = $hashed_args['menu']->term_id;
    884         }
    885 
    886         ksort( $hashed_args );
    887         $hashed_args['args_hash'] = $this->hash_nav_menu_args( $hashed_args );
    888 
    889         $this->preview_nav_menu_instance_args[ $this->preview_nav_menu_instance_number ] = $hashed_args;
     898
     899        if ( ! $can_selective_refresh ) {
     900            return $args;
     901        }
     902
     903        $exported_args = $args;
     904
     905        /*
     906         * Replace object menu arg with a term_id menu arg, as this exports better
     907         * to JS and is easier to compare hashes.
     908         */
     909        if ( ! empty( $exported_args['menu'] ) && is_object( $exported_args['menu'] ) ) {
     910            $exported_args['menu'] = $exported_args['menu']->term_id;
     911        }
     912
     913        ksort( $exported_args );
     914        $exported_args['args_hmac'] = $this->hash_nav_menu_args( $exported_args );
     915
     916        $args['customize_preview_nav_menus_args'] = $exported_args;
     917
    890918        return $args;
    891919    }
    892920
    893921    /**
    894      * Prepare wp_nav_menu() calls for partial refresh. Wraps output in container for refreshing.
     922     * Prepares wp_nav_menu() calls for partial refresh.
     923     *
     924     * Injects attributes into container element.
    895925     *
    896926     * @since 4.3.0
     
    904934     */
    905935    public function filter_wp_nav_menu( $nav_menu_content, $args ) {
    906         if ( ! empty( $args->can_partial_refresh ) && ! empty( $args->instance_number ) ) {
    907             $nav_menu_content = preg_replace(
    908                 '/(?<=class=")/',
    909                 sprintf( 'partial-refreshable-nav-menu partial-refreshable-nav-menu-%1$d ', $args->instance_number ),
    910                 $nav_menu_content,
    911                 1 // Only update the class on the first element found, the menu container.
    912             );
     936        if ( ! empty( $args->customize_preview_nav_menus_args ) ) {
     937            $attributes = sprintf( ' data-customize-partial-id="%s"', esc_attr( 'nav_menu_instance[' . $args->customize_preview_nav_menus_args['args_hmac'] . ']' ) );
     938            $attributes .= ' data-customize-partial-type="nav_menu_instance"';
     939            $attributes .= sprintf( ' data-customize-partial-placement-context="%s"', esc_attr( wp_json_encode( $args->customize_preview_nav_menus_args ) ) );
     940            $nav_menu_content = preg_replace( '#^(<\w+)#', '$1 ' . $attributes, $nav_menu_content, 1 );
    913941        }
    914942        return $nav_menu_content;
     
    916944
    917945    /**
    918      * Hash (hmac) the arguments with the nonce and secret auth key to ensure they
    919      * are not tampered with when submitted in the Ajax request.
     946     * Hashes (hmac) the nav menu arguments to ensure they are not tampered with when
     947     * submitted in the Ajax request.
     948     *
     949     * Note that the array is expected to be pre-sorted.
    920950     *
    921951     * @since 4.3.0
     
    923953     *
    924954     * @param array $args The arguments to hash.
    925      * @return string
     955     * @return string Hashed nav menu arguments.
    926956     */
    927957    public function hash_nav_menu_args( $args ) {
    928         return wp_hash( wp_create_nonce( self::RENDER_AJAX_ACTION ) . serialize( $args ) );
     958        return wp_hash( serialize( $args ) );
    929959    }
    930960
     
    936966     */
    937967    public function customize_preview_enqueue_deps() {
    938         wp_enqueue_script( 'customize-preview-nav-menus' );
     968        if ( isset( $this->manager->selective_refresh ) ) {
     969            $script = wp_scripts()->registered['customize-preview-nav-menus'];
     970            $script->deps[] = 'customize-selective-refresh';
     971        }
     972
     973        wp_enqueue_script( 'customize-preview-nav-menus' ); // Note that we have overridden this.
    939974        wp_enqueue_style( 'customize-preview' );
    940 
    941         add_action( 'wp_print_footer_scripts', array( $this, 'export_preview_data' ) );
    942     }
    943 
    944     /**
    945      * Export data from PHP to JS.
    946      *
    947      * @since 4.3.0
     975    }
     976
     977    /**
     978     * Exports data from PHP to JS.
     979     *
     980     * @since 4.3.0
     981     * @deprecated 4.5.0 Obsolete
    948982     * @access public
    949983     */
    950984    public function export_preview_data() {
    951 
    952         // Why not wp_localize_script? Because we're not localizing, and it forces values into strings.
    953         $exports = array(
    954             'renderQueryVar'        => self::RENDER_QUERY_VAR,
    955             'renderNonceValue'      => wp_create_nonce( self::RENDER_AJAX_ACTION ),
    956             'renderNoncePostKey'    => self::RENDER_NONCE_POST_KEY,
    957             'navMenuInstanceArgs'   => $this->preview_nav_menu_instance_args,
    958             'l10n'                  => array(
    959                 'editNavMenuItemTooltip' => __( 'Shift-click to edit this menu item.' ),
    960             ),
    961         );
    962 
    963         printf( '<script>var _wpCustomizePreviewNavMenusExports = %s;</script>', wp_json_encode( $exports ) );
     985        _deprecated_function( __METHOD__, '4.5.0' );
    964986    }
    965987
     
    971993     *
    972994     * @see wp_nav_menu()
    973      */
    974     public function render_menu() {
    975         if ( empty( $_POST[ self::RENDER_QUERY_VAR ] ) ) {
    976             return;
    977         }
    978 
    979         $this->manager->remove_preview_signature();
    980 
    981         if ( empty( $_POST[ self::RENDER_NONCE_POST_KEY ] ) ) {
    982             wp_send_json_error( 'missing_nonce_param' );
    983         }
    984 
    985         if ( ! is_customize_preview() ) {
    986             wp_send_json_error( 'expected_customize_preview' );
    987         }
    988 
    989         if ( ! check_ajax_referer( self::RENDER_AJAX_ACTION, self::RENDER_NONCE_POST_KEY, false ) ) {
    990             wp_send_json_error( 'nonce_check_fail' );
    991         }
    992 
    993         if ( ! current_user_can( 'edit_theme_options' ) ) {
    994             wp_send_json_error( 'unauthorized' );
    995         }
    996 
    997         if ( ! isset( $_POST['wp_nav_menu_args'] ) ) {
    998             wp_send_json_error( 'missing_param' );
    999         }
    1000 
    1001         if ( ! isset( $_POST['wp_nav_menu_args_hash'] ) ) {
    1002             wp_send_json_error( 'missing_param' );
    1003         }
    1004 
    1005         $wp_nav_menu_args = json_decode( wp_unslash( $_POST['wp_nav_menu_args'] ), true );
    1006         if ( ! is_array( $wp_nav_menu_args ) ) {
    1007             wp_send_json_error( 'wp_nav_menu_args_not_array' );
    1008         }
    1009 
    1010         $wp_nav_menu_args_hash = sanitize_text_field( wp_unslash( $_POST['wp_nav_menu_args_hash'] ) );
    1011         if ( ! hash_equals( $this->hash_nav_menu_args( $wp_nav_menu_args ), $wp_nav_menu_args_hash ) ) {
    1012             wp_send_json_error( 'wp_nav_menu_args_hash_mismatch' );
    1013         }
    1014 
    1015         $wp_nav_menu_args['echo'] = false;
    1016         wp_send_json_success( wp_nav_menu( $wp_nav_menu_args ) );
     995     *
     996     * @param WP_Customize_Partial $partial       Partial.
     997     * @param array                $nav_menu_args Nav menu args supplied as container context.
     998     * @return string|false
     999     */
     1000    public function render_nav_menu_partial( $partial, $nav_menu_args ) {
     1001        unset( $partial );
     1002
     1003        if ( ! isset( $nav_menu_args['args_hmac'] ) ) {
     1004            // Error: missing_args_hmac.
     1005            return false;
     1006        }
     1007
     1008        $nav_menu_args_hmac = $nav_menu_args['args_hmac'];
     1009        unset( $nav_menu_args['args_hmac'] );
     1010
     1011        ksort( $nav_menu_args );
     1012        if ( ! hash_equals( $this->hash_nav_menu_args( $nav_menu_args ), $nav_menu_args_hmac ) ) {
     1013            // Error: args_hmac_mismatch.
     1014            return false;
     1015        }
     1016
     1017        ob_start();
     1018        wp_nav_menu( $nav_menu_args );
     1019        $content = ob_get_clean();
     1020
     1021        return $content;
    10171022    }
    10181023}
Note: See TracChangeset for help on using the changeset viewer.