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