|
1 <?php |
|
2 /** |
|
3 * Customize API: WP_Customize_Nav_Menu_Item_Setting class |
|
4 * |
|
5 * @package WordPress |
|
6 * @subpackage Customize |
|
7 * @since 4.4.0 |
|
8 */ |
|
9 |
|
10 /** |
|
11 * Customize Setting to represent a nav_menu. |
|
12 * |
|
13 * Subclass of WP_Customize_Setting to represent a nav_menu taxonomy term, and |
|
14 * the IDs for the nav_menu_items associated with the nav menu. |
|
15 * |
|
16 * @since 4.3.0 |
|
17 * |
|
18 * @see WP_Customize_Setting |
|
19 */ |
|
20 class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting { |
|
21 |
|
22 const ID_PATTERN = '/^nav_menu_item\[(?P<id>-?\d+)\]$/'; |
|
23 |
|
24 const POST_TYPE = 'nav_menu_item'; |
|
25 |
|
26 const TYPE = 'nav_menu_item'; |
|
27 |
|
28 /** |
|
29 * Setting type. |
|
30 * |
|
31 * @since 4.3.0 |
|
32 * @var string |
|
33 */ |
|
34 public $type = self::TYPE; |
|
35 |
|
36 /** |
|
37 * Default setting value. |
|
38 * |
|
39 * @since 4.3.0 |
|
40 * @var array |
|
41 * |
|
42 * @see wp_setup_nav_menu_item() |
|
43 */ |
|
44 public $default = array( |
|
45 // The $menu_item_data for wp_update_nav_menu_item(). |
|
46 'object_id' => 0, |
|
47 'object' => '', // Taxonomy name. |
|
48 'menu_item_parent' => 0, // A.K.A. menu-item-parent-id; note that post_parent is different, and not included. |
|
49 'position' => 0, // A.K.A. menu_order. |
|
50 'type' => 'custom', // Note that type_label is not included here. |
|
51 'title' => '', |
|
52 'url' => '', |
|
53 'target' => '', |
|
54 'attr_title' => '', |
|
55 'description' => '', |
|
56 'classes' => '', |
|
57 'xfn' => '', |
|
58 'status' => 'publish', |
|
59 'original_title' => '', |
|
60 'nav_menu_term_id' => 0, // This will be supplied as the $menu_id arg for wp_update_nav_menu_item(). |
|
61 '_invalid' => false, |
|
62 ); |
|
63 |
|
64 /** |
|
65 * Default transport. |
|
66 * |
|
67 * @since 4.3.0 |
|
68 * @since 4.5.0 Default changed to 'refresh' |
|
69 * @var string |
|
70 */ |
|
71 public $transport = 'refresh'; |
|
72 |
|
73 /** |
|
74 * The post ID represented by this setting instance. This is the db_id. |
|
75 * |
|
76 * A negative value represents a placeholder ID for a new menu not yet saved. |
|
77 * |
|
78 * @since 4.3.0 |
|
79 * @var int |
|
80 */ |
|
81 public $post_id; |
|
82 |
|
83 /** |
|
84 * Storage of pre-setup menu item to prevent wasted calls to wp_setup_nav_menu_item(). |
|
85 * |
|
86 * @since 4.3.0 |
|
87 * @var array |
|
88 */ |
|
89 protected $value; |
|
90 |
|
91 /** |
|
92 * Previous (placeholder) post ID used before creating a new menu item. |
|
93 * |
|
94 * This value will be exported to JS via the customize_save_response filter |
|
95 * so that JavaScript can update the settings to refer to the newly-assigned |
|
96 * post ID. This value is always negative to indicate it does not refer to |
|
97 * a real post. |
|
98 * |
|
99 * @since 4.3.0 |
|
100 * @var int |
|
101 * |
|
102 * @see WP_Customize_Nav_Menu_Item_Setting::update() |
|
103 * @see WP_Customize_Nav_Menu_Item_Setting::amend_customize_save_response() |
|
104 */ |
|
105 public $previous_post_id; |
|
106 |
|
107 /** |
|
108 * When previewing or updating a menu item, this stores the previous nav_menu_term_id |
|
109 * which ensures that we can apply the proper filters. |
|
110 * |
|
111 * @since 4.3.0 |
|
112 * @var int |
|
113 */ |
|
114 public $original_nav_menu_term_id; |
|
115 |
|
116 /** |
|
117 * Whether or not update() was called. |
|
118 * |
|
119 * @since 4.3.0 |
|
120 * @var bool |
|
121 */ |
|
122 protected $is_updated = false; |
|
123 |
|
124 /** |
|
125 * Status for calling the update method, used in customize_save_response filter. |
|
126 * |
|
127 * See {@see 'customize_save_response'}. |
|
128 * |
|
129 * When status is inserted, the placeholder post ID is stored in $previous_post_id. |
|
130 * When status is error, the error is stored in $update_error. |
|
131 * |
|
132 * @since 4.3.0 |
|
133 * @var string updated|inserted|deleted|error |
|
134 * |
|
135 * @see WP_Customize_Nav_Menu_Item_Setting::update() |
|
136 * @see WP_Customize_Nav_Menu_Item_Setting::amend_customize_save_response() |
|
137 */ |
|
138 public $update_status; |
|
139 |
|
140 /** |
|
141 * Any error object returned by wp_update_nav_menu_item() when setting is updated. |
|
142 * |
|
143 * @since 4.3.0 |
|
144 * @var WP_Error |
|
145 * |
|
146 * @see WP_Customize_Nav_Menu_Item_Setting::update() |
|
147 * @see WP_Customize_Nav_Menu_Item_Setting::amend_customize_save_response() |
|
148 */ |
|
149 public $update_error; |
|
150 |
|
151 /** |
|
152 * Constructor. |
|
153 * |
|
154 * Any supplied $args override class property defaults. |
|
155 * |
|
156 * @since 4.3.0 |
|
157 * |
|
158 * @param WP_Customize_Manager $manager Bootstrap Customizer instance. |
|
159 * @param string $id An specific ID of the setting. Can be a |
|
160 * theme mod or option name. |
|
161 * @param array $args Optional. Setting arguments. |
|
162 * |
|
163 * @throws Exception If $id is not valid for this setting type. |
|
164 */ |
|
165 public function __construct( WP_Customize_Manager $manager, $id, array $args = array() ) { |
|
166 if ( empty( $manager->nav_menus ) ) { |
|
167 throw new Exception( 'Expected WP_Customize_Manager::$nav_menus to be set.' ); |
|
168 } |
|
169 |
|
170 if ( ! preg_match( self::ID_PATTERN, $id, $matches ) ) { |
|
171 throw new Exception( "Illegal widget setting ID: $id" ); |
|
172 } |
|
173 |
|
174 $this->post_id = intval( $matches['id'] ); |
|
175 add_action( 'wp_update_nav_menu_item', array( $this, 'flush_cached_value' ), 10, 2 ); |
|
176 |
|
177 parent::__construct( $manager, $id, $args ); |
|
178 |
|
179 // Ensure that an initially-supplied value is valid. |
|
180 if ( isset( $this->value ) ) { |
|
181 $this->populate_value(); |
|
182 foreach ( array_diff( array_keys( $this->default ), array_keys( $this->value ) ) as $missing ) { |
|
183 throw new Exception( "Supplied nav_menu_item value missing property: $missing" ); |
|
184 } |
|
185 } |
|
186 |
|
187 } |
|
188 |
|
189 /** |
|
190 * Clear the cached value when this nav menu item is updated. |
|
191 * |
|
192 * @since 4.3.0 |
|
193 * |
|
194 * @param int $menu_id The term ID for the menu. |
|
195 * @param int $menu_item_id The post ID for the menu item. |
|
196 */ |
|
197 public function flush_cached_value( $menu_id, $menu_item_id ) { |
|
198 unset( $menu_id ); |
|
199 if ( $menu_item_id === $this->post_id ) { |
|
200 $this->value = null; |
|
201 } |
|
202 } |
|
203 |
|
204 /** |
|
205 * Get the instance data for a given nav_menu_item setting. |
|
206 * |
|
207 * @since 4.3.0 |
|
208 * |
|
209 * @see wp_setup_nav_menu_item() |
|
210 * |
|
211 * @return array|false Instance data array, or false if the item is marked for deletion. |
|
212 */ |
|
213 public function value() { |
|
214 if ( $this->is_previewed && $this->_previewed_blog_id === get_current_blog_id() ) { |
|
215 $undefined = new stdClass(); // Symbol. |
|
216 $post_value = $this->post_value( $undefined ); |
|
217 |
|
218 if ( $undefined === $post_value ) { |
|
219 $value = $this->_original_value; |
|
220 } else { |
|
221 $value = $post_value; |
|
222 } |
|
223 if ( ! empty( $value ) && empty( $value['original_title'] ) ) { |
|
224 $value['original_title'] = $this->get_original_title( (object) $value ); |
|
225 } |
|
226 } elseif ( isset( $this->value ) ) { |
|
227 $value = $this->value; |
|
228 } else { |
|
229 $value = false; |
|
230 |
|
231 // Note that a ID of less than one indicates a nav_menu not yet inserted. |
|
232 if ( $this->post_id > 0 ) { |
|
233 $post = get_post( $this->post_id ); |
|
234 if ( $post && self::POST_TYPE === $post->post_type ) { |
|
235 $is_title_empty = empty( $post->post_title ); |
|
236 $value = (array) wp_setup_nav_menu_item( $post ); |
|
237 if ( $is_title_empty ) { |
|
238 $value['title'] = ''; |
|
239 } |
|
240 } |
|
241 } |
|
242 |
|
243 if ( ! is_array( $value ) ) { |
|
244 $value = $this->default; |
|
245 } |
|
246 |
|
247 // Cache the value for future calls to avoid having to re-call wp_setup_nav_menu_item(). |
|
248 $this->value = $value; |
|
249 $this->populate_value(); |
|
250 $value = $this->value; |
|
251 } |
|
252 |
|
253 if ( ! empty( $value ) && empty( $value['type_label'] ) ) { |
|
254 $value['type_label'] = $this->get_type_label( (object) $value ); |
|
255 } |
|
256 |
|
257 return $value; |
|
258 } |
|
259 |
|
260 /** |
|
261 * Get original title. |
|
262 * |
|
263 * @since 4.7.0 |
|
264 * |
|
265 * @param object $item Nav menu item. |
|
266 * @return string The original title. |
|
267 */ |
|
268 protected function get_original_title( $item ) { |
|
269 $original_title = ''; |
|
270 if ( 'post_type' === $item->type && ! empty( $item->object_id ) ) { |
|
271 $original_object = get_post( $item->object_id ); |
|
272 if ( $original_object ) { |
|
273 /** This filter is documented in wp-includes/post-template.php */ |
|
274 $original_title = apply_filters( 'the_title', $original_object->post_title, $original_object->ID ); |
|
275 |
|
276 if ( '' === $original_title ) { |
|
277 /* translators: %d: ID of a post */ |
|
278 $original_title = sprintf( __( '#%d (no title)' ), $original_object->ID ); |
|
279 } |
|
280 } |
|
281 } elseif ( 'taxonomy' === $item->type && ! empty( $item->object_id ) ) { |
|
282 $original_term_title = get_term_field( 'name', $item->object_id, $item->object, 'raw' ); |
|
283 if ( ! is_wp_error( $original_term_title ) ) { |
|
284 $original_title = $original_term_title; |
|
285 } |
|
286 } elseif ( 'post_type_archive' === $item->type ) { |
|
287 $original_object = get_post_type_object( $item->object ); |
|
288 if ( $original_object ) { |
|
289 $original_title = $original_object->labels->archives; |
|
290 } |
|
291 } |
|
292 $original_title = html_entity_decode( $original_title, ENT_QUOTES, get_bloginfo( 'charset' ) ); |
|
293 return $original_title; |
|
294 } |
|
295 |
|
296 /** |
|
297 * Get type label. |
|
298 * |
|
299 * @since 4.7.0 |
|
300 * |
|
301 * @param object $item Nav menu item. |
|
302 * @returns string The type label. |
|
303 */ |
|
304 protected function get_type_label( $item ) { |
|
305 if ( 'post_type' === $item->type ) { |
|
306 $object = get_post_type_object( $item->object ); |
|
307 if ( $object ) { |
|
308 $type_label = $object->labels->singular_name; |
|
309 } else { |
|
310 $type_label = $item->object; |
|
311 } |
|
312 } elseif ( 'taxonomy' === $item->type ) { |
|
313 $object = get_taxonomy( $item->object ); |
|
314 if ( $object ) { |
|
315 $type_label = $object->labels->singular_name; |
|
316 } else { |
|
317 $type_label = $item->object; |
|
318 } |
|
319 } elseif ( 'post_type_archive' === $item->type ) { |
|
320 $type_label = __( 'Post Type Archive' ); |
|
321 } else { |
|
322 $type_label = __( 'Custom Link' ); |
|
323 } |
|
324 return $type_label; |
|
325 } |
|
326 |
|
327 /** |
|
328 * Ensure that the value is fully populated with the necessary properties. |
|
329 * |
|
330 * Translates some properties added by wp_setup_nav_menu_item() and removes others. |
|
331 * |
|
332 * @since 4.3.0 |
|
333 * |
|
334 * @see WP_Customize_Nav_Menu_Item_Setting::value() |
|
335 */ |
|
336 protected function populate_value() { |
|
337 if ( ! is_array( $this->value ) ) { |
|
338 return; |
|
339 } |
|
340 |
|
341 if ( isset( $this->value['menu_order'] ) ) { |
|
342 $this->value['position'] = $this->value['menu_order']; |
|
343 unset( $this->value['menu_order'] ); |
|
344 } |
|
345 if ( isset( $this->value['post_status'] ) ) { |
|
346 $this->value['status'] = $this->value['post_status']; |
|
347 unset( $this->value['post_status'] ); |
|
348 } |
|
349 |
|
350 if ( ! isset( $this->value['original_title'] ) ) { |
|
351 $this->value['original_title'] = $this->get_original_title( (object) $this->value ); |
|
352 } |
|
353 |
|
354 if ( ! isset( $this->value['nav_menu_term_id'] ) && $this->post_id > 0 ) { |
|
355 $menus = wp_get_post_terms( $this->post_id, WP_Customize_Nav_Menu_Setting::TAXONOMY, array( |
|
356 'fields' => 'ids', |
|
357 ) ); |
|
358 if ( ! empty( $menus ) ) { |
|
359 $this->value['nav_menu_term_id'] = array_shift( $menus ); |
|
360 } else { |
|
361 $this->value['nav_menu_term_id'] = 0; |
|
362 } |
|
363 } |
|
364 |
|
365 foreach ( array( 'object_id', 'menu_item_parent', 'nav_menu_term_id' ) as $key ) { |
|
366 if ( ! is_int( $this->value[ $key ] ) ) { |
|
367 $this->value[ $key ] = intval( $this->value[ $key ] ); |
|
368 } |
|
369 } |
|
370 foreach ( array( 'classes', 'xfn' ) as $key ) { |
|
371 if ( is_array( $this->value[ $key ] ) ) { |
|
372 $this->value[ $key ] = implode( ' ', $this->value[ $key ] ); |
|
373 } |
|
374 } |
|
375 |
|
376 if ( ! isset( $this->value['title'] ) ) { |
|
377 $this->value['title'] = ''; |
|
378 } |
|
379 |
|
380 if ( ! isset( $this->value['_invalid'] ) ) { |
|
381 $this->value['_invalid'] = false; |
|
382 $is_known_invalid = ( |
|
383 ( ( 'post_type' === $this->value['type'] || 'post_type_archive' === $this->value['type'] ) && ! post_type_exists( $this->value['object'] ) ) |
|
384 || |
|
385 ( 'taxonomy' === $this->value['type'] && ! taxonomy_exists( $this->value['object'] ) ) |
|
386 ); |
|
387 if ( $is_known_invalid ) { |
|
388 $this->value['_invalid'] = true; |
|
389 } |
|
390 } |
|
391 |
|
392 // Remove remaining properties available on a setup nav_menu_item post object which aren't relevant to the setting value. |
|
393 $irrelevant_properties = array( |
|
394 'ID', |
|
395 'comment_count', |
|
396 'comment_status', |
|
397 'db_id', |
|
398 'filter', |
|
399 'guid', |
|
400 'ping_status', |
|
401 'pinged', |
|
402 'post_author', |
|
403 'post_content', |
|
404 'post_content_filtered', |
|
405 'post_date', |
|
406 'post_date_gmt', |
|
407 'post_excerpt', |
|
408 'post_mime_type', |
|
409 'post_modified', |
|
410 'post_modified_gmt', |
|
411 'post_name', |
|
412 'post_parent', |
|
413 'post_password', |
|
414 'post_title', |
|
415 'post_type', |
|
416 'to_ping', |
|
417 ); |
|
418 foreach ( $irrelevant_properties as $property ) { |
|
419 unset( $this->value[ $property ] ); |
|
420 } |
|
421 } |
|
422 |
|
423 /** |
|
424 * Handle previewing the setting. |
|
425 * |
|
426 * @since 4.3.0 |
|
427 * @since 4.4.0 Added boolean return value. |
|
428 * |
|
429 * @see WP_Customize_Manager::post_value() |
|
430 * |
|
431 * @return bool False if method short-circuited due to no-op. |
|
432 */ |
|
433 public function preview() { |
|
434 if ( $this->is_previewed ) { |
|
435 return false; |
|
436 } |
|
437 |
|
438 $undefined = new stdClass(); |
|
439 $is_placeholder = ( $this->post_id < 0 ); |
|
440 $is_dirty = ( $undefined !== $this->post_value( $undefined ) ); |
|
441 if ( ! $is_placeholder && ! $is_dirty ) { |
|
442 return false; |
|
443 } |
|
444 |
|
445 $this->is_previewed = true; |
|
446 $this->_original_value = $this->value(); |
|
447 $this->original_nav_menu_term_id = $this->_original_value['nav_menu_term_id']; |
|
448 $this->_previewed_blog_id = get_current_blog_id(); |
|
449 |
|
450 add_filter( 'wp_get_nav_menu_items', array( $this, 'filter_wp_get_nav_menu_items' ), 10, 3 ); |
|
451 |
|
452 $sort_callback = array( __CLASS__, 'sort_wp_get_nav_menu_items' ); |
|
453 if ( ! has_filter( 'wp_get_nav_menu_items', $sort_callback ) ) { |
|
454 add_filter( 'wp_get_nav_menu_items', array( __CLASS__, 'sort_wp_get_nav_menu_items' ), 1000, 3 ); |
|
455 } |
|
456 |
|
457 // @todo Add get_post_metadata filters for plugins to add their data. |
|
458 |
|
459 return true; |
|
460 } |
|
461 |
|
462 /** |
|
463 * Filters the wp_get_nav_menu_items() result to supply the previewed menu items. |
|
464 * |
|
465 * @since 4.3.0 |
|
466 * |
|
467 * @see wp_get_nav_menu_items() |
|
468 * |
|
469 * @param array $items An array of menu item post objects. |
|
470 * @param object $menu The menu object. |
|
471 * @param array $args An array of arguments used to retrieve menu item objects. |
|
472 * @return array Array of menu items, |
|
473 */ |
|
474 public function filter_wp_get_nav_menu_items( $items, $menu, $args ) { |
|
475 $this_item = $this->value(); |
|
476 $current_nav_menu_term_id = $this_item['nav_menu_term_id']; |
|
477 unset( $this_item['nav_menu_term_id'] ); |
|
478 |
|
479 $should_filter = ( |
|
480 $menu->term_id === $this->original_nav_menu_term_id |
|
481 || |
|
482 $menu->term_id === $current_nav_menu_term_id |
|
483 ); |
|
484 if ( ! $should_filter ) { |
|
485 return $items; |
|
486 } |
|
487 |
|
488 // Handle deleted menu item, or menu item moved to another menu. |
|
489 $should_remove = ( |
|
490 false === $this_item |
|
491 || |
|
492 true === $this_item['_invalid'] |
|
493 || |
|
494 ( |
|
495 $this->original_nav_menu_term_id === $menu->term_id |
|
496 && |
|
497 $current_nav_menu_term_id !== $this->original_nav_menu_term_id |
|
498 ) |
|
499 ); |
|
500 if ( $should_remove ) { |
|
501 $filtered_items = array(); |
|
502 foreach ( $items as $item ) { |
|
503 if ( $item->db_id !== $this->post_id ) { |
|
504 $filtered_items[] = $item; |
|
505 } |
|
506 } |
|
507 return $filtered_items; |
|
508 } |
|
509 |
|
510 $mutated = false; |
|
511 $should_update = ( |
|
512 is_array( $this_item ) |
|
513 && |
|
514 $current_nav_menu_term_id === $menu->term_id |
|
515 ); |
|
516 if ( $should_update ) { |
|
517 foreach ( $items as $item ) { |
|
518 if ( $item->db_id === $this->post_id ) { |
|
519 foreach ( get_object_vars( $this->value_as_wp_post_nav_menu_item() ) as $key => $value ) { |
|
520 $item->$key = $value; |
|
521 } |
|
522 $mutated = true; |
|
523 } |
|
524 } |
|
525 |
|
526 // Not found so we have to append it.. |
|
527 if ( ! $mutated ) { |
|
528 $items[] = $this->value_as_wp_post_nav_menu_item(); |
|
529 } |
|
530 } |
|
531 |
|
532 return $items; |
|
533 } |
|
534 |
|
535 /** |
|
536 * Re-apply the tail logic also applied on $items by wp_get_nav_menu_items(). |
|
537 * |
|
538 * @since 4.3.0 |
|
539 * @static |
|
540 * |
|
541 * @see wp_get_nav_menu_items() |
|
542 * |
|
543 * @param array $items An array of menu item post objects. |
|
544 * @param object $menu The menu object. |
|
545 * @param array $args An array of arguments used to retrieve menu item objects. |
|
546 * @return array Array of menu items, |
|
547 */ |
|
548 public static function sort_wp_get_nav_menu_items( $items, $menu, $args ) { |
|
549 // @todo We should probably re-apply some constraints imposed by $args. |
|
550 unset( $args['include'] ); |
|
551 |
|
552 // Remove invalid items only in front end. |
|
553 if ( ! is_admin() ) { |
|
554 $items = array_filter( $items, '_is_valid_nav_menu_item' ); |
|
555 } |
|
556 |
|
557 if ( ARRAY_A === $args['output'] ) { |
|
558 $items = wp_list_sort( $items, array( |
|
559 $args['output_key'] => 'ASC', |
|
560 ) ); |
|
561 $i = 1; |
|
562 |
|
563 foreach ( $items as $k => $item ) { |
|
564 $items[ $k ]->{$args['output_key']} = $i++; |
|
565 } |
|
566 } |
|
567 |
|
568 return $items; |
|
569 } |
|
570 |
|
571 /** |
|
572 * Get the value emulated into a WP_Post and set up as a nav_menu_item. |
|
573 * |
|
574 * @since 4.3.0 |
|
575 * |
|
576 * @return WP_Post With wp_setup_nav_menu_item() applied. |
|
577 */ |
|
578 public function value_as_wp_post_nav_menu_item() { |
|
579 $item = (object) $this->value(); |
|
580 unset( $item->nav_menu_term_id ); |
|
581 |
|
582 $item->post_status = $item->status; |
|
583 unset( $item->status ); |
|
584 |
|
585 $item->post_type = 'nav_menu_item'; |
|
586 $item->menu_order = $item->position; |
|
587 unset( $item->position ); |
|
588 |
|
589 if ( empty( $item->original_title ) ) { |
|
590 $item->original_title = $this->get_original_title( $item ); |
|
591 } |
|
592 if ( empty( $item->title ) && ! empty( $item->original_title ) ) { |
|
593 $item->title = $item->original_title; |
|
594 } |
|
595 if ( $item->title ) { |
|
596 $item->post_title = $item->title; |
|
597 } |
|
598 |
|
599 $item->ID = $this->post_id; |
|
600 $item->db_id = $this->post_id; |
|
601 $post = new WP_Post( (object) $item ); |
|
602 |
|
603 if ( empty( $post->post_author ) ) { |
|
604 $post->post_author = get_current_user_id(); |
|
605 } |
|
606 |
|
607 if ( ! isset( $post->type_label ) ) { |
|
608 $post->type_label = $this->get_type_label( $post ); |
|
609 } |
|
610 |
|
611 // Ensure nav menu item URL is set according to linked object. |
|
612 if ( 'post_type' === $post->type && ! empty( $post->object_id ) ) { |
|
613 $post->url = get_permalink( $post->object_id ); |
|
614 } elseif ( 'taxonomy' === $post->type && ! empty( $post->object ) && ! empty( $post->object_id ) ) { |
|
615 $post->url = get_term_link( (int) $post->object_id, $post->object ); |
|
616 } elseif ( 'post_type_archive' === $post->type && ! empty( $post->object ) ) { |
|
617 $post->url = get_post_type_archive_link( $post->object ); |
|
618 } |
|
619 if ( is_wp_error( $post->url ) ) { |
|
620 $post->url = ''; |
|
621 } |
|
622 |
|
623 /** This filter is documented in wp-includes/nav-menu.php */ |
|
624 $post->attr_title = apply_filters( 'nav_menu_attr_title', $post->attr_title ); |
|
625 |
|
626 /** This filter is documented in wp-includes/nav-menu.php */ |
|
627 $post->description = apply_filters( 'nav_menu_description', wp_trim_words( $post->description, 200 ) ); |
|
628 |
|
629 /** This filter is documented in wp-includes/nav-menu.php */ |
|
630 $post = apply_filters( 'wp_setup_nav_menu_item', $post ); |
|
631 |
|
632 return $post; |
|
633 } |
|
634 |
|
635 /** |
|
636 * Sanitize an input. |
|
637 * |
|
638 * Note that parent::sanitize() erroneously does wp_unslash() on $value, but |
|
639 * we remove that in this override. |
|
640 * |
|
641 * @since 4.3.0 |
|
642 * |
|
643 * @param array $menu_item_value The value to sanitize. |
|
644 * @return array|false|null|WP_Error Null or WP_Error if an input isn't valid. False if it is marked for deletion. |
|
645 * Otherwise the sanitized value. |
|
646 */ |
|
647 public function sanitize( $menu_item_value ) { |
|
648 // Menu is marked for deletion. |
|
649 if ( false === $menu_item_value ) { |
|
650 return $menu_item_value; |
|
651 } |
|
652 |
|
653 // Invalid. |
|
654 if ( ! is_array( $menu_item_value ) ) { |
|
655 return null; |
|
656 } |
|
657 |
|
658 $default = array( |
|
659 'object_id' => 0, |
|
660 'object' => '', |
|
661 'menu_item_parent' => 0, |
|
662 'position' => 0, |
|
663 'type' => 'custom', |
|
664 'title' => '', |
|
665 'url' => '', |
|
666 'target' => '', |
|
667 'attr_title' => '', |
|
668 'description' => '', |
|
669 'classes' => '', |
|
670 'xfn' => '', |
|
671 'status' => 'publish', |
|
672 'original_title' => '', |
|
673 'nav_menu_term_id' => 0, |
|
674 '_invalid' => false, |
|
675 ); |
|
676 $menu_item_value = array_merge( $default, $menu_item_value ); |
|
677 $menu_item_value = wp_array_slice_assoc( $menu_item_value, array_keys( $default ) ); |
|
678 $menu_item_value['position'] = intval( $menu_item_value['position'] ); |
|
679 |
|
680 foreach ( array( 'object_id', 'menu_item_parent', 'nav_menu_term_id' ) as $key ) { |
|
681 // Note we need to allow negative-integer IDs for previewed objects not inserted yet. |
|
682 $menu_item_value[ $key ] = intval( $menu_item_value[ $key ] ); |
|
683 } |
|
684 |
|
685 foreach ( array( 'type', 'object', 'target' ) as $key ) { |
|
686 $menu_item_value[ $key ] = sanitize_key( $menu_item_value[ $key ] ); |
|
687 } |
|
688 |
|
689 foreach ( array( 'xfn', 'classes' ) as $key ) { |
|
690 $value = $menu_item_value[ $key ]; |
|
691 if ( ! is_array( $value ) ) { |
|
692 $value = explode( ' ', $value ); |
|
693 } |
|
694 $menu_item_value[ $key ] = implode( ' ', array_map( 'sanitize_html_class', $value ) ); |
|
695 } |
|
696 |
|
697 $menu_item_value['original_title'] = sanitize_text_field( $menu_item_value['original_title'] ); |
|
698 |
|
699 // Apply the same filters as when calling wp_insert_post(). |
|
700 |
|
701 /** This filter is documented in wp-includes/post.php */ |
|
702 $menu_item_value['title'] = wp_unslash( apply_filters( 'title_save_pre', wp_slash( $menu_item_value['title'] ) ) ); |
|
703 |
|
704 /** This filter is documented in wp-includes/post.php */ |
|
705 $menu_item_value['attr_title'] = wp_unslash( apply_filters( 'excerpt_save_pre', wp_slash( $menu_item_value['attr_title'] ) ) ); |
|
706 |
|
707 /** This filter is documented in wp-includes/post.php */ |
|
708 $menu_item_value['description'] = wp_unslash( apply_filters( 'content_save_pre', wp_slash( $menu_item_value['description'] ) ) ); |
|
709 |
|
710 if ( '' !== $menu_item_value['url'] ) { |
|
711 $menu_item_value['url'] = esc_url_raw( $menu_item_value['url'] ); |
|
712 if ( '' === $menu_item_value['url'] ) { |
|
713 return new WP_Error( 'invalid_url', __( 'Invalid URL.' ) ); // Fail sanitization if URL is invalid. |
|
714 } |
|
715 } |
|
716 if ( 'publish' !== $menu_item_value['status'] ) { |
|
717 $menu_item_value['status'] = 'draft'; |
|
718 } |
|
719 |
|
720 $menu_item_value['_invalid'] = (bool) $menu_item_value['_invalid']; |
|
721 |
|
722 /** This filter is documented in wp-includes/class-wp-customize-setting.php */ |
|
723 return apply_filters( "customize_sanitize_{$this->id}", $menu_item_value, $this ); |
|
724 } |
|
725 |
|
726 /** |
|
727 * Creates/updates the nav_menu_item post for this setting. |
|
728 * |
|
729 * Any created menu items will have their assigned post IDs exported to the client |
|
730 * via the {@see 'customize_save_response'} filter. Likewise, any errors will be |
|
731 * exported to the client via the customize_save_response() filter. |
|
732 * |
|
733 * To delete a menu, the client can send false as the value. |
|
734 * |
|
735 * @since 4.3.0 |
|
736 * |
|
737 * @see wp_update_nav_menu_item() |
|
738 * |
|
739 * @param array|false $value The menu item array to update. If false, then the menu item will be deleted |
|
740 * entirely. See WP_Customize_Nav_Menu_Item_Setting::$default for what the value |
|
741 * should consist of. |
|
742 * @return null|void |
|
743 */ |
|
744 protected function update( $value ) { |
|
745 if ( $this->is_updated ) { |
|
746 return; |
|
747 } |
|
748 |
|
749 $this->is_updated = true; |
|
750 $is_placeholder = ( $this->post_id < 0 ); |
|
751 $is_delete = ( false === $value ); |
|
752 |
|
753 // Update the cached value. |
|
754 $this->value = $value; |
|
755 |
|
756 add_filter( 'customize_save_response', array( $this, 'amend_customize_save_response' ) ); |
|
757 |
|
758 if ( $is_delete ) { |
|
759 // If the current setting post is a placeholder, a delete request is a no-op. |
|
760 if ( $is_placeholder ) { |
|
761 $this->update_status = 'deleted'; |
|
762 } else { |
|
763 $r = wp_delete_post( $this->post_id, true ); |
|
764 |
|
765 if ( false === $r ) { |
|
766 $this->update_error = new WP_Error( 'delete_failure' ); |
|
767 $this->update_status = 'error'; |
|
768 } else { |
|
769 $this->update_status = 'deleted'; |
|
770 } |
|
771 // @todo send back the IDs for all associated nav menu items deleted, so these settings (and controls) can be removed from Customizer? |
|
772 } |
|
773 } else { |
|
774 |
|
775 // Handle saving menu items for menus that are being newly-created. |
|
776 if ( $value['nav_menu_term_id'] < 0 ) { |
|
777 $nav_menu_setting_id = sprintf( 'nav_menu[%s]', $value['nav_menu_term_id'] ); |
|
778 $nav_menu_setting = $this->manager->get_setting( $nav_menu_setting_id ); |
|
779 |
|
780 if ( ! $nav_menu_setting || ! ( $nav_menu_setting instanceof WP_Customize_Nav_Menu_Setting ) ) { |
|
781 $this->update_status = 'error'; |
|
782 $this->update_error = new WP_Error( 'unexpected_nav_menu_setting' ); |
|
783 return; |
|
784 } |
|
785 |
|
786 if ( false === $nav_menu_setting->save() ) { |
|
787 $this->update_status = 'error'; |
|
788 $this->update_error = new WP_Error( 'nav_menu_setting_failure' ); |
|
789 return; |
|
790 } |
|
791 |
|
792 if ( $nav_menu_setting->previous_term_id !== intval( $value['nav_menu_term_id'] ) ) { |
|
793 $this->update_status = 'error'; |
|
794 $this->update_error = new WP_Error( 'unexpected_previous_term_id' ); |
|
795 return; |
|
796 } |
|
797 |
|
798 $value['nav_menu_term_id'] = $nav_menu_setting->term_id; |
|
799 } |
|
800 |
|
801 // Handle saving a nav menu item that is a child of a nav menu item being newly-created. |
|
802 if ( $value['menu_item_parent'] < 0 ) { |
|
803 $parent_nav_menu_item_setting_id = sprintf( 'nav_menu_item[%s]', $value['menu_item_parent'] ); |
|
804 $parent_nav_menu_item_setting = $this->manager->get_setting( $parent_nav_menu_item_setting_id ); |
|
805 |
|
806 if ( ! $parent_nav_menu_item_setting || ! ( $parent_nav_menu_item_setting instanceof WP_Customize_Nav_Menu_Item_Setting ) ) { |
|
807 $this->update_status = 'error'; |
|
808 $this->update_error = new WP_Error( 'unexpected_nav_menu_item_setting' ); |
|
809 return; |
|
810 } |
|
811 |
|
812 if ( false === $parent_nav_menu_item_setting->save() ) { |
|
813 $this->update_status = 'error'; |
|
814 $this->update_error = new WP_Error( 'nav_menu_item_setting_failure' ); |
|
815 return; |
|
816 } |
|
817 |
|
818 if ( $parent_nav_menu_item_setting->previous_post_id !== intval( $value['menu_item_parent'] ) ) { |
|
819 $this->update_status = 'error'; |
|
820 $this->update_error = new WP_Error( 'unexpected_previous_post_id' ); |
|
821 return; |
|
822 } |
|
823 |
|
824 $value['menu_item_parent'] = $parent_nav_menu_item_setting->post_id; |
|
825 } |
|
826 |
|
827 // Insert or update menu. |
|
828 $menu_item_data = array( |
|
829 'menu-item-object-id' => $value['object_id'], |
|
830 'menu-item-object' => $value['object'], |
|
831 'menu-item-parent-id' => $value['menu_item_parent'], |
|
832 'menu-item-position' => $value['position'], |
|
833 'menu-item-type' => $value['type'], |
|
834 'menu-item-title' => $value['title'], |
|
835 'menu-item-url' => $value['url'], |
|
836 'menu-item-description' => $value['description'], |
|
837 'menu-item-attr-title' => $value['attr_title'], |
|
838 'menu-item-target' => $value['target'], |
|
839 'menu-item-classes' => $value['classes'], |
|
840 'menu-item-xfn' => $value['xfn'], |
|
841 'menu-item-status' => $value['status'], |
|
842 ); |
|
843 |
|
844 $r = wp_update_nav_menu_item( |
|
845 $value['nav_menu_term_id'], |
|
846 $is_placeholder ? 0 : $this->post_id, |
|
847 wp_slash( $menu_item_data ) |
|
848 ); |
|
849 |
|
850 if ( is_wp_error( $r ) ) { |
|
851 $this->update_status = 'error'; |
|
852 $this->update_error = $r; |
|
853 } else { |
|
854 if ( $is_placeholder ) { |
|
855 $this->previous_post_id = $this->post_id; |
|
856 $this->post_id = $r; |
|
857 $this->update_status = 'inserted'; |
|
858 } else { |
|
859 $this->update_status = 'updated'; |
|
860 } |
|
861 } |
|
862 } |
|
863 |
|
864 } |
|
865 |
|
866 /** |
|
867 * Export data for the JS client. |
|
868 * |
|
869 * @since 4.3.0 |
|
870 * |
|
871 * @see WP_Customize_Nav_Menu_Item_Setting::update() |
|
872 * |
|
873 * @param array $data Additional information passed back to the 'saved' event on `wp.customize`. |
|
874 * @return array Save response data. |
|
875 */ |
|
876 public function amend_customize_save_response( $data ) { |
|
877 if ( ! isset( $data['nav_menu_item_updates'] ) ) { |
|
878 $data['nav_menu_item_updates'] = array(); |
|
879 } |
|
880 |
|
881 $data['nav_menu_item_updates'][] = array( |
|
882 'post_id' => $this->post_id, |
|
883 'previous_post_id' => $this->previous_post_id, |
|
884 'error' => $this->update_error ? $this->update_error->get_error_code() : null, |
|
885 'status' => $this->update_status, |
|
886 ); |
|
887 return $data; |
|
888 } |
|
889 } |