| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
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 #[AllowDynamicProperties] 20 final class WP_Customize_Nav_Menus { 21 22 /** 23 * WP_Customize_Manager instance. 24 * 25 * @since 4.3.0 26 * @var WP_Customize_Manager 27 */ 28 public $manager; 29 30 /** 31 * Original nav menu locations before the theme was switched. 32 * 33 * @since 4.9.0 34 * @var array 35 */ 36 protected $original_nav_menu_locations; 37 38 /** 39 * Constructor. 40 * 41 * @since 4.3.0 42 * 43 * @param WP_Customize_Manager $manager Customizer bootstrap instance. 44 */ 45 public function __construct( $manager ) { 46 $this->manager = $manager; 47 $this->original_nav_menu_locations = get_nav_menu_locations(); 48 49 // See https://github.com/xwp/wp-customize-snapshots/blob/962586659688a5b1fd9ae93618b7ce2d4e7a421c/php/class-customize-snapshot-manager.php#L469-L499 50 add_action( 'customize_register', array( $this, 'customize_register' ), 11 ); 51 add_filter( 'customize_dynamic_setting_args', array( $this, 'filter_dynamic_setting_args' ), 10, 2 ); 52 add_filter( 'customize_dynamic_setting_class', array( $this, 'filter_dynamic_setting_class' ), 10, 3 ); 53 add_action( 'customize_save_nav_menus_created_posts', array( $this, 'save_nav_menus_created_posts' ) ); 54 55 // Skip remaining hooks when the user can't manage nav menus anyway. 56 if ( ! current_user_can( 'edit_theme_options' ) ) { 57 return; 58 } 59 60 add_filter( 'customize_refresh_nonces', array( $this, 'filter_nonces' ) ); 61 add_action( 'wp_ajax_load-available-menu-items-customizer', array( $this, 'ajax_load_available_items' ) ); 62 add_action( 'wp_ajax_search-available-menu-items-customizer', array( $this, 'ajax_search_available_items' ) ); 63 add_action( 'wp_ajax_customize-nav-menus-insert-auto-draft', array( $this, 'ajax_insert_auto_draft_post' ) ); 64 add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); 65 add_action( 'customize_controls_print_footer_scripts', array( $this, 'print_templates' ) ); 66 add_action( 'customize_controls_print_footer_scripts', array( $this, 'available_items_template' ) ); 67 add_action( 'customize_preview_init', array( $this, 'customize_preview_init' ) ); 68 add_action( 'customize_preview_init', array( $this, 'make_auto_draft_status_previewable' ) ); 69 70 // Selective Refresh partials. 71 add_filter( 'customize_dynamic_partial_args', array( $this, 'customize_dynamic_partial_args' ), 10, 2 ); 72 } 73 74 /** 75 * Adds a nonce for customizing menus. 76 * 77 * @since 4.5.0 78 * 79 * @param string[] $nonces Array of nonces. 80 * @return string[] Modified array of nonces. 81 */ 82 public function filter_nonces( $nonces ) { 83 $nonces['customize-menus'] = wp_create_nonce( 'customize-menus' ); 84 return $nonces; 85 } 86 87 /** 88 * Ajax handler for loading available menu items. 89 * 90 * @since 4.3.0 91 * 92 * @return never 93 */ 94 public function ajax_load_available_items() { 95 check_ajax_referer( 'customize-menus', 'customize-menus-nonce' ); 96 97 if ( ! current_user_can( 'edit_theme_options' ) ) { 98 wp_die( -1 ); 99 } 100 101 $all_items = array(); 102 $item_types = array(); 103 if ( isset( $_POST['item_types'] ) && is_array( $_POST['item_types'] ) ) { 104 $item_types = wp_unslash( $_POST['item_types'] ); 105 } elseif ( isset( $_POST['type'] ) && isset( $_POST['object'] ) ) { // Back compat. 106 $item_types[] = array( 107 'type' => wp_unslash( $_POST['type'] ), 108 'object' => wp_unslash( $_POST['object'] ), 109 'page' => empty( $_POST['page'] ) ? 0 : absint( $_POST['page'] ), 110 ); 111 } else { 112 wp_send_json_error( 'nav_menus_missing_type_or_object_parameter' ); 113 } 114 115 foreach ( $item_types as $item_type ) { 116 if ( empty( $item_type['type'] ) || empty( $item_type['object'] ) ) { 117 wp_send_json_error( 'nav_menus_missing_type_or_object_parameter' ); 118 } 119 $type = sanitize_key( $item_type['type'] ); 120 $object = sanitize_key( $item_type['object'] ); 121 $page = empty( $item_type['page'] ) ? 0 : absint( $item_type['page'] ); 122 $items = $this->load_available_items_query( $type, $object, $page ); 123 if ( is_wp_error( $items ) ) { 124 wp_send_json_error( $items->get_error_code() ); 125 } 126 $all_items[ $item_type['type'] . ':' . $item_type['object'] ] = $items; 127 } 128 129 wp_send_json_success( array( 'items' => $all_items ) ); 130 } 131 132 /** 133 * Performs the post_type and taxonomy queries for loading available menu items. 134 * 135 * @since 4.3.0 136 * 137 * @param string $object_type Optional. Accepts any custom object type and has built-in support for 138 * 'post_type' and 'taxonomy'. Default is 'post_type'. 139 * @param string $object_name Optional. Accepts any registered taxonomy or post type name. Default is 'page'. 140 * @param int $page Optional. The page number used to generate the query offset. Default is '0'. 141 * @return array|WP_Error An array of menu items on success, a WP_Error object on failure. 142 */ 143 public function load_available_items_query( $object_type = 'post_type', $object_name = 'page', $page = 0 ) { 144 $items = array(); 145 146 if ( 'post_type' === $object_type ) { 147 $post_type = get_post_type_object( $object_name ); 148 if ( ! $post_type ) { 149 return new WP_Error( 'nav_menus_invalid_post_type' ); 150 } 151 152 /* 153 * If we're dealing with pages, let's prioritize the Front Page, 154 * Posts Page and Privacy Policy Page at the top of the list. 155 */ 156 $important_pages = array(); 157 $suppress_page_ids = array(); 158 if ( 0 === $page && 'page' === $object_name ) { 159 // Insert Front Page or custom "Home" link. 160 $front_page = 'page' === get_option( 'show_on_front' ) ? (int) get_option( 'page_on_front' ) : 0; 161 if ( ! empty( $front_page ) ) { 162 $front_page_obj = get_post( $front_page ); 163 $important_pages[] = $front_page_obj; 164 $suppress_page_ids[] = $front_page_obj->ID; 165 } else { 166 // Add "Home" link. Treat as a page, but switch to custom on add. 167 $items[] = array( 168 'id' => 'home', 169 'title' => _x( 'Home', 'nav menu home label' ), 170 'type' => 'custom', 171 'type_label' => __( 'Custom Link' ), 172 'object' => '', 173 'url' => home_url(), 174 ); 175 } 176 177 // Insert Posts Page. 178 $posts_page = 'page' === get_option( 'show_on_front' ) ? (int) get_option( 'page_for_posts' ) : 0; 179 if ( ! empty( $posts_page ) ) { 180 $posts_page_obj = get_post( $posts_page ); 181 $important_pages[] = $posts_page_obj; 182 $suppress_page_ids[] = $posts_page_obj->ID; 183 } 184 185 // Insert Privacy Policy Page. 186 $privacy_policy_page_id = (int) get_option( 'wp_page_for_privacy_policy' ); 187 if ( ! empty( $privacy_policy_page_id ) ) { 188 $privacy_policy_page = get_post( $privacy_policy_page_id ); 189 if ( $privacy_policy_page instanceof WP_Post && 'publish' === $privacy_policy_page->post_status ) { 190 $important_pages[] = $privacy_policy_page; 191 $suppress_page_ids[] = $privacy_policy_page->ID; 192 } 193 } 194 } elseif ( 'post' !== $object_name && 0 === $page && $post_type->has_archive ) { 195 // Add a post type archive link. 196 $title = $post_type->labels->archives; 197 $items[] = array( 198 'id' => $object_name . '-archive', 199 'title' => $title, 200 'original_title' => $title, 201 'type' => 'post_type_archive', 202 'type_label' => __( 'Post Type Archive' ), 203 'object' => $object_name, 204 'url' => get_post_type_archive_link( $object_name ), 205 ); 206 } 207 208 // Prepend posts with nav_menus_created_posts on first page. 209 $posts = array(); 210 if ( 0 === $page && $this->manager->get_setting( 'nav_menus_created_posts' ) ) { 211 foreach ( $this->manager->get_setting( 'nav_menus_created_posts' )->value() as $post_id ) { 212 $auto_draft_post = get_post( $post_id ); 213 if ( $post_type->name === $auto_draft_post->post_type ) { 214 $posts[] = $auto_draft_post; 215 } 216 } 217 } 218 219 $args = array( 220 'numberposts' => 10, 221 'offset' => 10 * $page, 222 'orderby' => 'date', 223 'order' => 'DESC', 224 'post_type' => $object_name, 225 ); 226 227 // Add suppression array to arguments for get_posts. 228 if ( ! empty( $suppress_page_ids ) ) { 229 $args['post__not_in'] = $suppress_page_ids; 230 } 231 232 $posts = array_merge( 233 $posts, 234 $important_pages, 235 get_posts( $args ) 236 ); 237 238 foreach ( $posts as $post ) { 239 $post_title = $post->post_title; 240 if ( '' === $post_title ) { 241 /* translators: %d: ID of a post. */ 242 $post_title = sprintf( __( '#%d (no title)' ), $post->ID ); 243 } 244 245 $post_type_label = get_post_type_object( $post->post_type )->labels->singular_name; 246 $post_states = get_post_states( $post ); 247 if ( ! empty( $post_states ) ) { 248 $post_type_label = implode( ',', $post_states ); 249 } 250 251 $title = html_entity_decode( $post_title, ENT_QUOTES, get_bloginfo( 'charset' ) ); 252 $items[] = array( 253 'id' => "post-{$post->ID}", 254 'title' => $title, 255 'original_title' => $title, 256 'type' => 'post_type', 257 'type_label' => $post_type_label, 258 'object' => $post->post_type, 259 'object_id' => (int) $post->ID, 260 'url' => get_permalink( (int) $post->ID ), 261 ); 262 } 263 } elseif ( 'taxonomy' === $object_type ) { 264 $terms = get_terms( 265 array( 266 'taxonomy' => $object_name, 267 'child_of' => 0, 268 'exclude' => '', 269 'hide_empty' => false, 270 'hierarchical' => 1, 271 'include' => '', 272 'number' => 10, 273 'offset' => 10 * $page, 274 'order' => 'DESC', 275 'orderby' => 'count', 276 'pad_counts' => false, 277 ) 278 ); 279 280 if ( is_wp_error( $terms ) ) { 281 return $terms; 282 } 283 284 foreach ( $terms as $term ) { 285 $title = html_entity_decode( $term->name, ENT_QUOTES, get_bloginfo( 'charset' ) ); 286 $items[] = array( 287 'id' => "term-{$term->term_id}", 288 'title' => $title, 289 'original_title' => $title, 290 'type' => 'taxonomy', 291 'type_label' => get_taxonomy( $term->taxonomy )->labels->singular_name, 292 'object' => $term->taxonomy, 293 'object_id' => (int) $term->term_id, 294 'url' => get_term_link( (int) $term->term_id, $term->taxonomy ), 295 ); 296 } 297 } 298 299 /** 300 * Filters the available menu items. 301 * 302 * @since 4.3.0 303 * 304 * @param array $items The array of menu items. 305 * @param string $object_type The object type. 306 * @param string $object_name The object name. 307 * @param int $page The current page number. 308 */ 309 $items = apply_filters( 'customize_nav_menu_available_items', $items, $object_type, $object_name, $page ); 310 311 return $items; 312 } 313 314 /** 315 * Ajax handler for searching available menu items. 316 * 317 * @since 4.3.0 318 * 319 * @return never 320 */ 321 public function ajax_search_available_items() { 322 check_ajax_referer( 'customize-menus', 'customize-menus-nonce' ); 323 324 if ( ! current_user_can( 'edit_theme_options' ) ) { 325 wp_die( -1 ); 326 } 327 328 if ( empty( $_POST['search'] ) ) { 329 wp_send_json_error( 'nav_menus_missing_search_parameter' ); 330 } 331 332 $p = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 0; 333 if ( $p < 1 ) { 334 $p = 1; 335 } 336 337 $s = sanitize_text_field( wp_unslash( $_POST['search'] ) ); 338 $items = $this->search_available_items_query( 339 array( 340 'pagenum' => $p, 341 's' => $s, 342 ) 343 ); 344 345 if ( empty( $items ) ) { 346 wp_send_json_error( array( 'message' => __( 'No results found.' ) ) ); 347 } else { 348 wp_send_json_success( array( 'items' => $items ) ); 349 } 350 } 351 352 /** 353 * Performs post queries for available-item searching. 354 * 355 * Based on WP_Editor::wp_link_query(). 356 * 357 * @since 4.3.0 358 * 359 * @param array $args Optional. Accepts 'pagenum' and 's' (search) arguments. 360 * @return array Menu items. 361 */ 362 public function search_available_items_query( $args = array() ) { 363 $items = array(); 364 365 $post_type_objects = get_post_types( array( 'show_in_nav_menus' => true ), 'objects' ); 366 $query = array( 367 'post_type' => array_keys( $post_type_objects ), 368 'suppress_filters' => true, 369 'update_post_term_cache' => false, 370 'update_post_meta_cache' => false, 371 'post_status' => 'publish', 372 'posts_per_page' => 20, 373 ); 374 375 $args['pagenum'] = isset( $args['pagenum'] ) ? absint( $args['pagenum'] ) : 1; 376 $query['offset'] = $args['pagenum'] > 1 ? $query['posts_per_page'] * ( $args['pagenum'] - 1 ) : 0; 377 378 if ( isset( $args['s'] ) ) { 379 $query['s'] = $args['s']; 380 } 381 382 $posts = array(); 383 384 // Prepend list of posts with nav_menus_created_posts search results on first page. 385 $nav_menus_created_posts_setting = $this->manager->get_setting( 'nav_menus_created_posts' ); 386 if ( 1 === $args['pagenum'] && $nav_menus_created_posts_setting && count( $nav_menus_created_posts_setting->value() ) > 0 ) { 387 $stub_post_query = new WP_Query( 388 array_merge( 389 $query, 390 array( 391 'post_status' => 'auto-draft', 392 'post__in' => $nav_menus_created_posts_setting->value(), 393 'posts_per_page' => -1, 394 ) 395 ) 396 ); 397 $posts = array_merge( $posts, $stub_post_query->posts ); 398 } 399 400 // Query posts. 401 $get_posts = new WP_Query( $query ); 402 $posts = array_merge( $posts, $get_posts->posts ); 403 404 // Create items for posts. 405 foreach ( $posts as $post ) { 406 $post_title = $post->post_title; 407 if ( '' === $post_title ) { 408 /* translators: %d: ID of a post. */ 409 $post_title = sprintf( __( '#%d (no title)' ), $post->ID ); 410 } 411 412 $post_type_label = $post_type_objects[ $post->post_type ]->labels->singular_name; 413 $post_states = get_post_states( $post ); 414 if ( ! empty( $post_states ) ) { 415 $post_type_label = implode( ',', $post_states ); 416 } 417 418 $items[] = array( 419 'id' => 'post-' . $post->ID, 420 'title' => html_entity_decode( $post_title, ENT_QUOTES, get_bloginfo( 'charset' ) ), 421 'type' => 'post_type', 422 'type_label' => $post_type_label, 423 'object' => $post->post_type, 424 'object_id' => (int) $post->ID, 425 'url' => get_permalink( (int) $post->ID ), 426 ); 427 } 428 429 // Query taxonomy terms. 430 $taxonomies = get_taxonomies( array( 'show_in_nav_menus' => true ), 'names' ); 431 $terms = get_terms( 432 array( 433 'taxonomies' => $taxonomies, 434 'name__like' => $args['s'], 435 'number' => 20, 436 'hide_empty' => false, 437 'offset' => 20 * ( $args['pagenum'] - 1 ), 438 ) 439 ); 440 441 // Check if any taxonomies were found. 442 if ( ! empty( $terms ) ) { 443 foreach ( $terms as $term ) { 444 $items[] = array( 445 'id' => 'term-' . $term->term_id, 446 'title' => html_entity_decode( $term->name, ENT_QUOTES, get_bloginfo( 'charset' ) ), 447 'type' => 'taxonomy', 448 'type_label' => get_taxonomy( $term->taxonomy )->labels->singular_name, 449 'object' => $term->taxonomy, 450 'object_id' => (int) $term->term_id, 451 'url' => get_term_link( (int) $term->term_id, $term->taxonomy ), 452 ); 453 } 454 } 455 456 // Add "Home" link if search term matches. Treat as a page, but switch to custom on add. 457 if ( isset( $args['s'] ) ) { 458 // Only insert custom "Home" link if there's no Front Page 459 $front_page = 'page' === get_option( 'show_on_front' ) ? (int) get_option( 'page_on_front' ) : 0; 460 if ( empty( $front_page ) ) { 461 $title = _x( 'Home', 'nav menu home label' ); 462 $matches = function_exists( 'mb_stripos' ) ? false !== mb_stripos( $title, $args['s'] ) : false !== stripos( $title, $args['s'] ); 463 if ( $matches ) { 464 $items[] = array( 465 'id' => 'home', 466 'title' => $title, 467 'type' => 'custom', 468 'type_label' => __( 'Custom Link' ), 469 'object' => '', 470 'url' => home_url(), 471 ); 472 } 473 } 474 } 475 476 /** 477 * Filters the available menu items during a search request. 478 * 479 * @since 4.5.0 480 * 481 * @param array $items The array of menu items. 482 * @param array $args Includes 'pagenum' and 's' (search) arguments. 483 */ 484 $items = apply_filters( 'customize_nav_menu_searched_items', $items, $args ); 485 486 return $items; 487 } 488 489 /** 490 * Enqueues scripts and styles for Customizer pane. 491 * 492 * @since 4.3.0 493 */ 494 public function enqueue_scripts() { 495 wp_enqueue_style( 'customize-nav-menus' ); 496 wp_enqueue_script( 'customize-nav-menus' ); 497 498 $temp_nav_menu_setting = new WP_Customize_Nav_Menu_Setting( $this->manager, 'nav_menu[-1]' ); 499 $temp_nav_menu_item_setting = new WP_Customize_Nav_Menu_Item_Setting( $this->manager, 'nav_menu_item[-1]' ); 500 501 $num_locations = count( get_registered_nav_menus() ); 502 503 if ( 1 === $num_locations ) { 504 $locations_description = __( 'Your theme can display menus in one location.' ); 505 } else { 506 /* translators: %s: Number of menu locations. */ 507 $locations_description = sprintf( _n( 'Your theme can display menus in %s location.', 'Your theme can display menus in %s locations.', $num_locations ), number_format_i18n( $num_locations ) ); 508 } 509 510 // Pass data to JS. 511 $settings = array( 512 'allMenus' => wp_get_nav_menus(), 513 'itemTypes' => $this->available_item_types(), 514 'l10n' => array( 515 'untitled' => _x( '(no label)', 'missing menu item navigation label' ), 516 'unnamed' => _x( '(unnamed)', 'Missing menu name.' ), 517 'custom_label' => __( 'Custom Link' ), 518 'page_label' => get_post_type_object( 'page' )->labels->singular_name, 519 /* translators: %s: Menu location. */ 520 'menuLocation' => _x( '(Currently set to: %s)', 'menu' ), 521 'locationsTitle' => 1 === $num_locations ? __( 'Menu Location' ) : __( 'Menu Locations' ), 522 'locationsDescription' => $locations_description, 523 'menuNameLabel' => __( 'Menu Name' ), 524 'newMenuNameDescription' => __( 'If your theme has multiple menus, giving them clear names will help you manage them.' ), 525 'itemAdded' => __( 'Menu item added' ), 526 'itemDeleted' => __( 'Menu item deleted' ), 527 'menuAdded' => __( 'Menu created' ), 528 'menuDeleted' => __( 'Menu deleted' ), 529 'movedUp' => __( 'Menu item moved up' ), 530 'movedDown' => __( 'Menu item moved down' ), 531 'movedLeft' => __( 'Menu item moved out of submenu' ), 532 'movedRight' => __( 'Menu item is now a sub-item' ), 533 /* translators: ▸ is the unicode right-pointing triangle. %s: Section title in the Customizer. */ 534 'customizingMenus' => sprintf( __( 'Customizing ▸ %s' ), esc_html( $this->manager->get_panel( 'nav_menus' )->title ) ), 535 /* translators: %s: Title of an invalid menu item. */ 536 'invalidTitleTpl' => __( '%s (Invalid)' ), 537 /* translators: %s: Title of a menu item in draft status. */ 538 'pendingTitleTpl' => __( '%s (Pending)' ), 539 /* translators: %d: Number of menu items found. */ 540 'itemsFound' => __( 'Number of items found: %d' ), 541 /* translators: %d: Number of additional menu items found. */ 542 'itemsFoundMore' => __( 'Additional items found: %d' ), 543 'itemsLoadingMore' => __( 'Loading more results... please wait.' ), 544 'reorderModeOn' => __( 'Reorder mode enabled' ), 545 'reorderModeOff' => __( 'Reorder mode closed' ), 546 'reorderLabelOn' => esc_attr__( 'Reorder menu items' ), 547 'reorderLabelOff' => esc_attr__( 'Close reorder mode' ), 548 ), 549 'settingTransport' => 'postMessage', 550 'phpIntMax' => PHP_INT_MAX, 551 'defaultSettingValues' => array( 552 'nav_menu' => $temp_nav_menu_setting->default, 553 'nav_menu_item' => $temp_nav_menu_item_setting->default, 554 ), 555 'locationSlugMappedToName' => get_registered_nav_menus(), 556 ); 557 558 $data = sprintf( 'var _wpCustomizeNavMenusSettings = %s;', wp_json_encode( $settings, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ) ); 559 wp_scripts()->add_data( 'customize-nav-menus', 'data', $data ); 560 561 // This is copied from nav-menus.php, and it has an unfortunate object name of `menus`. 562 $nav_menus_l10n = array( 563 'oneThemeLocationNoMenus' => null, 564 'moveUp' => __( 'Move up one' ), 565 'moveDown' => __( 'Move down one' ), 566 'moveToTop' => __( 'Move to the top' ), 567 /* translators: %s: Previous item name. */ 568 'moveUnder' => __( 'Move under %s' ), 569 /* translators: %s: Previous item name. */ 570 'moveOutFrom' => __( 'Move out from under %s' ), 571 /* translators: %s: Previous item name. */ 572 'under' => __( 'Under %s' ), 573 /* translators: %s: Previous item name. */ 574 'outFrom' => __( 'Out from under %s' ), 575 /* translators: 1: Item name, 2: Item type, 3: Item index, 4: Total items. */ 576 'menuFocus' => __( 'Edit %1$s (%2$s, %3$d of %4$d)' ), 577 /* translators: 1: Item name, 2: Item type, 3: Item index, 4: Total items, 5: Item parent. */ 578 'subMenuFocus' => __( 'Edit %1$s (%2$s, sub-item %3$d of %4$d under %5$s)' ), 579 /* translators: 1: Item name, 2: Item type, 3: Item index, 4: Total items, 5: Item parent, 6: Item depth. */ 580 'subMenuMoreDepthFocus' => __( 'Edit %1$s (%2$s, sub-item %3$d of %4$d under %5$s, level %6$d)' ), 581 ); 582 wp_localize_script( 'nav-menu', 'menus', $nav_menus_l10n ); 583 } 584 585 /** 586 * Filters a dynamic setting's constructor args. 587 * 588 * For a dynamic setting to be registered, this filter must be employed 589 * to override the default false value with an array of args to pass to 590 * the WP_Customize_Setting constructor. 591 * 592 * @since 4.3.0 593 * 594 * @param false|array $setting_args The arguments to the WP_Customize_Setting constructor. 595 * @param string $setting_id ID for dynamic setting, usually coming from `$_POST['customized']`. 596 * @return array|false 597 */ 598 public function filter_dynamic_setting_args( $setting_args, $setting_id ) { 599 if ( preg_match( WP_Customize_Nav_Menu_Setting::ID_PATTERN, $setting_id ) ) { 600 $setting_args = array( 601 'type' => WP_Customize_Nav_Menu_Setting::TYPE, 602 'transport' => 'postMessage', 603 ); 604 } elseif ( preg_match( WP_Customize_Nav_Menu_Item_Setting::ID_PATTERN, $setting_id ) ) { 605 $setting_args = array( 606 'type' => WP_Customize_Nav_Menu_Item_Setting::TYPE, 607 'transport' => 'postMessage', 608 ); 609 } 610 return $setting_args; 611 } 612 613 /** 614 * Allows non-statically created settings to be constructed with custom WP_Customize_Setting subclass. 615 * 616 * @since 4.3.0 617 * 618 * @param string $setting_class WP_Customize_Setting or a subclass. 619 * @param string $setting_id ID for dynamic setting, usually coming from `$_POST['customized']`. 620 * @param array $setting_args WP_Customize_Setting or a subclass. 621 * @return string 622 */ 623 public function filter_dynamic_setting_class( $setting_class, $setting_id, $setting_args ) { 624 unset( $setting_id ); 625 626 if ( ! empty( $setting_args['type'] ) && WP_Customize_Nav_Menu_Setting::TYPE === $setting_args['type'] ) { 627 $setting_class = 'WP_Customize_Nav_Menu_Setting'; 628 } elseif ( ! empty( $setting_args['type'] ) && WP_Customize_Nav_Menu_Item_Setting::TYPE === $setting_args['type'] ) { 629 $setting_class = 'WP_Customize_Nav_Menu_Item_Setting'; 630 } 631 return $setting_class; 632 } 633 634 /** 635 * Adds the customizer settings and controls. 636 * 637 * @since 4.3.0 638 */ 639 public function customize_register() { 640 $changeset = $this->manager->unsanitized_post_values(); 641 642 // Preview settings for nav menus early so that the sections and controls will be added properly. 643 $nav_menus_setting_ids = array(); 644 foreach ( array_keys( $changeset ) as $setting_id ) { 645 if ( preg_match( '/^(nav_menu_locations|nav_menu|nav_menu_item)\[/', $setting_id ) ) { 646 $nav_menus_setting_ids[] = $setting_id; 647 } 648 } 649 $settings = $this->manager->add_dynamic_settings( $nav_menus_setting_ids ); 650 if ( $this->manager->settings_previewed() ) { 651 foreach ( $settings as $setting ) { 652 $setting->preview(); 653 } 654 } 655 656 // Require JS-rendered control types. 657 $this->manager->register_panel_type( 'WP_Customize_Nav_Menus_Panel' ); 658 $this->manager->register_control_type( 'WP_Customize_Nav_Menu_Control' ); 659 $this->manager->register_control_type( 'WP_Customize_Nav_Menu_Name_Control' ); 660 $this->manager->register_control_type( 'WP_Customize_Nav_Menu_Locations_Control' ); 661 $this->manager->register_control_type( 'WP_Customize_Nav_Menu_Auto_Add_Control' ); 662 $this->manager->register_control_type( 'WP_Customize_Nav_Menu_Item_Control' ); 663 664 // Create a panel for Menus. 665 $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>'; 666 if ( current_theme_supports( 'widgets' ) ) { 667 $description .= '<p>' . sprintf( 668 /* translators: %s: URL to the Widgets panel of the Customizer. */ 669 __( 'Menus can be displayed in locations defined by your theme or in <a href="%s">widget areas</a> by adding a “Navigation Menu” widget.' ), 670 "javascript:wp.customize.panel( 'widgets' ).focus();" 671 ) . '</p>'; 672 } else { 673 $description .= '<p>' . __( 'Menus can be displayed in locations defined by your theme.' ) . '</p>'; 674 } 675 676 /* 677 * Once multiple theme supports are allowed in WP_Customize_Panel, 678 * this panel can be restricted to themes that support menus or widgets. 679 */ 680 $this->manager->add_panel( 681 new WP_Customize_Nav_Menus_Panel( 682 $this->manager, 683 'nav_menus', 684 array( 685 'title' => __( 'Menus' ), 686 'description' => $description, 687 'priority' => 100, 688 ) 689 ) 690 ); 691 $menus = wp_get_nav_menus(); 692 693 // Menu locations. 694 $locations = get_registered_nav_menus(); 695 $num_locations = count( $locations ); 696 697 if ( 1 === $num_locations ) { 698 $description = '<p>' . __( 'Your theme can display menus in one location. Select which menu you would like to use.' ) . '</p>'; 699 } else { 700 /* translators: %s: Number of menu locations. */ 701 $description = '<p>' . sprintf( _n( 'Your theme can display menus in %s location. Select which menu you would like to use.', 'Your theme can display menus in %s locations. Select which menu appears in each location.', $num_locations ), number_format_i18n( $num_locations ) ) . '</p>'; 702 } 703 704 if ( current_theme_supports( 'widgets' ) ) { 705 /* translators: URL to the Widgets panel of the Customizer. */ 706 $description .= '<p>' . sprintf( __( 'If your theme has widget areas, you can also add menus there. Visit the <a href="%s">Widgets panel</a> and add a “Navigation Menu widget” to display a menu in a sidebar or footer.' ), "javascript:wp.customize.panel( 'widgets' ).focus();" ) . '</p>'; 707 } 708 709 $this->manager->add_section( 710 'menu_locations', 711 array( 712 'title' => 1 === $num_locations ? _x( 'View Location', 'menu locations' ) : _x( 'View All Locations', 'menu locations' ), 713 'panel' => 'nav_menus', 714 'priority' => 30, 715 'description' => $description, 716 ) 717 ); 718 719 $choices = array( '0' => __( '— Select —' ) ); 720 foreach ( $menus as $menu ) { 721 $choices[ $menu->term_id ] = wp_html_excerpt( $menu->name, 40, '…' ); 722 } 723 724 // Attempt to re-map the nav menu location assignments when previewing a theme switch. 725 $mapped_nav_menu_locations = array(); 726 if ( ! $this->manager->is_theme_active() ) { 727 $theme_mods = get_option( 'theme_mods_' . $this->manager->get_stylesheet(), array() ); 728 729 // If there is no data from a previous activation, start fresh. 730 if ( empty( $theme_mods['nav_menu_locations'] ) ) { 731 $theme_mods['nav_menu_locations'] = array(); 732 } 733 734 $mapped_nav_menu_locations = wp_map_nav_menu_locations( $theme_mods['nav_menu_locations'], $this->original_nav_menu_locations ); 735 } 736 737 foreach ( $locations as $location => $description ) { 738 $setting_id = "nav_menu_locations[{$location}]"; 739 740 $setting = $this->manager->get_setting( $setting_id ); 741 if ( $setting ) { 742 $setting->transport = 'postMessage'; 743 remove_filter( "customize_sanitize_{$setting_id}", 'absint' ); 744 add_filter( "customize_sanitize_{$setting_id}", array( $this, 'intval_base10' ) ); 745 } else { 746 $this->manager->add_setting( 747 $setting_id, 748 array( 749 'sanitize_callback' => array( $this, 'intval_base10' ), 750 'theme_supports' => 'menus', 751 'type' => 'theme_mod', 752 'transport' => 'postMessage', 753 'default' => 0, 754 ) 755 ); 756 } 757 758 // Override the assigned nav menu location if mapped during previewed theme switch. 759 if ( empty( $changeset[ $setting_id ] ) && isset( $mapped_nav_menu_locations[ $location ] ) ) { 760 $this->manager->set_post_value( $setting_id, $mapped_nav_menu_locations[ $location ] ); 761 } 762 763 $this->manager->add_control( 764 new WP_Customize_Nav_Menu_Location_Control( 765 $this->manager, 766 $setting_id, 767 array( 768 'label' => $description, 769 'location_id' => $location, 770 'section' => 'menu_locations', 771 'choices' => $choices, 772 ) 773 ) 774 ); 775 } 776 777 // Used to denote post states for special pages. 778 if ( ! function_exists( 'get_post_states' ) ) { 779 require_once ABSPATH . 'wp-admin/includes/template.php'; 780 } 781 782 // Register each menu as a Customizer section, and add each menu item to each menu. 783 foreach ( $menus as $menu ) { 784 $menu_id = $menu->term_id; 785 786 // Create a section for each menu. 787 $section_id = 'nav_menu[' . $menu_id . ']'; 788 $this->manager->add_section( 789 new WP_Customize_Nav_Menu_Section( 790 $this->manager, 791 $section_id, 792 array( 793 'title' => html_entity_decode( $menu->name, ENT_QUOTES, get_bloginfo( 'charset' ) ), 794 'priority' => 10, 795 'panel' => 'nav_menus', 796 ) 797 ) 798 ); 799 800 $nav_menu_setting_id = 'nav_menu[' . $menu_id . ']'; 801 $this->manager->add_setting( 802 new WP_Customize_Nav_Menu_Setting( 803 $this->manager, 804 $nav_menu_setting_id, 805 array( 806 'transport' => 'postMessage', 807 ) 808 ) 809 ); 810 811 // Add the menu contents. 812 $menu_items = (array) wp_get_nav_menu_items( $menu_id ); 813 814 foreach ( array_values( $menu_items ) as $i => $item ) { 815 816 // Create a setting for each menu item (which doesn't actually manage data, currently). 817 $menu_item_setting_id = 'nav_menu_item[' . $item->ID . ']'; 818 819 $value = (array) $item; 820 if ( empty( $value['post_title'] ) ) { 821 $value['title'] = ''; 822 } 823 824 $value['nav_menu_term_id'] = $menu_id; 825 $this->manager->add_setting( 826 new WP_Customize_Nav_Menu_Item_Setting( 827 $this->manager, 828 $menu_item_setting_id, 829 array( 830 'value' => $value, 831 'transport' => 'postMessage', 832 ) 833 ) 834 ); 835 836 // Create a control for each menu item. 837 $this->manager->add_control( 838 new WP_Customize_Nav_Menu_Item_Control( 839 $this->manager, 840 $menu_item_setting_id, 841 array( 842 'label' => $item->title, 843 'section' => $section_id, 844 'priority' => 10 + $i, 845 ) 846 ) 847 ); 848 } 849 850 // Note: other controls inside of this section get added dynamically in JS via the MenuSection.ready() function. 851 } 852 853 // Add the add-new-menu section and controls. 854 $this->manager->add_section( 855 'add_menu', 856 array( 857 'type' => 'new_menu', 858 'title' => __( 'New Menu' ), 859 'panel' => 'nav_menus', 860 'priority' => 20, 861 ) 862 ); 863 864 $this->manager->add_setting( 865 new WP_Customize_Filter_Setting( 866 $this->manager, 867 'nav_menus_created_posts', 868 array( 869 'transport' => 'postMessage', 870 'type' => 'option', // To prevent theme prefix in changeset. 871 'default' => array(), 872 'sanitize_callback' => array( $this, 'sanitize_nav_menus_created_posts' ), 873 ) 874 ) 875 ); 876 } 877 878 /** 879 * Gets the base10 intval. 880 * 881 * This is used as a setting's sanitize_callback; we can't use just plain 882 * intval because the second argument is not what intval() expects. 883 * 884 * @since 4.3.0 885 * 886 * @param mixed $value Number to convert. 887 * @return int Integer. 888 */ 889 public function intval_base10( $value ) { 890 return intval( $value, 10 ); 891 } 892 893 /** 894 * Returns an array of all the available item types. 895 * 896 * @since 4.3.0 897 * @since 4.7.0 Each array item now includes a `$type_label` in addition to `$title`, `$type`, and `$object`. 898 * 899 * @return array The available menu item types. 900 */ 901 public function available_item_types() { 902 $item_types = array(); 903 904 $post_types = get_post_types( array( 'show_in_nav_menus' => true ), 'objects' ); 905 if ( $post_types ) { 906 foreach ( $post_types as $slug => $post_type ) { 907 $item_types[] = array( 908 'title' => $post_type->labels->name, 909 'type_label' => $post_type->labels->singular_name, 910 'type' => 'post_type', 911 'object' => $post_type->name, 912 ); 913 } 914 } 915 916 $taxonomies = get_taxonomies( array( 'show_in_nav_menus' => true ), 'objects' ); 917 if ( $taxonomies ) { 918 foreach ( $taxonomies as $slug => $taxonomy ) { 919 if ( 'post_format' === $taxonomy && ! current_theme_supports( 'post-formats' ) ) { 920 continue; 921 } 922 $item_types[] = array( 923 'title' => $taxonomy->labels->name, 924 'type_label' => $taxonomy->labels->singular_name, 925 'type' => 'taxonomy', 926 'object' => $taxonomy->name, 927 ); 928 } 929 } 930 931 /** 932 * Filters the available menu item types. 933 * 934 * @since 4.3.0 935 * @since 4.7.0 Each array item now includes a `$type_label` in addition to `$title`, `$type`, and `$object`. 936 * 937 * @param array $item_types Navigation menu item types. 938 */ 939 $item_types = apply_filters( 'customize_nav_menu_available_item_types', $item_types ); 940 941 return $item_types; 942 } 943 944 /** 945 * Adds a new `auto-draft` post. 946 * 947 * @since 4.7.0 948 * 949 * @param array $postarr { 950 * Post array. Note that post_status is overridden to be `auto-draft`. 951 * 952 * @type string $post_title Post title. Required. 953 * @type string $post_type Post type. Required. 954 * @type string $post_name Post name. 955 * @type string $post_content Post content. 956 * } 957 * @return WP_Post|WP_Error Inserted auto-draft post object or error. 958 */ 959 public function insert_auto_draft_post( $postarr ) { 960 if ( ! isset( $postarr['post_type'] ) ) { 961 return new WP_Error( 'unknown_post_type', __( 'Invalid post type.' ) ); 962 } 963 if ( empty( $postarr['post_title'] ) ) { 964 return new WP_Error( 'empty_title', __( 'Empty title.' ) ); 965 } 966 if ( ! empty( $postarr['post_status'] ) ) { 967 return new WP_Error( 'status_forbidden', __( 'Status is forbidden.' ) ); 968 } 969 970 /* 971 * If the changeset is a draft, this will change to draft the next time the changeset 972 * is updated; otherwise, auto-draft will persist in autosave revisions, until save. 973 */ 974 $postarr['post_status'] = 'auto-draft'; 975 976 // Auto-drafts are allowed to have empty post_names, so it has to be explicitly set. 977 if ( empty( $postarr['post_name'] ) ) { 978 $postarr['post_name'] = sanitize_title( $postarr['post_title'] ); 979 } 980 if ( ! isset( $postarr['meta_input'] ) ) { 981 $postarr['meta_input'] = array(); 982 } 983 $postarr['meta_input']['_customize_draft_post_name'] = $postarr['post_name']; 984 $postarr['meta_input']['_customize_changeset_uuid'] = $this->manager->changeset_uuid(); 985 unset( $postarr['post_name'] ); 986 987 add_filter( 'wp_insert_post_empty_content', '__return_false', 1000 ); 988 $r = wp_insert_post( wp_slash( $postarr ), true ); 989 remove_filter( 'wp_insert_post_empty_content', '__return_false', 1000 ); 990 991 if ( is_wp_error( $r ) ) { 992 return $r; 993 } else { 994 return get_post( $r ); 995 } 996 } 997 998 /** 999 * Ajax handler for adding a new auto-draft post. 1000 * 1001 * @since 4.7.0 1002 * 1003 * @return never 1004 */ 1005 public function ajax_insert_auto_draft_post() { 1006 if ( ! check_ajax_referer( 'customize-menus', 'customize-menus-nonce', false ) ) { 1007 wp_send_json_error( 'bad_nonce', 400 ); 1008 } 1009 1010 if ( ! current_user_can( 'customize' ) ) { 1011 wp_send_json_error( 'customize_not_allowed', 403 ); 1012 } 1013 1014 if ( empty( $_POST['params'] ) || ! is_array( $_POST['params'] ) ) { 1015 wp_send_json_error( 'missing_params', 400 ); 1016 } 1017 1018 $params = wp_unslash( $_POST['params'] ); 1019 $illegal_params = array_diff( array_keys( $params ), array( 'post_type', 'post_title' ) ); 1020 if ( ! empty( $illegal_params ) ) { 1021 wp_send_json_error( 'illegal_params', 400 ); 1022 } 1023 1024 $params = array_merge( 1025 array( 1026 'post_type' => '', 1027 'post_title' => '', 1028 ), 1029 $params 1030 ); 1031 1032 if ( empty( $params['post_type'] ) || ! post_type_exists( $params['post_type'] ) ) { 1033 status_header( 400 ); 1034 wp_send_json_error( 'missing_post_type_param' ); 1035 } 1036 1037 $post_type_object = get_post_type_object( $params['post_type'] ); 1038 if ( ! current_user_can( $post_type_object->cap->create_posts ) || ! current_user_can( $post_type_object->cap->publish_posts ) ) { 1039 status_header( 403 ); 1040 wp_send_json_error( 'insufficient_post_permissions' ); 1041 } 1042 1043 $params['post_title'] = trim( $params['post_title'] ); 1044 if ( '' === $params['post_title'] ) { 1045 status_header( 400 ); 1046 wp_send_json_error( 'missing_post_title' ); 1047 } 1048 1049 $r = $this->insert_auto_draft_post( $params ); 1050 if ( is_wp_error( $r ) ) { 1051 $error = $r; 1052 if ( ! empty( $post_type_object->labels->singular_name ) ) { 1053 $singular_name = $post_type_object->labels->singular_name; 1054 } else { 1055 $singular_name = __( 'Post' ); 1056 } 1057 1058 $data = array( 1059 /* translators: 1: Post type name, 2: Error message. */ 1060 'message' => sprintf( __( '%1$s could not be created: %2$s' ), $singular_name, $error->get_error_message() ), 1061 ); 1062 wp_send_json_error( $data ); 1063 } else { 1064 $post = $r; 1065 $data = array( 1066 'post_id' => $post->ID, 1067 'url' => get_permalink( $post->ID ), 1068 ); 1069 wp_send_json_success( $data ); 1070 } 1071 } 1072 1073 /** 1074 * Prints the JavaScript templates used to render Menu Customizer components. 1075 * 1076 * Templates are imported into the JS use wp.template. 1077 * 1078 * @since 4.3.0 1079 */ 1080 public function print_templates() { 1081 ?> 1082 <script type="text/html" id="tmpl-available-menu-item"> 1083 <li id="menu-item-tpl-{{ data.id }}" class="menu-item-tpl" data-menu-item-id="{{ data.id }}"> 1084 <div class="menu-item-bar"> 1085 <div class="menu-item-handle"> 1086 <span class="item-type" aria-hidden="true">{{ data.type_label }}</span> 1087 <span class="item-title" aria-hidden="true"> 1088 <span class="menu-item-title<# if ( ! data.title ) { #> no-title<# } #>">{{ data.title || wp.customize.Menus.data.l10n.untitled }}</span> 1089 </span> 1090 <button type="button" class="button-link item-add"> 1091 <span class="screen-reader-text"> 1092 <?php 1093 /* translators: Hidden accessibility text. 1: Title of a menu item, 2: Type of a menu item. */ 1094 printf( __( 'Add to menu: %1$s (%2$s)' ), '{{ data.title || wp.customize.Menus.data.l10n.untitled }}', '{{ data.type_label }}' ); 1095 ?> 1096 </span> 1097 </button> 1098 </div> 1099 </div> 1100 </li> 1101 </script> 1102 1103 <script type="text/html" id="tmpl-menu-item-reorder-nav"> 1104 <div class="menu-item-reorder-nav"> 1105 <?php 1106 printf( 1107 '<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>', 1108 __( 'Move up' ), 1109 __( 'Move down' ), 1110 __( 'Move one level up' ), 1111 __( 'Move one level down' ) 1112 ); 1113 ?> 1114 </div> 1115 </script> 1116 1117 <script type="text/html" id="tmpl-nav-menu-delete-button"> 1118 <div class="menu-delete-item"> 1119 <button type="button" class="button-link button-link-delete"> 1120 <?php _e( 'Delete Menu' ); ?> 1121 </button> 1122 </div> 1123 </script> 1124 1125 <script type="text/html" id="tmpl-nav-menu-submit-new-button"> 1126 <p id="customize-new-menu-submit-description"><?php _e( 'Click “Next” to start adding links to your new menu.' ); ?></p> 1127 <button id="customize-new-menu-submit" type="button" class="button" aria-describedby="customize-new-menu-submit-description"><?php _e( 'Next' ); ?></button> 1128 </script> 1129 1130 <script type="text/html" id="tmpl-nav-menu-locations-header"> 1131 <span class="customize-control-title customize-section-title-menu_locations-heading">{{ data.l10n.locationsTitle }}</span> 1132 <p class="customize-control-description customize-section-title-menu_locations-description">{{ data.l10n.locationsDescription }}</p> 1133 </script> 1134 1135 <script type="text/html" id="tmpl-nav-menu-create-menu-section-title"> 1136 <p class="add-new-menu-notice"> 1137 <?php _e( 'It does not look like your site has any menus yet. Want to build one? Click the button to start.' ); ?> 1138 </p> 1139 <p class="add-new-menu-notice"> 1140 <?php _e( 'You’ll create a menu, assign it a location, and add menu items like links to pages and categories. If your theme has multiple menu areas, you might need to create more than one.' ); ?> 1141 </p> 1142 <h3> 1143 <button type="button" class="button customize-add-menu-button"> 1144 <?php _e( 'Create New Menu' ); ?> 1145 </button> 1146 </h3> 1147 </script> 1148 <?php 1149 } 1150 1151 /** 1152 * Prints the HTML template used to render the add-menu-item frame. 1153 * 1154 * @since 4.3.0 1155 */ 1156 public function available_items_template() { 1157 ?> 1158 <div id="available-menu-items" class="accordion-container"> 1159 <div class="customize-section-title"> 1160 <button type="button" class="customize-section-back" tabindex="-1"> 1161 <span class="screen-reader-text"> 1162 <?php 1163 /* translators: Hidden accessibility text. */ 1164 _e( 'Back' ); 1165 ?> 1166 </span> 1167 </button> 1168 <h3> 1169 <span class="customize-action"> 1170 <?php 1171 /* translators: ▸ is the unicode right-pointing triangle. %s: Section title in the Customizer. */ 1172 printf( __( 'Customizing ▸ %s' ), esc_html( $this->manager->get_panel( 'nav_menus' )->title ) ); 1173 ?> 1174 </span> 1175 <?php _e( 'Add Menu Items' ); ?> 1176 </h3> 1177 </div> 1178 <div id="available-menu-items-search" class="accordion-section cannot-expand"> 1179 <div class="accordion-section-title"> 1180 <label for="menu-items-search"><?php _e( 'Search Menu Items' ); ?></label> 1181 <input type="text" id="menu-items-search" aria-describedby="menu-items-search-desc" /> 1182 <p class="screen-reader-text" id="menu-items-search-desc"> 1183 <?php 1184 /* translators: Hidden accessibility text. */ 1185 _e( 'The search results will be updated as you type.' ); 1186 ?> 1187 </p> 1188 <span class="spinner"></span> 1189 <div class="search-icon" aria-hidden="true"></div> 1190 <button type="button" class="clear-results"><span class="screen-reader-text"> 1191 <?php 1192 /* translators: Hidden accessibility text. */ 1193 _e( 'Clear Results' ); 1194 ?> 1195 </span></button> 1196 </div> 1197 <ul class="accordion-section-content available-menu-items-list" data-type="search"></ul> 1198 </div> 1199 <?php 1200 1201 // Ensure the page post type comes first in the list. 1202 $item_types = $this->available_item_types(); 1203 $page_item_type = null; 1204 foreach ( $item_types as $i => $item_type ) { 1205 if ( isset( $item_type['object'] ) && 'page' === $item_type['object'] ) { 1206 $page_item_type = $item_type; 1207 unset( $item_types[ $i ] ); 1208 } 1209 } 1210 1211 $this->print_custom_links_available_menu_item(); 1212 if ( $page_item_type ) { 1213 $this->print_post_type_container( $page_item_type ); 1214 } 1215 // Containers for per-post-type item browsing; items are added with JS. 1216 foreach ( $item_types as $item_type ) { 1217 $this->print_post_type_container( $item_type ); 1218 } 1219 ?> 1220 </div><!-- #available-menu-items --> 1221 <?php 1222 } 1223 1224 /** 1225 * Prints the markup for new menu items. 1226 * 1227 * To be used in the template #available-menu-items. 1228 * 1229 * @since 4.7.0 1230 * 1231 * @param array $available_item_type Menu item data to output, including title, type, and label. 1232 */ 1233 protected function print_post_type_container( $available_item_type ) { 1234 $id = sprintf( 'available-menu-items-%s-%s', $available_item_type['type'], $available_item_type['object'] ); 1235 ?> 1236 <div id="<?php echo esc_attr( $id ); ?>" class="accordion-section"> 1237 <h4 class="accordion-section-title"> 1238 <button type="button" class="accordion-trigger" aria-expanded="false" aria-controls="<?php echo esc_attr( $id ); ?>-content"> 1239 <?php echo esc_html( $available_item_type['title'] ); ?> 1240 <span class="spinner"></span> 1241 <span class="no-items"><?php _e( 'No items' ); ?></span> 1242 <span class="toggle-indicator" aria-hidden="true"></span> 1243 </button> 1244 </h4> 1245 <div class="accordion-section-content" id="<?php echo esc_attr( $id ); ?>-content"> 1246 <?php if ( 'post_type' === $available_item_type['type'] ) : ?> 1247 <?php $post_type_obj = get_post_type_object( $available_item_type['object'] ); ?> 1248 <?php if ( current_user_can( $post_type_obj->cap->create_posts ) && current_user_can( $post_type_obj->cap->publish_posts ) ) : ?> 1249 <div class="new-content-item-wrapper"> 1250 <label for="<?php echo esc_attr( 'create-item-input-' . $available_item_type['object'] ); ?>"><?php echo esc_html( $post_type_obj->labels->add_new_item ); ?></label> 1251 <div class="new-content-item"> 1252 <input type="text" id="<?php echo esc_attr( 'create-item-input-' . $available_item_type['object'] ); ?>" class="create-item-input form-required"> 1253 <button type="button" class="button add-content"><?php _e( 'Add' ); ?></button> 1254 </div> 1255 <span id="create-input-<?php echo esc_attr( $available_item_type['object'] ); ?>-error" class="create-item-error error-message" style="display: none;"><?php _e( 'Please enter an item title' ); ?></span> 1256 1257 </div> 1258 <?php endif; ?> 1259 <?php endif; ?> 1260 <ul class="available-menu-items-list" data-type="<?php echo esc_attr( $available_item_type['type'] ); ?>" data-object="<?php echo esc_attr( $available_item_type['object'] ); ?>" data-type_label="<?php echo esc_attr( $available_item_type['type_label'] ?? $available_item_type['type'] ); ?>"></ul> 1261 </div> 1262 </div> 1263 <?php 1264 } 1265 1266 /** 1267 * Prints the markup for available menu item custom links. 1268 * 1269 * @since 4.7.0 1270 */ 1271 protected function print_custom_links_available_menu_item() { 1272 ?> 1273 <div id="new-custom-menu-item" class="accordion-section"> 1274 <h4 class="accordion-section-title"> 1275 <button type="button" class="accordion-trigger" aria-expanded="false" aria-controls="new-custom-menu-item-content"> 1276 <?php _e( 'Custom Links' ); ?> 1277 <span class="toggle-indicator" aria-hidden="true"></span> 1278 </button> 1279 </h4> 1280 <div class="accordion-section-content customlinkdiv" id="new-custom-menu-item-content"> 1281 <input type="hidden" value="custom" id="custom-menu-item-type" name="menu-item[-1][menu-item-type]" /> 1282 <p id="menu-item-url-wrap" class="wp-clearfix"> 1283 <label class="howto" for="custom-menu-item-url"><?php _e( 'URL' ); ?></label> 1284 <input id="custom-menu-item-url" name="menu-item[-1][menu-item-url]" type="text" class="code menu-item-textbox" placeholder="https://"> 1285 <span id="custom-url-error" class="error-message" style="display: none;"><?php _e( 'Please provide a valid link.' ); ?></span> 1286 </p> 1287 <p id="menu-item-name-wrap" class="wp-clearfix"> 1288 <label class="howto" for="custom-menu-item-name"><?php _e( 'Link Text' ); ?></label> 1289 <input id="custom-menu-item-name" name="menu-item[-1][menu-item-title]" type="text" class="regular-text menu-item-textbox"> 1290 <span id="custom-name-error" class="error-message" style="display: none;"><?php _e( 'The link text cannot be empty.' ); ?></span> 1291 </p> 1292 <p class="button-controls"> 1293 <span class="add-to-menu"> 1294 <input type="submit" class="button submit-add-to-menu right" value="<?php esc_attr_e( 'Add to Menu' ); ?>" name="add-custom-menu-item" id="custom-menu-item-submit"> 1295 <span class="spinner"></span> 1296 </span> 1297 </p> 1298 </div> 1299 </div> 1300 <?php 1301 } 1302 1303 // 1304 // Start functionality specific to partial-refresh of menu changes in Customizer preview. 1305 // 1306 1307 /** 1308 * Nav menu args used for each instance, keyed by the args HMAC. 1309 * 1310 * @since 4.3.0 1311 * @var array 1312 */ 1313 public $preview_nav_menu_instance_args = array(); 1314 1315 /** 1316 * Filters arguments for dynamic nav_menu selective refresh partials. 1317 * 1318 * @since 4.5.0 1319 * 1320 * @param array|false $partial_args Partial args. 1321 * @param string $partial_id Partial ID. 1322 * @return array Partial args. 1323 */ 1324 public function customize_dynamic_partial_args( $partial_args, $partial_id ) { 1325 1326 if ( preg_match( '/^nav_menu_instance\[[0-9a-f]{32}\]$/', $partial_id ) ) { 1327 if ( false === $partial_args ) { 1328 $partial_args = array(); 1329 } 1330 $partial_args = array_merge( 1331 $partial_args, 1332 array( 1333 'type' => 'nav_menu_instance', 1334 'render_callback' => array( $this, 'render_nav_menu_partial' ), 1335 'container_inclusive' => true, 1336 'settings' => array(), // Empty because the nav menu instance may relate to a menu or a location. 1337 'capability' => 'edit_theme_options', 1338 ) 1339 ); 1340 } 1341 1342 return $partial_args; 1343 } 1344 1345 /** 1346 * Adds hooks for the Customizer preview. 1347 * 1348 * @since 4.3.0 1349 */ 1350 public function customize_preview_init() { 1351 add_action( 'wp_enqueue_scripts', array( $this, 'customize_preview_enqueue_deps' ) ); 1352 add_filter( 'wp_nav_menu_args', array( $this, 'filter_wp_nav_menu_args' ), 1000 ); 1353 add_filter( 'wp_nav_menu', array( $this, 'filter_wp_nav_menu' ), 10, 2 ); 1354 add_action( 'wp_footer', array( $this, 'export_preview_data' ), 1 ); 1355 add_filter( 'customize_render_partials_response', array( $this, 'export_partial_rendered_nav_menu_instances' ) ); 1356 } 1357 1358 /** 1359 * Makes the auto-draft status protected so that it can be queried. 1360 * 1361 * @since 4.7.0 1362 * 1363 * @global stdClass[] $wp_post_statuses List of post statuses. 1364 */ 1365 public function make_auto_draft_status_previewable() { 1366 global $wp_post_statuses; 1367 $wp_post_statuses['auto-draft']->protected = true; 1368 } 1369 1370 /** 1371 * Sanitizes post IDs for posts created for nav menu items to be published. 1372 * 1373 * @since 4.7.0 1374 * 1375 * @param array $value Post IDs. 1376 * @return array Post IDs. 1377 */ 1378 public function sanitize_nav_menus_created_posts( $value ) { 1379 $post_ids = array(); 1380 foreach ( wp_parse_id_list( $value ) as $post_id ) { 1381 if ( empty( $post_id ) ) { 1382 continue; 1383 } 1384 $post = get_post( $post_id ); 1385 if ( 'auto-draft' !== $post->post_status && 'draft' !== $post->post_status ) { 1386 continue; 1387 } 1388 $post_type_obj = get_post_type_object( $post->post_type ); 1389 if ( ! $post_type_obj ) { 1390 continue; 1391 } 1392 if ( ! current_user_can( $post_type_obj->cap->publish_posts ) || ! current_user_can( 'edit_post', $post_id ) ) { 1393 continue; 1394 } 1395 $post_ids[] = $post->ID; 1396 } 1397 return $post_ids; 1398 } 1399 1400 /** 1401 * Publishes the auto-draft posts that were created for nav menu items. 1402 * 1403 * The post IDs will have been sanitized by already by 1404 * `WP_Customize_Nav_Menu_Items::sanitize_nav_menus_created_posts()` to 1405 * remove any post IDs for which the user cannot publish or for which the 1406 * post is not an auto-draft. 1407 * 1408 * @since 4.7.0 1409 * 1410 * @param WP_Customize_Setting $setting Customizer setting object. 1411 */ 1412 public function save_nav_menus_created_posts( $setting ) { 1413 $post_ids = $setting->post_value(); 1414 if ( ! empty( $post_ids ) ) { 1415 foreach ( $post_ids as $post_id ) { 1416 1417 // Prevent overriding the status that a user may have prematurely updated the post to. 1418 $current_status = get_post_status( $post_id ); 1419 if ( 'auto-draft' !== $current_status && 'draft' !== $current_status ) { 1420 continue; 1421 } 1422 1423 $target_status = 'attachment' === get_post_type( $post_id ) ? 'inherit' : 'publish'; 1424 $args = array( 1425 'ID' => $post_id, 1426 'post_status' => $target_status, 1427 ); 1428 $post_name = get_post_meta( $post_id, '_customize_draft_post_name', true ); 1429 if ( $post_name ) { 1430 $args['post_name'] = $post_name; 1431 } 1432 1433 // Note that wp_publish_post() cannot be used because unique slugs need to be assigned. 1434 wp_update_post( wp_slash( $args ) ); 1435 1436 delete_post_meta( $post_id, '_customize_draft_post_name' ); 1437 } 1438 } 1439 } 1440 1441 /** 1442 * Keeps track of the arguments that are being passed to wp_nav_menu(). 1443 * 1444 * @since 4.3.0 1445 * 1446 * @see wp_nav_menu() 1447 * @see WP_Customize_Widgets::filter_dynamic_sidebar_params() 1448 * 1449 * @param array $args An array containing wp_nav_menu() arguments. 1450 * @return array Arguments. 1451 */ 1452 public function filter_wp_nav_menu_args( $args ) { 1453 /* 1454 * The following conditions determine whether or not this instance of 1455 * wp_nav_menu() can use selective refreshed. A wp_nav_menu() can be 1456 * selective refreshed if... 1457 */ 1458 $can_partial_refresh = ( 1459 // ...if wp_nav_menu() is directly echoing out the menu (and thus isn't manipulating the string after generated), 1460 ! empty( $args['echo'] ) 1461 && 1462 // ...and if the fallback_cb can be serialized to JSON, since it will be included in the placement context data, 1463 ( empty( $args['fallback_cb'] ) || is_string( $args['fallback_cb'] ) ) 1464 && 1465 // ...and if the walker can also be serialized to JSON, since it will be included in the placement context data as well, 1466 ( empty( $args['walker'] ) || is_string( $args['walker'] ) ) 1467 // ...and if it has a theme location assigned or an assigned menu to display, 1468 && ( 1469 ! empty( $args['theme_location'] ) 1470 || 1471 ( ! empty( $args['menu'] ) && ( is_numeric( $args['menu'] ) || is_object( $args['menu'] ) ) ) 1472 ) 1473 && 1474 // ...and if the nav menu would be rendered with a wrapper container element (upon which to attach data-* attributes). 1475 ( 1476 ! empty( $args['container'] ) 1477 || 1478 ( isset( $args['items_wrap'] ) && str_starts_with( $args['items_wrap'], '<' ) ) 1479 ) 1480 ); 1481 $args['can_partial_refresh'] = $can_partial_refresh; 1482 1483 $exported_args = $args; 1484 1485 // Empty out args which may not be JSON-serializable. 1486 if ( ! $can_partial_refresh ) { 1487 $exported_args['fallback_cb'] = ''; 1488 $exported_args['walker'] = ''; 1489 } 1490 1491 /* 1492 * Replace object menu arg with a term_id menu arg, as this exports better 1493 * to JS and is easier to compare hashes. 1494 */ 1495 if ( ! empty( $exported_args['menu'] ) && is_object( $exported_args['menu'] ) ) { 1496 $exported_args['menu'] = $exported_args['menu']->term_id; 1497 } 1498 1499 ksort( $exported_args ); 1500 $exported_args['args_hmac'] = $this->hash_nav_menu_args( $exported_args ); 1501 1502 $args['customize_preview_nav_menus_args'] = $exported_args; 1503 $this->preview_nav_menu_instance_args[ $exported_args['args_hmac'] ] = $exported_args; 1504 return $args; 1505 } 1506 1507 /** 1508 * Prepares wp_nav_menu() calls for partial refresh. 1509 * 1510 * Injects attributes into container element. 1511 * 1512 * @since 4.3.0 1513 * 1514 * @see wp_nav_menu() 1515 * 1516 * @param string $nav_menu_content The HTML content for the navigation menu. 1517 * @param object $args An object containing wp_nav_menu() arguments. 1518 * @return string Nav menu HTML with selective refresh attributes added if partial can be refreshed. 1519 */ 1520 public function filter_wp_nav_menu( $nav_menu_content, $args ) { 1521 if ( isset( $args->customize_preview_nav_menus_args['can_partial_refresh'] ) && $args->customize_preview_nav_menus_args['can_partial_refresh'] ) { 1522 $attributes = sprintf( ' data-customize-partial-id="%s"', esc_attr( 'nav_menu_instance[' . $args->customize_preview_nav_menus_args['args_hmac'] . ']' ) ); 1523 $attributes .= ' data-customize-partial-type="nav_menu_instance"'; 1524 $attributes .= sprintf( ' data-customize-partial-placement-context="%s"', esc_attr( wp_json_encode( $args->customize_preview_nav_menus_args ) ) ); 1525 $nav_menu_content = preg_replace( '#^(<\w+)#', '$1 ' . str_replace( '\\', '\\\\', $attributes ), $nav_menu_content, 1 ); 1526 } 1527 return $nav_menu_content; 1528 } 1529 1530 /** 1531 * Hashes (hmac) the nav menu arguments to ensure they are not tampered with when 1532 * submitted in the Ajax request. 1533 * 1534 * Note that the array is expected to be pre-sorted. 1535 * 1536 * @since 4.3.0 1537 * 1538 * @param array $args The arguments to hash. 1539 * @return string Hashed nav menu arguments. 1540 */ 1541 public function hash_nav_menu_args( $args ) { 1542 return wp_hash( serialize( $args ) ); 1543 } 1544 1545 /** 1546 * Enqueues scripts for the Customizer preview. 1547 * 1548 * @since 4.3.0 1549 */ 1550 public function customize_preview_enqueue_deps() { 1551 wp_enqueue_script( 'customize-preview-nav-menus' ); // Note that we have overridden this. 1552 } 1553 1554 /** 1555 * Exports data from PHP to JS. 1556 * 1557 * @since 4.3.0 1558 */ 1559 public function export_preview_data() { 1560 1561 // Why not wp_localize_script? Because we're not localizing, and it forces values into strings. 1562 $exports = array( 1563 'navMenuInstanceArgs' => $this->preview_nav_menu_instance_args, 1564 ); 1565 wp_print_inline_script_tag( sprintf( 'var _wpCustomizePreviewNavMenusExports = %s;', wp_json_encode( $exports, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ) ) . "\n//# sourceURL=" . rawurlencode( __METHOD__ ) ); 1566 } 1567 1568 /** 1569 * Exports any wp_nav_menu() calls during the rendering of any partials. 1570 * 1571 * @since 4.5.0 1572 * 1573 * @param array $response Response. 1574 * @return array Response. 1575 */ 1576 public function export_partial_rendered_nav_menu_instances( $response ) { 1577 $response['nav_menu_instance_args'] = $this->preview_nav_menu_instance_args; 1578 return $response; 1579 } 1580 1581 /** 1582 * Renders a specific menu via wp_nav_menu() using the supplied arguments. 1583 * 1584 * @since 4.3.0 1585 * 1586 * @see wp_nav_menu() 1587 * 1588 * @param WP_Customize_Partial $partial Partial. 1589 * @param array $nav_menu_args Nav menu args supplied as container context. 1590 * @return string|false 1591 */ 1592 public function render_nav_menu_partial( $partial, $nav_menu_args ) { 1593 unset( $partial ); 1594 1595 if ( ! isset( $nav_menu_args['args_hmac'] ) ) { 1596 // Error: missing_args_hmac. 1597 return false; 1598 } 1599 1600 $nav_menu_args_hmac = $nav_menu_args['args_hmac']; 1601 unset( $nav_menu_args['args_hmac'] ); 1602 1603 ksort( $nav_menu_args ); 1604 if ( ! hash_equals( $this->hash_nav_menu_args( $nav_menu_args ), $nav_menu_args_hmac ) ) { 1605 // Error: args_hmac_mismatch. 1606 return false; 1607 } 1608 1609 ob_start(); 1610 wp_nav_menu( $nav_menu_args ); 1611 $content = ob_get_clean(); 1612 1613 return $content; 1614 } 1615 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Mon Aug 3 08:20:19 2026 | Cross-referenced by PHPXref |