WordPress 4.3
[autoinstalls/wordpress.git] / wp-includes / class-wp-customize-nav-menus.php
1 <?php
2 /**
3  * WordPress Customize Nav Menus classes
4  *
5  * @package WordPress
6  * @subpackage Customize
7  * @since 4.3.0
8  */
9
10 /**
11  * Customize Nav Menus class.
12  *
13  * Implements menu management in the Customizer.
14  *
15  * @since 4.3.0
16  *
17  * @see WP_Customize_Manager
18  */
19 final class WP_Customize_Nav_Menus {
20
21         /**
22          * WP_Customize_Manager instance.
23          *
24          * @since 4.3.0
25          * @access public
26          * @var WP_Customize_Manager
27          */
28         public $manager;
29
30         /**
31          * Previewed Menus.
32          *
33          * @since 4.3.0
34          * @access public
35          * @var array
36          */
37         public $previewed_menus;
38
39         /**
40          * Constructor.
41          *
42          * @since 4.3.0
43          * @access public
44          *
45          * @param object $manager An instance of the WP_Customize_Manager class.
46          */
47         public function __construct( $manager ) {
48                 $this->previewed_menus = array();
49                 $this->manager         = $manager;
50
51                 add_action( 'wp_ajax_load-available-menu-items-customizer', array( $this, 'ajax_load_available_items' ) );
52                 add_action( 'wp_ajax_search-available-menu-items-customizer', array( $this, 'ajax_search_available_items' ) );
53                 add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
54
55                 // Needs to run after core Navigation section is set up.
56                 add_action( 'customize_register', array( $this, 'customize_register' ), 11 );
57
58                 add_filter( 'customize_dynamic_setting_args', array( $this, 'filter_dynamic_setting_args' ), 10, 2 );
59                 add_filter( 'customize_dynamic_setting_class', array( $this, 'filter_dynamic_setting_class' ), 10, 3 );
60                 add_action( 'customize_controls_print_footer_scripts', array( $this, 'print_templates' ) );
61                 add_action( 'customize_controls_print_footer_scripts', array( $this, 'available_items_template' ) );
62                 add_action( 'customize_preview_init', array( $this, 'customize_preview_init' ) );
63         }
64
65         /**
66          * Ajax handler for loading available menu items.
67          *
68          * @since 4.3.0
69          * @access public
70          */
71         public function ajax_load_available_items() {
72                 check_ajax_referer( 'customize-menus', 'customize-menus-nonce' );
73
74                 if ( ! current_user_can( 'edit_theme_options' ) ) {
75                         wp_die( -1 );
76                 }
77
78                 if ( empty( $_POST['type'] ) || empty( $_POST['object'] ) ) {
79                         wp_send_json_error( 'nav_menus_missing_type_or_object_parameter' );
80                 }
81
82                 $type = sanitize_key( $_POST['type'] );
83                 $object = sanitize_key( $_POST['object'] );
84                 $page = empty( $_POST['page'] ) ? 0 : absint( $_POST['page'] );
85                 $items = $this->load_available_items_query( $type, $object, $page );
86
87                 if ( is_wp_error( $items ) ) {
88                         wp_send_json_error( $items->get_error_code() );
89                 } else {
90                         wp_send_json_success( array( 'items' => $items ) );
91                 }
92         }
93
94         /**
95          * Performs the post_type and taxonomy queries for loading available menu items.
96          *
97          * @since 4.3.0
98          * @access public
99          *
100          * @param string $type   Optional. Accepts any custom object type and has built-in support for
101          *                         'post_type' and 'taxonomy'. Default is 'post_type'.
102          * @param string $object Optional. Accepts any registered taxonomy or post type name. Default is 'page'.
103          * @param int    $page   Optional. The page number used to generate the query offset. Default is '0'.
104          * @return WP_Error|array Returns either a WP_Error object or an array of menu items.
105          */
106         public function load_available_items_query( $type = 'post_type', $object = 'page', $page = 0 ) {
107                 $items = array();
108
109                 if ( 'post_type' === $type ) {
110                         if ( ! get_post_type_object( $object ) ) {
111                                 return new WP_Error( 'nav_menus_invalid_post_type' );
112                         }
113
114                         if ( 0 === $page && 'page' === $object ) {
115                                 // Add "Home" link. Treat as a page, but switch to custom on add.
116                                 $items[] = array(
117                                         'id'         => 'home',
118                                         'title'      => _x( 'Home', 'nav menu home label' ),
119                                         'type'       => 'custom',
120                                         'type_label' => __( 'Custom Link' ),
121                                         'object'     => '',
122                                         'url'        => home_url(),
123                                 );
124                         }
125
126                         $posts = get_posts( array(
127                                 'numberposts' => 10,
128                                 'offset'      => 10 * $page,
129                                 'orderby'     => 'date',
130                                 'order'       => 'DESC',
131                                 'post_type'   => $object,
132                         ) );
133                         foreach ( $posts as $post ) {
134                                 $post_title = $post->post_title;
135                                 if ( '' === $post_title ) {
136                                         /* translators: %d: ID of a post */
137                                         $post_title = sprintf( __( '#%d (no title)' ), $post->ID );
138                                 }
139                                 $items[] = array(
140                                         'id'         => "post-{$post->ID}",
141                                         'title'      => html_entity_decode( $post_title, ENT_QUOTES, get_bloginfo( 'charset' ) ),
142                                         'type'       => 'post_type',
143                                         'type_label' => get_post_type_object( $post->post_type )->labels->singular_name,
144                                         'object'     => $post->post_type,
145                                         'object_id'  => intval( $post->ID ),
146                                         'url'        => get_permalink( intval( $post->ID ) ),
147                                 );
148                         }
149                 } elseif ( 'taxonomy' === $type ) {
150                         $terms = get_terms( $object, array(
151                                 'child_of'     => 0,
152                                 'exclude'      => '',
153                                 'hide_empty'   => false,
154                                 'hierarchical' => 1,
155                                 'include'      => '',
156                                 'number'       => 10,
157                                 'offset'       => 10 * $page,
158                                 'order'        => 'DESC',
159                                 'orderby'      => 'count',
160                                 'pad_counts'   => false,
161                         ) );
162                         if ( is_wp_error( $terms ) ) {
163                                 return $terms;
164                         }
165
166                         foreach ( $terms as $term ) {
167                                 $items[] = array(
168                                         'id'         => "term-{$term->term_id}",
169                                         'title'      => html_entity_decode( $term->name, ENT_QUOTES, get_bloginfo( 'charset' ) ),
170                                         'type'       => 'taxonomy',
171                                         'type_label' => get_taxonomy( $term->taxonomy )->labels->singular_name,
172                                         'object'     => $term->taxonomy,
173                                         'object_id'  => intval( $term->term_id ),
174                                         'url'        => get_term_link( intval( $term->term_id ), $term->taxonomy ),
175                                 );
176                         }
177                 }
178
179                 /**
180                  * Filter the available menu items.
181                  *
182                  * @since 4.3.0
183                  *
184                  * @param array  $items  The array of menu items.
185                  * @param string $type   The object type.
186                  * @param string $object The object name.
187                  * @param int    $page   The current page number.
188                  */
189                 $items = apply_filters( 'customize_nav_menu_available_items', $items, $type, $object, $page );
190
191                 return $items;
192         }
193
194         /**
195          * Ajax handler for searching available menu items.
196          *
197          * @since 4.3.0
198          * @access public
199          */
200         public function ajax_search_available_items() {
201                 check_ajax_referer( 'customize-menus', 'customize-menus-nonce' );
202
203                 if ( ! current_user_can( 'edit_theme_options' ) ) {
204                         wp_die( -1 );
205                 }
206
207                 if ( empty( $_POST['search'] ) ) {
208                         wp_send_json_error( 'nav_menus_missing_search_parameter' );
209                 }
210
211                 $p = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 0;
212                 if ( $p < 1 ) {
213                         $p = 1;
214                 }
215
216                 $s = sanitize_text_field( wp_unslash( $_POST['search'] ) );
217                 $items = $this->search_available_items_query( array( 'pagenum' => $p, 's' => $s ) );
218
219                 if ( empty( $items ) ) {
220                         wp_send_json_error( array( 'message' => __( 'No results found.' ) ) );
221                 } else {
222                         wp_send_json_success( array( 'items' => $items ) );
223                 }
224         }
225
226         /**
227          * Performs post queries for available-item searching.
228          *
229          * Based on WP_Editor::wp_link_query().
230          *
231          * @since 4.3.0
232          * @access public
233          *
234          * @param array $args Optional. Accepts 'pagenum' and 's' (search) arguments.
235          * @return array Menu items.
236          */
237         public function search_available_items_query( $args = array() ) {
238                 $items = array();
239
240                 $post_type_objects = get_post_types( array( 'show_in_nav_menus' => true ), 'objects' );
241                 $query = array(
242                         'post_type'              => array_keys( $post_type_objects ),
243                         'suppress_filters'       => true,
244                         'update_post_term_cache' => false,
245                         'update_post_meta_cache' => false,
246                         'post_status'            => 'publish',
247                         'posts_per_page'         => 20,
248                 );
249
250                 $args['pagenum'] = isset( $args['pagenum'] ) ? absint( $args['pagenum'] ) : 1;
251                 $query['offset'] = $args['pagenum'] > 1 ? $query['posts_per_page'] * ( $args['pagenum'] - 1 ) : 0;
252
253                 if ( isset( $args['s'] ) ) {
254                         $query['s'] = $args['s'];
255                 }
256
257                 // Query posts.
258                 $get_posts = new WP_Query( $query );
259
260                 // Check if any posts were found.
261                 if ( $get_posts->post_count ) {
262                         foreach ( $get_posts->posts as $post ) {
263                                 $post_title = $post->post_title;
264                                 if ( '' === $post_title ) {
265                                         /* translators: %d: ID of a post */
266                                         $post_title = sprintf( __( '#%d (no title)' ), $post->ID );
267                                 }
268                                 $items[] = array(
269                                         'id'         => 'post-' . $post->ID,
270                                         'title'      => html_entity_decode( $post_title, ENT_QUOTES, get_bloginfo( 'charset' ) ),
271                                         'type'       => 'post_type',
272                                         'type_label' => $post_type_objects[ $post->post_type ]->labels->singular_name,
273                                         'object'     => $post->post_type,
274                                         'object_id'  => intval( $post->ID ),
275                                         'url'        => get_permalink( intval( $post->ID ) ),
276                                 );
277                         }
278                 }
279
280                 // Query taxonomy terms.
281                 $taxonomies = get_taxonomies( array( 'show_in_nav_menus' => true ), 'names' );
282                 $terms = get_terms( $taxonomies, array(
283                         'name__like' => $args['s'],
284                         'number'     => 20,
285                         'offset'     => 20 * ($args['pagenum'] - 1),
286                 ) );
287
288                 // Check if any taxonomies were found.
289                 if ( ! empty( $terms ) ) {
290                         foreach ( $terms as $term ) {
291                                 $items[] = array(
292                                         'id'         => 'term-' . $term->term_id,
293                                         'title'      => html_entity_decode( $term->name, ENT_QUOTES, get_bloginfo( 'charset' ) ),
294                                         'type'       => 'taxonomy',
295                                         'type_label' => get_taxonomy( $term->taxonomy )->labels->singular_name,
296                                         'object'     => $term->taxonomy,
297                                         'object_id'  => intval( $term->term_id ),
298                                         'url'        => get_term_link( intval( $term->term_id ), $term->taxonomy ),
299                                 );
300                         }
301                 }
302
303                 return $items;
304         }
305
306         /**
307          * Enqueue scripts and styles for Customizer pane.
308          *
309          * @since 4.3.0
310          * @access public
311          */
312         public function enqueue_scripts() {
313                 wp_enqueue_style( 'customize-nav-menus' );
314                 wp_enqueue_script( 'customize-nav-menus' );
315
316                 $temp_nav_menu_setting      = new WP_Customize_Nav_Menu_Setting( $this->manager, 'nav_menu[-1]' );
317                 $temp_nav_menu_item_setting = new WP_Customize_Nav_Menu_Item_Setting( $this->manager, 'nav_menu_item[-1]' );
318
319                 // Pass data to JS.
320                 $settings = array(
321                         'nonce'                => wp_create_nonce( 'customize-menus' ),
322                         'allMenus'             => wp_get_nav_menus(),
323                         'itemTypes'            => $this->available_item_types(),
324                         'l10n'                 => array(
325                                 'untitled'          => _x( '(no label)', 'missing menu item navigation label' ),
326                                 'unnamed'           => _x( '(unnamed)', 'Missing menu name.' ),
327                                 'custom_label'      => __( 'Custom Link' ),
328                                 /* translators: %s: Current menu location */
329                                 'menuLocation'      => __( '(Currently set to: %s)' ),
330                                 'menuNameLabel'     => __( 'Menu Name' ),
331                                 'itemAdded'         => __( 'Menu item added' ),
332                                 'itemDeleted'       => __( 'Menu item deleted' ),
333                                 'menuAdded'         => __( 'Menu created' ),
334                                 'menuDeleted'       => __( 'Menu deleted' ),
335                                 'movedUp'           => __( 'Menu item moved up' ),
336                                 'movedDown'         => __( 'Menu item moved down' ),
337                                 'movedLeft'         => __( 'Menu item moved out of submenu' ),
338                                 'movedRight'        => __( 'Menu item is now a sub-item' ),
339                                 /* translators: &#9656; is the unicode right-pointing triangle, and %s is the section title in the Customizer */
340                                 'customizingMenus'  => sprintf( __( 'Customizing &#9656; %s' ), esc_html( $this->manager->get_panel( 'nav_menus' )->title ) ),
341                                 /* translators: %s: title of menu item which is invalid */
342                                 'invalidTitleTpl'   => __( '%s (Invalid)' ),
343                                 /* translators: %s: title of menu item in draft status */
344                                 'pendingTitleTpl'   => __( '%s (Pending)' ),
345                                 'taxonomyTermLabel' => __( 'Taxonomy' ),
346                                 'postTypeLabel'     => __( 'Post Type' ),
347                                 'itemsFound'        => __( 'Number of items found: %d' ),
348                                 'itemsFoundMore'    => __( 'Additional items found: %d' ),
349                                 'itemsLoadingMore'  => __( 'Loading more results... please wait.' ),
350                                 'reorderModeOn'     => __( 'Reorder mode enabled' ),
351                                 'reorderModeOff'    => __( 'Reorder mode closed' ),
352                                 'reorderLabelOn'    => esc_attr__( 'Reorder menu items' ),
353                                 'reorderLabelOff'   => esc_attr__( 'Close reorder mode' ),
354                         ),
355                         'menuItemTransport'    => 'postMessage',
356                         'phpIntMax'            => PHP_INT_MAX,
357                         'defaultSettingValues' => array(
358                                 'nav_menu'      => $temp_nav_menu_setting->default,
359                                 'nav_menu_item' => $temp_nav_menu_item_setting->default,
360                         ),
361                 );
362
363                 $data = sprintf( 'var _wpCustomizeNavMenusSettings = %s;', wp_json_encode( $settings ) );
364                 wp_scripts()->add_data( 'customize-nav-menus', 'data', $data );
365
366                 // This is copied from nav-menus.php, and it has an unfortunate object name of `menus`.
367                 $nav_menus_l10n = array(
368                         'oneThemeLocationNoMenus' => null,
369                         'moveUp'       => __( 'Move up one' ),
370                         'moveDown'     => __( 'Move down one' ),
371                         'moveToTop'    => __( 'Move to the top' ),
372                         /* translators: %s: previous item name */
373                         'moveUnder'    => __( 'Move under %s' ),
374                         /* translators: %s: previous item name */
375                         'moveOutFrom'  => __( 'Move out from under %s' ),
376                         /* translators: %s: previous item name */
377                         'under'        => __( 'Under %s' ),
378                         /* translators: %s: previous item name */
379                         'outFrom'      => __( 'Out from under %s' ),
380                         /* translators: 1: item name, 2: item position, 3: total number of items */
381                         'menuFocus'    => __( '%1$s. Menu item %2$d of %3$d.' ),
382                         /* translators: 1: item name, 2: item position, 3: parent item name */
383                         'subMenuFocus' => __( '%1$s. Sub item number %2$d under %3$s.' ),
384                 );
385                 wp_localize_script( 'nav-menu', 'menus', $nav_menus_l10n );
386         }
387
388         /**
389          * Filter a dynamic setting's constructor args.
390          *
391          * For a dynamic setting to be registered, this filter must be employed
392          * to override the default false value with an array of args to pass to
393          * the WP_Customize_Setting constructor.
394          *
395          * @since 4.3.0
396          * @access public
397          *
398          * @param false|array $setting_args The arguments to the WP_Customize_Setting constructor.
399          * @param string      $setting_id   ID for dynamic setting, usually coming from `$_POST['customized']`.
400          * @return array|false
401          */
402         public function filter_dynamic_setting_args( $setting_args, $setting_id ) {
403                 if ( preg_match( WP_Customize_Nav_Menu_Setting::ID_PATTERN, $setting_id ) ) {
404                         $setting_args = array(
405                                 'type' => WP_Customize_Nav_Menu_Setting::TYPE,
406                         );
407                 } elseif ( preg_match( WP_Customize_Nav_Menu_Item_Setting::ID_PATTERN, $setting_id ) ) {
408                         $setting_args = array(
409                                 'type' => WP_Customize_Nav_Menu_Item_Setting::TYPE,
410                         );
411                 }
412                 return $setting_args;
413         }
414
415         /**
416          * Allow non-statically created settings to be constructed with custom WP_Customize_Setting subclass.
417          *
418          * @since 4.3.0
419          * @access public
420          *
421          * @param string $setting_class WP_Customize_Setting or a subclass.
422          * @param string $setting_id    ID for dynamic setting, usually coming from `$_POST['customized']`.
423          * @param array  $setting_args  WP_Customize_Setting or a subclass.
424          * @return string
425          */
426         public function filter_dynamic_setting_class( $setting_class, $setting_id, $setting_args ) {
427                 unset( $setting_id );
428
429                 if ( ! empty( $setting_args['type'] ) && WP_Customize_Nav_Menu_Setting::TYPE === $setting_args['type'] ) {
430                         $setting_class = 'WP_Customize_Nav_Menu_Setting';
431                 } elseif ( ! empty( $setting_args['type'] ) && WP_Customize_Nav_Menu_Item_Setting::TYPE === $setting_args['type'] ) {
432                         $setting_class = 'WP_Customize_Nav_Menu_Item_Setting';
433                 }
434                 return $setting_class;
435         }
436
437         /**
438          * Add the customizer settings and controls.
439          *
440          * @since 4.3.0
441          * @access public
442          */
443         public function customize_register() {
444
445                 // Require JS-rendered control types.
446                 $this->manager->register_panel_type( 'WP_Customize_Nav_Menus_Panel' );
447                 $this->manager->register_control_type( 'WP_Customize_Nav_Menu_Control' );
448                 $this->manager->register_control_type( 'WP_Customize_Nav_Menu_Name_Control' );
449                 $this->manager->register_control_type( 'WP_Customize_Nav_Menu_Auto_Add_Control' );
450                 $this->manager->register_control_type( 'WP_Customize_Nav_Menu_Item_Control' );
451
452                 // Create a panel for Menus.
453                 $description = '<p>' . __( 'This panel is used for managing navigation menus for content you have already published on your site. You can create menus and add items for existing content such as pages, posts, categories, tags, formats, or custom links.' ) . '</p>';
454                 if ( current_theme_supports( 'widgets' ) ) {
455                         $description .= '<p>' . sprintf( __( 'Menus can be displayed in locations defined by your theme or in <a href="%s">widget areas</a> by adding a &#8220;Custom Menu&#8221; widget.' ), "javascript:wp.customize.panel( 'widgets' ).focus();" ) . '</p>';
456                 } else {
457                         $description .= '<p>' . __( 'Menus can be displayed in locations defined by your theme.' ) . '</p>';
458                 }
459                 $this->manager->add_panel( new WP_Customize_Nav_Menus_Panel( $this->manager, 'nav_menus', array(
460                         'title'       => __( 'Menus' ),
461                         'description' => $description,
462                         'priority'    => 100,
463                         // 'theme_supports' => 'menus|widgets', @todo allow multiple theme supports
464                 ) ) );
465                 $menus = wp_get_nav_menus();
466
467                 // Menu loactions.
468                 $locations     = get_registered_nav_menus();
469                 $num_locations = count( array_keys( $locations ) );
470                 $description   = '<p>' . sprintf( _n( 'Your theme contains %s menu location. Select which menu you would like to use.', 'Your theme contains %s menu locations. Select which menu appears in each location.', $num_locations ), number_format_i18n( $num_locations ) );
471                 $description  .= '</p><p>' . __( 'You can also place menus in widget areas with the Custom Menu widget.' ) . '</p>';
472
473                 $this->manager->add_section( 'menu_locations', array(
474                         'title'       => __( 'Menu Locations' ),
475                         'panel'       => 'nav_menus',
476                         'priority'    => 5,
477                         'description' => $description,
478                 ) );
479
480                 $choices = array( '0' => __( '&mdash; Select &mdash;' ) );
481                 foreach ( $menus as $menu ) {
482                         $choices[ $menu->term_id ] = wp_html_excerpt( $menu->name, 40, '&hellip;' );
483                 }
484
485                 foreach ( $locations as $location => $description ) {
486                         $setting_id = "nav_menu_locations[{$location}]";
487
488                         $setting = $this->manager->get_setting( $setting_id );
489                         if ( $setting ) {
490                                 $setting->transport = 'postMessage';
491                                 remove_filter( "customize_sanitize_{$setting_id}", 'absint' );
492                                 add_filter( "customize_sanitize_{$setting_id}", array( $this, 'intval_base10' ) );
493                         } else {
494                                 $this->manager->add_setting( $setting_id, array(
495                                         'sanitize_callback' => array( $this, 'intval_base10' ),
496                                         'theme_supports'    => 'menus',
497                                         'type'              => 'theme_mod',
498                                         'transport'         => 'postMessage',
499                                         'default'           => 0,
500                                 ) );
501                         }
502
503                         $this->manager->add_control( new WP_Customize_Nav_Menu_Location_Control( $this->manager, $setting_id, array(
504                                 'label'       => $description,
505                                 'location_id' => $location,
506                                 'section'     => 'menu_locations',
507                                 'choices'     => $choices,
508                         ) ) );
509                 }
510
511                 // Register each menu as a Customizer section, and add each menu item to each menu.
512                 foreach ( $menus as $menu ) {
513                         $menu_id = $menu->term_id;
514
515                         // Create a section for each menu.
516                         $section_id = 'nav_menu[' . $menu_id . ']';
517                         $this->manager->add_section( new WP_Customize_Nav_Menu_Section( $this->manager, $section_id, array(
518                                 'title'     => html_entity_decode( $menu->name, ENT_QUOTES, get_bloginfo( 'charset' ) ),
519                                 'priority'  => 10,
520                                 'panel'     => 'nav_menus',
521                         ) ) );
522
523                         $nav_menu_setting_id = 'nav_menu[' . $menu_id . ']';
524                         $this->manager->add_setting( new WP_Customize_Nav_Menu_Setting( $this->manager, $nav_menu_setting_id ) );
525
526                         // Add the menu contents.
527                         $menu_items = (array) wp_get_nav_menu_items( $menu_id );
528
529                         foreach ( array_values( $menu_items ) as $i => $item ) {
530
531                                 // Create a setting for each menu item (which doesn't actually manage data, currently).
532                                 $menu_item_setting_id = 'nav_menu_item[' . $item->ID . ']';
533
534                                 $value = (array) $item;
535                                 $value['nav_menu_term_id'] = $menu_id;
536                                 $this->manager->add_setting( new WP_Customize_Nav_Menu_Item_Setting( $this->manager, $menu_item_setting_id, array(
537                                         'value' => $value,
538                                 ) ) );
539
540                                 // Create a control for each menu item.
541                                 $this->manager->add_control( new WP_Customize_Nav_Menu_Item_Control( $this->manager, $menu_item_setting_id, array(
542                                         'label'    => $item->title,
543                                         'section'  => $section_id,
544                                         'priority' => 10 + $i,
545                                 ) ) );
546                         }
547
548                         // Note: other controls inside of this section get added dynamically in JS via the MenuSection.ready() function.
549                 }
550
551                 // Add the add-new-menu section and controls.
552                 $this->manager->add_section( new WP_Customize_New_Menu_Section( $this->manager, 'add_menu', array(
553                         'title'    => __( 'Add a Menu' ),
554                         'panel'    => 'nav_menus',
555                         'priority' => 999,
556                 ) ) );
557
558                 $this->manager->add_setting( 'new_menu_name', array(
559                         'type'      => 'new_menu',
560                         'default'   => '',
561                         'transport' => 'postMessage',
562                 ) );
563
564                 $this->manager->add_control( 'new_menu_name', array(
565                         'label'       => '',
566                         'section'     => 'add_menu',
567                         'type'        => 'text',
568                         'input_attrs' => array(
569                                 'class'       => 'menu-name-field',
570                                 'placeholder' => __( 'New menu name' ),
571                         ),
572                 ) );
573
574                 $this->manager->add_setting( 'create_new_menu', array(
575                         'type' => 'new_menu',
576                 ) );
577
578                 $this->manager->add_control( new WP_Customize_New_Menu_Control( $this->manager, 'create_new_menu', array(
579                         'section' => 'add_menu',
580                 ) ) );
581         }
582
583         /**
584          * Get the base10 intval.
585          *
586          * This is used as a setting's sanitize_callback; we can't use just plain
587          * intval because the second argument is not what intval() expects.
588          *
589          * @since 4.3.0
590          * @access public
591          *
592          * @param mixed $value Number to convert.
593          * @return int Integer.
594          */
595         public function intval_base10( $value ) {
596                 return intval( $value, 10 );
597         }
598
599         /**
600          * Return an array of all the available item types.
601          *
602          * @since 4.3.0
603          * @access public
604          *
605          * @return array The available menu item types.
606          */
607         public function available_item_types() {
608                 $item_types = array();
609
610                 $post_types = get_post_types( array( 'show_in_nav_menus' => true ), 'objects' );
611                 if ( $post_types ) {
612                         foreach ( $post_types as $slug => $post_type ) {
613                                 $item_types[] = array(
614                                         'title'  => $post_type->labels->singular_name,
615                                         'type'   => 'post_type',
616                                         'object' => $post_type->name,
617                                 );
618                         }
619                 }
620
621                 $taxonomies = get_taxonomies( array( 'show_in_nav_menus' => true ), 'objects' );
622                 if ( $taxonomies ) {
623                         foreach ( $taxonomies as $slug => $taxonomy ) {
624                                 if ( 'post_format' === $taxonomy && ! current_theme_supports( 'post-formats' ) ) {
625                                         continue;
626                                 }
627                                 $item_types[] = array(
628                                         'title'  => $taxonomy->labels->singular_name,
629                                         'type'   => 'taxonomy',
630                                         'object' => $taxonomy->name,
631                                 );
632                         }
633                 }
634
635                 /**
636                  * Filter the available menu item types.
637                  *
638                  * @since 4.3.0
639                  *
640                  * @param array $item_types Custom menu item types.
641                  */
642                 $item_types = apply_filters( 'customize_nav_menu_available_item_types', $item_types );
643
644                 return $item_types;
645         }
646
647         /**
648          * Print the JavaScript templates used to render Menu Customizer components.
649          *
650          * Templates are imported into the JS use wp.template.
651          *
652          * @since 4.3.0
653          * @access public
654          */
655         public function print_templates() {
656                 ?>
657                 <script type="text/html" id="tmpl-available-menu-item">
658                         <li id="menu-item-tpl-{{ data.id }}" class="menu-item-tpl" data-menu-item-id="{{ data.id }}">
659                                 <div class="menu-item-bar">
660                                         <div class="menu-item-handle">
661                                                 <span class="item-type" aria-hidden="true">{{ data.type_label }}</span>
662                                                 <span class="item-title" aria-hidden="true">
663                                                         <span class="menu-item-title<# if ( ! data.title ) { #> no-title<# } #>">{{ data.title || wp.customize.Menus.data.l10n.untitled }}</span>
664                                                 </span>
665                                                 <button type="button" class="not-a-button item-add">
666                                                         <span class="screen-reader-text"><?php
667                                                                 /* translators: 1: Title of a menu item, 2: Type of a menu item */
668                                                                 printf( __( 'Add to menu: %1$s (%2$s)' ), '{{ data.title || wp.customize.Menus.data.l10n.untitled }}', '{{ data.type_label }}' );
669                                                         ?></span>
670                                                 </button>
671                                         </div>
672                                 </div>
673                         </li>
674                 </script>
675
676                 <script type="text/html" id="tmpl-menu-item-reorder-nav">
677                         <div class="menu-item-reorder-nav">
678                                 <?php
679                                 printf(
680                                         '<button type="button" class="menus-move-up">%1$s</button><button type="button" class="menus-move-down">%2$s</button><button type="button" class="menus-move-left">%3$s</button><button type="button" class="menus-move-right">%4$s</button>',
681                                         __( 'Move up' ),
682                                         __( 'Move down' ),
683                                         __( 'Move one level up' ),
684                                         __( 'Move one level down' )
685                                 );
686                                 ?>
687                         </div>
688                 </script>
689         <?php
690         }
691
692         /**
693          * Print the html template used to render the add-menu-item frame.
694          *
695          * @since 4.3.0
696          * @access public
697          */
698         public function available_items_template() {
699                 ?>
700                 <div id="available-menu-items" class="accordion-container">
701                         <div class="customize-section-title">
702                                 <button type="button" class="customize-section-back" tabindex="-1">
703                                         <span class="screen-reader-text"><?php _e( 'Back' ); ?></span>
704                                 </button>
705                                 <h3>
706                                         <span class="customize-action">
707                                                 <?php
708                                                         /* translators: &#9656; is the unicode right-pointing triangle, and %s is the section title in the Customizer */
709                                                         printf( __( 'Customizing &#9656; %s' ), esc_html( $this->manager->get_panel( 'nav_menus' )->title ) );
710                                                 ?>
711                                         </span>
712                                         <?php _e( 'Add Menu Items' ); ?>
713                                 </h3>
714                         </div>
715                         <div id="available-menu-items-search" class="accordion-section cannot-expand">
716                                 <div class="accordion-section-title">
717                                         <label class="screen-reader-text" for="menu-items-search"><?php _e( 'Search Menu Items' ); ?></label>
718                                         <input type="text" id="menu-items-search" placeholder="<?php esc_attr_e( 'Search menu items&hellip;' ) ?>" aria-describedby="menu-items-search-desc" />
719                                         <p class="screen-reader-text" id="menu-items-search-desc"><?php _e( 'The search results will be updated as you type.' ); ?></p>
720                                         <span class="spinner"></span>
721                                         <span class="clear-results"><span class="screen-reader-text"><?php _e( 'Clear Results' ); ?></span></span>
722                                 </div>
723                                 <ul class="accordion-section-content" data-type="search"></ul>
724                         </div>
725                         <div id="new-custom-menu-item" class="accordion-section">
726                                 <h4 class="accordion-section-title" role="presentation">
727                                         <?php _e( 'Custom Links' ); ?>
728                                         <button type="button" class="not-a-button" aria-expanded="false">
729                                                 <span class="screen-reader-text"><?php _e( 'Toggle section: Custom Links' ); ?></span>
730                                                 <span class="toggle-indicator" aria-hidden="true"></span>
731                                         </button>
732                                 </h4>
733                                 <div class="accordion-section-content">
734                                         <input type="hidden" value="custom" id="custom-menu-item-type" name="menu-item[-1][menu-item-type]" />
735                                         <p id="menu-item-url-wrap">
736                                                 <label class="howto" for="custom-menu-item-url">
737                                                         <span><?php _e( 'URL' ); ?></span>
738                                                         <input id="custom-menu-item-url" name="menu-item[-1][menu-item-url]" type="text" class="code menu-item-textbox" value="http://">
739                                                 </label>
740                                         </p>
741                                         <p id="menu-item-name-wrap">
742                                                 <label class="howto" for="custom-menu-item-name">
743                                                         <span><?php _e( 'Link Text' ); ?></span>
744                                                         <input id="custom-menu-item-name" name="menu-item[-1][menu-item-title]" type="text" class="regular-text menu-item-textbox">
745                                                 </label>
746                                         </p>
747                                         <p class="button-controls">
748                                                 <span class="add-to-menu">
749                                                         <input type="submit" class="button-secondary submit-add-to-menu right" value="<?php esc_attr_e( 'Add to Menu' ); ?>" name="add-custom-menu-item" id="custom-menu-item-submit">
750                                                         <span class="spinner"></span>
751                                                 </span>
752                                         </p>
753                                 </div>
754                         </div>
755                         <?php
756                         // Containers for per-post-type item browsing; items added with JS.
757                         foreach ( $this->available_item_types() as $available_item_type ) {
758                                 $id = sprintf( 'available-menu-items-%s-%s', $available_item_type['type'], $available_item_type['object'] );
759                                 ?>
760                                 <div id="<?php echo esc_attr( $id ); ?>" class="accordion-section">
761                                         <h4 class="accordion-section-title" role="presentation">
762                                                 <?php echo esc_html( $available_item_type['title'] ); ?>
763                                                 <span class="spinner"></span>
764                                                 <span class="no-items"><?php _e( 'No items' ); ?></span>
765                                                 <button type="button" class="not-a-button" aria-expanded="false">
766                                                         <span class="screen-reader-text"><?php
767                                                         /* translators: %s: Title of a section with menu items */
768                                                         printf( __( 'Toggle section: %s' ), esc_html( $available_item_type['title'] ) ); ?></span>
769                                                         <span class="toggle-indicator" aria-hidden="true"></span>
770                                                 </button>
771                                         </h4>
772                                         <ul class="accordion-section-content" data-type="<?php echo esc_attr( $available_item_type['type'] ); ?>" data-object="<?php echo esc_attr( $available_item_type['object'] ); ?>"></ul>
773                                 </div>
774                                 <?php
775                         }
776                         ?>
777                 </div><!-- #available-menu-items -->
778         <?php
779         }
780
781         // Start functionality specific to partial-refresh of menu changes in Customizer preview.
782         const RENDER_AJAX_ACTION = 'customize_render_menu_partial';
783         const RENDER_NONCE_POST_KEY = 'render-menu-nonce';
784         const RENDER_QUERY_VAR = 'wp_customize_menu_render';
785
786         /**
787          * The number of wp_nav_menu() calls which have happened in the preview.
788          *
789          * @since 4.3.0
790          * @access public
791          * @var int
792          */
793         public $preview_nav_menu_instance_number = 0;
794
795         /**
796          * Nav menu args used for each instance.
797          *
798          * @since 4.3.0
799          * @access public
800          * @var array
801          */
802         public $preview_nav_menu_instance_args = array();
803
804         /**
805          * Add hooks for the Customizer preview.
806          *
807          * @since 4.3.0
808          * @access public
809          */
810         public function customize_preview_init() {
811                 add_action( 'template_redirect', array( $this, 'render_menu' ) );
812                 add_action( 'wp_enqueue_scripts', array( $this, 'customize_preview_enqueue_deps' ) );
813
814                 if ( ! isset( $_REQUEST[ self::RENDER_QUERY_VAR ] ) ) {
815                         add_filter( 'wp_nav_menu_args', array( $this, 'filter_wp_nav_menu_args' ), 1000 );
816                         add_filter( 'wp_nav_menu', array( $this, 'filter_wp_nav_menu' ), 10, 2 );
817                 }
818         }
819
820         /**
821          * Keep track of the arguments that are being passed to wp_nav_menu().
822          *
823          * @since 4.3.0
824          * @access public
825          *
826          * @see wp_nav_menu()
827          *
828          * @param array $args An array containing wp_nav_menu() arguments.
829          * @return array Arguments.
830          */
831         public function filter_wp_nav_menu_args( $args ) {
832                 $this->preview_nav_menu_instance_number += 1;
833                 $args['instance_number'] = $this->preview_nav_menu_instance_number;
834
835                 $can_partial_refresh = (
836                         ! empty( $args['echo'] )
837                         &&
838                         ( empty( $args['fallback_cb'] ) || is_string( $args['fallback_cb'] ) )
839                         &&
840                         ( empty( $args['walker'] ) || is_string( $args['walker'] ) )
841                         &&
842                         (
843                                 ! empty( $args['theme_location'] )
844                                 ||
845                                 ( ! empty( $args['menu'] ) && ( is_numeric( $args['menu'] ) || is_object( $args['menu'] ) ) )
846                         )
847                 );
848                 $args['can_partial_refresh'] = $can_partial_refresh;
849
850                 $hashed_args = $args;
851
852                 if ( ! $can_partial_refresh ) {
853                         $hashed_args['fallback_cb'] = '';
854                         $hashed_args['walker'] = '';
855                 }
856
857                 // Replace object menu arg with a term_id menu arg, as this exports better to JS and is easier to compare hashes.
858                 if ( ! empty( $hashed_args['menu'] ) && is_object( $hashed_args['menu'] ) ) {
859                         $hashed_args['menu'] = $hashed_args['menu']->term_id;
860                 }
861
862                 ksort( $hashed_args );
863                 $hashed_args['args_hash'] = $this->hash_nav_menu_args( $hashed_args );
864
865                 $this->preview_nav_menu_instance_args[ $this->preview_nav_menu_instance_number ] = $hashed_args;
866                 return $args;
867         }
868
869         /**
870          * Prepare wp_nav_menu() calls for partial refresh. Wraps output in container for refreshing.
871          *
872          * @since 4.3.0
873          * @access public
874          *
875          * @see wp_nav_menu()
876          *
877          * @param string $nav_menu_content The HTML content for the navigation menu.
878          * @param object $args             An object containing wp_nav_menu() arguments.
879          * @return null
880          */
881         public function filter_wp_nav_menu( $nav_menu_content, $args ) {
882                 if ( ! empty( $args->can_partial_refresh ) && ! empty( $args->instance_number ) ) {
883                         $nav_menu_content = preg_replace(
884                                 '/(?<=class=")/',
885                                 sprintf( 'partial-refreshable-nav-menu partial-refreshable-nav-menu-%1$d ', $args->instance_number ),
886                                 $nav_menu_content,
887                                 1 // Only update the class on the first element found, the menu container.
888                         );
889                 }
890                 return $nav_menu_content;
891         }
892
893         /**
894          * Hash (hmac) the arguments with the nonce and secret auth key to ensure they
895          * are not tampered with when submitted in the Ajax request.
896          *
897          * @since 4.3.0
898          * @access public
899          *
900          * @param array $args The arguments to hash.
901          * @return string
902          */
903         public function hash_nav_menu_args( $args ) {
904                 return wp_hash( wp_create_nonce( self::RENDER_AJAX_ACTION ) . serialize( $args ) );
905         }
906
907         /**
908          * Enqueue scripts for the Customizer preview.
909          *
910          * @since 4.3.0
911          * @access public
912          */
913         public function customize_preview_enqueue_deps() {
914                 wp_enqueue_script( 'customize-preview-nav-menus' );
915                 wp_enqueue_style( 'customize-preview' );
916
917                 add_action( 'wp_print_footer_scripts', array( $this, 'export_preview_data' ) );
918         }
919
920         /**
921          * Export data from PHP to JS.
922          *
923          * @since 4.3.0
924          * @access public
925          */
926         public function export_preview_data() {
927
928                 // Why not wp_localize_script? Because we're not localizing, and it forces values into strings.
929                 $exports = array(
930                         'renderQueryVar'        => self::RENDER_QUERY_VAR,
931                         'renderNonceValue'      => wp_create_nonce( self::RENDER_AJAX_ACTION ),
932                         'renderNoncePostKey'    => self::RENDER_NONCE_POST_KEY,
933                         'requestUri'            => '/',
934                         'theme'                 => array(
935                                 'stylesheet' => $this->manager->get_stylesheet(),
936                                 'active'     => $this->manager->is_theme_active(),
937                         ),
938                         'previewCustomizeNonce' => wp_create_nonce( 'preview-customize_' . $this->manager->get_stylesheet() ),
939                         'navMenuInstanceArgs'   => $this->preview_nav_menu_instance_args,
940                 );
941
942                 if ( ! empty( $_SERVER['REQUEST_URI'] ) ) {
943                         $exports['requestUri'] = esc_url_raw( home_url( wp_unslash( $_SERVER['REQUEST_URI'] ) ) );
944                 }
945
946                 printf( '<script>var _wpCustomizePreviewNavMenusExports = %s;</script>', wp_json_encode( $exports ) );
947         }
948
949         /**
950          * Render a specific menu via wp_nav_menu() using the supplied arguments.
951          *
952          * @since 4.3.0
953          * @access public
954          *
955          * @see wp_nav_menu()
956          */
957         public function render_menu() {
958                 if ( empty( $_POST[ self::RENDER_QUERY_VAR ] ) ) {
959                         return;
960                 }
961
962                 $this->manager->remove_preview_signature();
963
964                 if ( empty( $_POST[ self::RENDER_NONCE_POST_KEY ] ) ) {
965                         wp_send_json_error( 'missing_nonce_param' );
966                 }
967
968                 if ( ! is_customize_preview() ) {
969                         wp_send_json_error( 'expected_customize_preview' );
970                 }
971
972                 if ( ! check_ajax_referer( self::RENDER_AJAX_ACTION, self::RENDER_NONCE_POST_KEY, false ) ) {
973                         wp_send_json_error( 'nonce_check_fail' );
974                 }
975
976                 if ( ! current_user_can( 'edit_theme_options' ) ) {
977                         wp_send_json_error( 'unauthorized' );
978                 }
979
980                 if ( ! isset( $_POST['wp_nav_menu_args'] ) ) {
981                         wp_send_json_error( 'missing_param' );
982                 }
983
984                 if ( ! isset( $_POST['wp_nav_menu_args_hash'] ) ) {
985                         wp_send_json_error( 'missing_param' );
986                 }
987
988                 $wp_nav_menu_args = json_decode( wp_unslash( $_POST['wp_nav_menu_args'] ), true );
989                 if ( ! is_array( $wp_nav_menu_args ) ) {
990                         wp_send_json_error( 'wp_nav_menu_args_not_array' );
991                 }
992
993                 $wp_nav_menu_args_hash = sanitize_text_field( wp_unslash( $_POST['wp_nav_menu_args_hash'] ) );
994                 if ( ! hash_equals( $this->hash_nav_menu_args( $wp_nav_menu_args ), $wp_nav_menu_args_hash ) ) {
995                         wp_send_json_error( 'wp_nav_menu_args_hash_mismatch' );
996                 }
997
998                 $wp_nav_menu_args['echo'] = false;
999                 wp_send_json_success( wp_nav_menu( $wp_nav_menu_args ) );
1000         }
1001 }