48
48
$this->previewed_menus = array();
49
49
$this->manager = $manager;
51
// Skip useless hooks when the user can't manage nav menus anyway.
52
if ( ! current_user_can( 'edit_theme_options' ) ) {
56
add_filter( 'customize_refresh_nonces', array( $this, 'filter_nonces' ) );
51
57
add_action( 'wp_ajax_load-available-menu-items-customizer', array( $this, 'ajax_load_available_items' ) );
52
58
add_action( 'wp_ajax_search-available-menu-items-customizer', array( $this, 'ajax_search_available_items' ) );
53
59
add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
60
66
add_action( 'customize_controls_print_footer_scripts', array( $this, 'print_templates' ) );
61
67
add_action( 'customize_controls_print_footer_scripts', array( $this, 'available_items_template' ) );
62
68
add_action( 'customize_preview_init', array( $this, 'customize_preview_init' ) );
70
// Selective Refresh partials.
71
add_filter( 'customize_dynamic_partial_args', array( $this, 'customize_dynamic_partial_args' ), 10, 2 );
75
* Adds a nonce for customizing menus.
80
* @param array $nonces Array of nonces.
81
* @return array $nonces Modified array of nonces.
83
public function filter_nonces( $nonces ) {
84
$nonces['customize-menus'] = wp_create_nonce( 'customize-menus' );
330
363
// Pass data to JS.
331
364
$settings = array(
332
'nonce' => wp_create_nonce( 'customize-menus' ),
333
365
'allMenus' => wp_get_nav_menus(),
334
366
'itemTypes' => $this->available_item_types(),
336
368
'untitled' => _x( '(no label)', 'missing menu item navigation label' ),
337
369
'unnamed' => _x( '(unnamed)', 'Missing menu name.' ),
338
370
'custom_label' => __( 'Custom Link' ),
339
/* translators: %s: menu location slug */
371
/* translators: %s: menu location */
340
372
'menuLocation' => _x( '(Currently set to: %s)', 'menu' ),
341
373
'menuNameLabel' => __( 'Menu Name' ),
342
374
'itemAdded' => __( 'Menu item added' ),
361
393
'reorderLabelOn' => esc_attr__( 'Reorder menu items' ),
362
394
'reorderLabelOff' => esc_attr__( 'Close reorder mode' ),
364
'menuItemTransport' => 'postMessage',
396
'settingTransport' => 'postMessage',
365
397
'phpIntMax' => PHP_INT_MAX,
366
398
'defaultSettingValues' => array(
367
399
'nav_menu' => $temp_nav_menu_setting->default,
368
400
'nav_menu_item' => $temp_nav_menu_item_setting->default,
402
'locationSlugMappedToName' => get_registered_nav_menus(),
372
405
$data = sprintf( 'var _wpCustomizeNavMenusSettings = %s;', wp_json_encode( $settings ) );
411
444
public function filter_dynamic_setting_args( $setting_args, $setting_id ) {
412
445
if ( preg_match( WP_Customize_Nav_Menu_Setting::ID_PATTERN, $setting_id ) ) {
413
446
$setting_args = array(
414
'type' => WP_Customize_Nav_Menu_Setting::TYPE,
447
'type' => WP_Customize_Nav_Menu_Setting::TYPE,
448
'transport' => 'postMessage',
416
450
} elseif ( preg_match( WP_Customize_Nav_Menu_Item_Setting::ID_PATTERN, $setting_id ) ) {
417
451
$setting_args = array(
418
'type' => WP_Customize_Nav_Menu_Item_Setting::TYPE,
452
'type' => WP_Customize_Nav_Menu_Item_Setting::TYPE,
453
'transport' => 'postMessage',
421
456
return $setting_args;
568
606
'priority' => 999,
571
$this->manager->add_setting( 'new_menu_name', array(
572
'type' => 'new_menu',
574
'transport' => 'postMessage',
577
609
$this->manager->add_control( 'new_menu_name', array(
579
611
'section' => 'add_menu',
580
612
'type' => 'text',
613
'settings' => array(),
581
614
'input_attrs' => array(
582
615
'class' => 'menu-name-field',
583
616
'placeholder' => __( 'New menu name' ),
587
$this->manager->add_setting( 'create_new_menu', array(
588
'type' => 'new_menu',
591
620
$this->manager->add_control( new WP_Customize_New_Menu_Control( $this->manager, 'create_new_menu', array(
592
'section' => 'add_menu',
621
'section' => 'add_menu',
622
'settings' => array(),
743
773
<span class="toggle-indicator" aria-hidden="true"></span>
746
<div class="accordion-section-content">
776
<div class="accordion-section-content customlinkdiv">
747
777
<input type="hidden" value="custom" id="custom-menu-item-type" name="menu-item[-1][menu-item-type]" />
748
<p id="menu-item-url-wrap">
749
<label class="howto" for="custom-menu-item-url">
750
<span><?php _e( 'URL' ); ?></span>
751
<input id="custom-menu-item-url" name="menu-item[-1][menu-item-url]" type="text" class="code menu-item-textbox" value="http://">
778
<p id="menu-item-url-wrap" class="wp-clearfix">
779
<label class="howto" for="custom-menu-item-url"><?php _e( 'URL' ); ?></label>
780
<input id="custom-menu-item-url" name="menu-item[-1][menu-item-url]" type="text" class="code menu-item-textbox" value="http://">
754
<p id="menu-item-name-wrap">
755
<label class="howto" for="custom-menu-item-name">
756
<span><?php _e( 'Link Text' ); ?></span>
757
<input id="custom-menu-item-name" name="menu-item[-1][menu-item-title]" type="text" class="regular-text menu-item-textbox">
782
<p id="menu-item-name-wrap" class="wp-clearfix">
783
<label class="howto" for="custom-menu-item-name"><?php _e( 'Link Text' ); ?></label>
784
<input id="custom-menu-item-name" name="menu-item[-1][menu-item-title]" type="text" class="regular-text menu-item-textbox">
760
786
<p class="button-controls">
761
787
<span class="add-to-menu">
794
821
// Start functionality specific to partial-refresh of menu changes in Customizer preview.
795
const RENDER_AJAX_ACTION = 'customize_render_menu_partial';
796
const RENDER_NONCE_POST_KEY = 'render-menu-nonce';
797
const RENDER_QUERY_VAR = 'wp_customize_menu_render';
800
* The number of wp_nav_menu() calls which have happened in the preview.
806
public $preview_nav_menu_instance_number = 0;
809
* Nav menu args used for each instance.
825
* Nav menu args used for each instance, keyed by the args HMAC.
815
831
public $preview_nav_menu_instance_args = array();
834
* Filter arguments for dynamic nav_menu selective refresh partials.
839
* @param array|false $partial_args Partial args.
840
* @param string $partial_id Partial ID.
841
* @return array Partial args.
843
public function customize_dynamic_partial_args( $partial_args, $partial_id ) {
845
if ( preg_match( '/^nav_menu_instance\[[0-9a-f]{32}\]$/', $partial_id ) ) {
846
if ( false === $partial_args ) {
847
$partial_args = array();
849
$partial_args = array_merge(
852
'type' => 'nav_menu_instance',
853
'render_callback' => array( $this, 'render_nav_menu_partial' ),
854
'container_inclusive' => true,
855
'settings' => array(), // Empty because the nav menu instance may relate to a menu or a location.
856
'capability' => 'edit_theme_options',
861
return $partial_args;
818
865
* Add hooks for the Customizer preview.
823
870
public function customize_preview_init() {
824
add_action( 'template_redirect', array( $this, 'render_menu' ) );
825
871
add_action( 'wp_enqueue_scripts', array( $this, 'customize_preview_enqueue_deps' ) );
827
if ( ! isset( $_REQUEST[ self::RENDER_QUERY_VAR ] ) ) {
828
add_filter( 'wp_nav_menu_args', array( $this, 'filter_wp_nav_menu_args' ), 1000 );
829
add_filter( 'wp_nav_menu', array( $this, 'filter_wp_nav_menu' ), 10, 2 );
872
add_filter( 'wp_nav_menu_args', array( $this, 'filter_wp_nav_menu_args' ), 1000 );
873
add_filter( 'wp_nav_menu', array( $this, 'filter_wp_nav_menu' ), 10, 2 );
874
add_filter( 'wp_footer', array( $this, 'export_preview_data' ), 1 );
875
add_filter( 'customize_render_partials_response', array( $this, 'export_partial_rendered_nav_menu_instances' ) );
839
883
* @see wp_nav_menu()
884
* @see WP_Customize_Widgets_Partial_Refresh::filter_dynamic_sidebar_params()
841
886
* @param array $args An array containing wp_nav_menu() arguments.
842
887
* @return array Arguments.
844
889
public function filter_wp_nav_menu_args( $args ) {
845
$this->preview_nav_menu_instance_number += 1;
846
$args['instance_number'] = $this->preview_nav_menu_instance_number;
891
* The following conditions determine whether or not this instance of
892
* wp_nav_menu() can use selective refreshed. A wp_nav_menu() can be
893
* selective refreshed if...
848
895
$can_partial_refresh = (
896
// ...if wp_nav_menu() is directly echoing out the menu (and thus isn't manipulating the string after generated),
849
897
! empty( $args['echo'] )
899
// ...and if the fallback_cb can be serialized to JSON, since it will be included in the placement context data,
851
900
( empty( $args['fallback_cb'] ) || is_string( $args['fallback_cb'] ) )
902
// ...and if the walker can also be serialized to JSON, since it will be included in the placement context data as well,
853
903
( empty( $args['walker'] ) || is_string( $args['walker'] ) )
904
// ...and if it has a theme location assigned or an assigned menu to display,
856
906
! empty( $args['theme_location'] )
858
908
( ! empty( $args['menu'] ) && ( is_numeric( $args['menu'] ) || is_object( $args['menu'] ) ) )
911
// ...and if the nav menu would be rendered with a wrapper container element (upon which to attach data-* attributes).
913
! empty( $args['container'] )
915
( isset( $args['items_wrap'] ) && '<' === substr( $args['items_wrap'], 0, 1 ) )
861
918
$args['can_partial_refresh'] = $can_partial_refresh;
863
$hashed_args = $args;
920
$exported_args = $args;
922
// Empty out args which may not be JSON-serializable.
865
923
if ( ! $can_partial_refresh ) {
866
$hashed_args['fallback_cb'] = '';
867
$hashed_args['walker'] = '';
870
// Replace object menu arg with a term_id menu arg, as this exports better to JS and is easier to compare hashes.
871
if ( ! empty( $hashed_args['menu'] ) && is_object( $hashed_args['menu'] ) ) {
872
$hashed_args['menu'] = $hashed_args['menu']->term_id;
875
ksort( $hashed_args );
876
$hashed_args['args_hash'] = $this->hash_nav_menu_args( $hashed_args );
878
$this->preview_nav_menu_instance_args[ $this->preview_nav_menu_instance_number ] = $hashed_args;
924
$exported_args['fallback_cb'] = '';
925
$exported_args['walker'] = '';
929
* Replace object menu arg with a term_id menu arg, as this exports better
930
* to JS and is easier to compare hashes.
932
if ( ! empty( $exported_args['menu'] ) && is_object( $exported_args['menu'] ) ) {
933
$exported_args['menu'] = $exported_args['menu']->term_id;
936
ksort( $exported_args );
937
$exported_args['args_hmac'] = $this->hash_nav_menu_args( $exported_args );
939
$args['customize_preview_nav_menus_args'] = $exported_args;
940
$this->preview_nav_menu_instance_args[ $exported_args['args_hmac'] ] = $exported_args;
883
* Prepare wp_nav_menu() calls for partial refresh. Wraps output in container for refreshing.
945
* Prepares wp_nav_menu() calls for partial refresh.
947
* Injects attributes into container element.
894
958
public function filter_wp_nav_menu( $nav_menu_content, $args ) {
895
if ( ! empty( $args->can_partial_refresh ) && ! empty( $args->instance_number ) ) {
896
$nav_menu_content = preg_replace(
898
sprintf( 'partial-refreshable-nav-menu partial-refreshable-nav-menu-%1$d ', $args->instance_number ),
900
1 // Only update the class on the first element found, the menu container.
959
if ( isset( $args->customize_preview_nav_menus_args['can_partial_refresh'] ) && $args->customize_preview_nav_menus_args['can_partial_refresh'] ) {
960
$attributes = sprintf( ' data-customize-partial-id="%s"', esc_attr( 'nav_menu_instance[' . $args->customize_preview_nav_menus_args['args_hmac'] . ']' ) );
961
$attributes .= ' data-customize-partial-type="nav_menu_instance"';
962
$attributes .= sprintf( ' data-customize-partial-placement-context="%s"', esc_attr( wp_json_encode( $args->customize_preview_nav_menus_args ) ) );
963
$nav_menu_content = preg_replace( '#^(<\w+)#', '$1 ' . $attributes, $nav_menu_content, 1 );
903
965
return $nav_menu_content;
907
* Hash (hmac) the arguments with the nonce and secret auth key to ensure they
908
* are not tampered with when submitted in the Ajax request.
969
* Hashes (hmac) the nav menu arguments to ensure they are not tampered with when
970
* submitted in the Ajax request.
972
* Note that the array is expected to be pre-sorted.
913
977
* @param array $args The arguments to hash.
978
* @return string Hashed nav menu arguments.
916
980
public function hash_nav_menu_args( $args ) {
917
return wp_hash( wp_create_nonce( self::RENDER_AJAX_ACTION ) . serialize( $args ) );
981
return wp_hash( serialize( $args ) );
941
1003
// Why not wp_localize_script? Because we're not localizing, and it forces values into strings.
942
1004
$exports = array(
943
'renderQueryVar' => self::RENDER_QUERY_VAR,
944
'renderNonceValue' => wp_create_nonce( self::RENDER_AJAX_ACTION ),
945
'renderNoncePostKey' => self::RENDER_NONCE_POST_KEY,
946
'requestUri' => empty( $_SERVER['REQUEST_URI'] ) ? home_url( '/' ) : esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ),
948
'stylesheet' => $this->manager->get_stylesheet(),
949
'active' => $this->manager->is_theme_active(),
951
'previewCustomizeNonce' => wp_create_nonce( 'preview-customize_' . $this->manager->get_stylesheet() ),
952
'navMenuInstanceArgs' => $this->preview_nav_menu_instance_args,
1005
'navMenuInstanceArgs' => $this->preview_nav_menu_instance_args,
955
1007
printf( '<script>var _wpCustomizePreviewNavMenusExports = %s;</script>', wp_json_encode( $exports ) );
1011
* Export any wp_nav_menu() calls during the rendering of any partials.
1016
* @param array $response Response.
1017
* @return array Response.
1019
public function export_partial_rendered_nav_menu_instances( $response ) {
1020
$response['nav_menu_instance_args'] = $this->preview_nav_menu_instance_args;
959
1025
* Render a specific menu via wp_nav_menu() using the supplied arguments.
962
1028
* @access public
964
1030
* @see wp_nav_menu()
1032
* @param WP_Customize_Partial $partial Partial.
1033
* @param array $nav_menu_args Nav menu args supplied as container context.
1034
* @return string|false
966
public function render_menu() {
967
if ( empty( $_POST[ self::RENDER_QUERY_VAR ] ) ) {
971
$this->manager->remove_preview_signature();
973
if ( empty( $_POST[ self::RENDER_NONCE_POST_KEY ] ) ) {
974
wp_send_json_error( 'missing_nonce_param' );
977
if ( ! is_customize_preview() ) {
978
wp_send_json_error( 'expected_customize_preview' );
981
if ( ! check_ajax_referer( self::RENDER_AJAX_ACTION, self::RENDER_NONCE_POST_KEY, false ) ) {
982
wp_send_json_error( 'nonce_check_fail' );
985
if ( ! current_user_can( 'edit_theme_options' ) ) {
986
wp_send_json_error( 'unauthorized' );
989
if ( ! isset( $_POST['wp_nav_menu_args'] ) ) {
990
wp_send_json_error( 'missing_param' );
993
if ( ! isset( $_POST['wp_nav_menu_args_hash'] ) ) {
994
wp_send_json_error( 'missing_param' );
997
$wp_nav_menu_args = json_decode( wp_unslash( $_POST['wp_nav_menu_args'] ), true );
998
if ( ! is_array( $wp_nav_menu_args ) ) {
999
wp_send_json_error( 'wp_nav_menu_args_not_array' );
1002
$wp_nav_menu_args_hash = sanitize_text_field( wp_unslash( $_POST['wp_nav_menu_args_hash'] ) );
1003
if ( ! hash_equals( $this->hash_nav_menu_args( $wp_nav_menu_args ), $wp_nav_menu_args_hash ) ) {
1004
wp_send_json_error( 'wp_nav_menu_args_hash_mismatch' );
1007
$wp_nav_menu_args['echo'] = false;
1008
wp_send_json_success( wp_nav_menu( $wp_nav_menu_args ) );
1036
public function render_nav_menu_partial( $partial, $nav_menu_args ) {
1039
if ( ! isset( $nav_menu_args['args_hmac'] ) ) {
1040
// Error: missing_args_hmac.
1044
$nav_menu_args_hmac = $nav_menu_args['args_hmac'];
1045
unset( $nav_menu_args['args_hmac'] );
1047
ksort( $nav_menu_args );
1048
if ( ! hash_equals( $this->hash_nav_menu_args( $nav_menu_args ), $nav_menu_args_hmac ) ) {
1049
// Error: args_hmac_mismatch.
1054
wp_nav_menu( $nav_menu_args );
1055
$content = ob_get_clean();