PluginProbe
Gutenberg / 8.3.0
Gutenberg v8.3.0
24.0.0 23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 12.6.0 All 403 releases
gutenberg / lib / class-wp-rest-menu-items-controller.php

class-wp-rest-menu-items-controller.php in Gutenberg 8.3.0, at lib/class-wp-rest-menu-items-controller.php

1,064 lines 36.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * REST API: WP_REST_Menu_Items_Controller class
4 *
5 * @package WordPress
6 * @subpackage REST_API
7 */
8
9 /**
10 * Core class to access nav items via the REST API.
11 *
12 * @see WP_REST_Posts_Controller
13 */
14 class WP_REST_Menu_Items_Controller extends WP_REST_Posts_Controller {
15 /**
16 * Constructor.
17 *
18 * @param string $post_type Post type.
19 */
20 public function __construct( $post_type ) {
21 parent::__construct( $post_type );
22 $this->namespace = '__experimental';
23 }
24
25 /**
26 * Get the post, if the ID is valid.
27 *
28 * @param int $id Supplied ID.
29 *
30 * @return object|WP_Error Post object if ID is valid, WP_Error otherwise.
31 */
32 protected function get_post( $id ) {
33 return $this->get_nav_menu_item( $id );
34 }
35
36 /**
37 * Get the nav menu item, if the ID is valid.
38 *
39 * @param int $id Supplied ID.
40 *
41 * @return object|WP_Error Post object if ID is valid, WP_Error otherwise.
42 */
43 protected function get_nav_menu_item( $id ) {
44 $post = parent::get_post( $id );
45 if ( is_wp_error( $post ) ) {
46 return $post;
47 }
48 $nav_item = wp_setup_nav_menu_item( $post );
49
50 return $nav_item;
51 }
52
53 /**
54 * Checks if a given request has access to read a menu item if they have access to edit them.
55 *
56 * @param WP_REST_Request $request Full details about the request.
57 * @return bool|WP_Error True if the request has read access for the item, WP_Error object otherwise.
58 */
59 public function get_item_permissions_check( $request ) {
60 $post = $this->get_post( $request['id'] );
61 if ( is_wp_error( $post ) ) {
62 return $post;
63 }
64 if ( $post && ! $this->check_update_permission( $post ) ) {
65 return new WP_Error( 'rest_cannot_view', __( 'Sorry, you cannot view this menu item, unless you have access to permission edit it. ', 'gutenberg' ), array( 'status' => rest_authorization_required_code() ) );
66 }
67
68 return parent::get_item_permissions_check( $request );
69 }
70
71 /**
72 * Checks if a given request has access to read menu items if they have access to edit them.
73 *
74 * @param WP_REST_Request $request Full details about the request.
75 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
76 */
77 public function get_items_permissions_check( $request ) {
78 $post_type = get_post_type_object( $this->post_type );
79 if ( ! current_user_can( $post_type->cap->edit_posts ) ) {
80 if ( 'edit' === $request['context'] ) {
81 return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to edit posts in this post type.', 'gutenberg' ), array( 'status' => rest_authorization_required_code() ) );
82 }
83 return new WP_Error( 'rest_cannot_view', __( 'Sorry, you cannot view these menu items, unless you have access to permission edit them. ', 'gutenberg' ), array( 'status' => rest_authorization_required_code() ) );
84 }
85 return true;
86 }
87
88 /**
89 * Creates a single post.
90 *
91 * @param WP_REST_Request $request Full details about the request.
92 *
93 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
94 */
95 public function create_item( $request ) {
96 if ( ! empty( $request['id'] ) ) {
97 return new WP_Error( 'rest_post_exists', __( 'Cannot create existing post.', 'gutenberg' ), array( 'status' => 400 ) );
98 }
99
100 $prepared_nav_item = $this->prepare_item_for_database( $request );
101
102 if ( is_wp_error( $prepared_nav_item ) ) {
103 return $prepared_nav_item;
104 }
105 $prepared_nav_item = (array) $prepared_nav_item;
106
107 $nav_menu_item_id = wp_update_nav_menu_item( $prepared_nav_item['menu-id'], $prepared_nav_item['menu-item-db-id'], $prepared_nav_item );
108 if ( is_wp_error( $nav_menu_item_id ) ) {
109 if ( 'db_insert_error' === $nav_menu_item_id->get_error_code() ) {
110 $nav_menu_item_id->add_data( array( 'status' => 500 ) );
111 } else {
112 $nav_menu_item_id->add_data( array( 'status' => 400 ) );
113 }
114
115 return $nav_menu_item_id;
116 }
117
118 $nav_menu_item = $this->get_nav_menu_item( $nav_menu_item_id );
119 if ( is_wp_error( $nav_menu_item ) ) {
120 $nav_menu_item->add_data( array( 'status' => 404 ) );
121
122 return $nav_menu_item;
123 }
124
125 /**
126 * Fires after a single nav menu item is created or updated via the REST API.
127 *
128 * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
129 *
130 * @param object $nav_menu_item Inserted or updated nav item object.
131 * @param WP_REST_Request $request Request object.
132 * @param bool $creating True when creating a post, false when updating.
133 * SA
134 */
135 do_action( "rest_insert_{$this->post_type}", $nav_menu_item, $request, true );
136
137 $schema = $this->get_item_schema();
138
139 if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
140 $meta_update = $this->meta->update_value( $request['meta'], $nav_menu_item_id );
141
142 if ( is_wp_error( $meta_update ) ) {
143 return $meta_update;
144 }
145 }
146
147 $nav_menu_item = $this->get_nav_menu_item( $nav_menu_item_id );
148 $fields_update = $this->update_additional_fields_for_object( $nav_menu_item, $request );
149
150 if ( is_wp_error( $fields_update ) ) {
151 return $fields_update;
152 }
153
154 $request->set_param( 'context', 'edit' );
155
156 /**
157 * Fires after a single nav menu item is completely created or updated via the REST API.
158 *
159 * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
160 *
161 * @param object $nav_menu_item Inserted or updated nav item object.
162 * @param WP_REST_Request $request Request object.
163 * @param bool $creating True when creating a post, false when updating.
164 */
165 do_action( "rest_after_insert_{$this->post_type}", $nav_menu_item, $request, true );
166
167 $response = $this->prepare_item_for_response( $nav_menu_item, $request );
168 $response = rest_ensure_response( $response );
169
170 $response->set_status( 201 );
171 $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $nav_menu_item_id ) ) );
172
173 return $response;
174 }
175
176 /**
177 * Updates a single nav menu item.
178 *
179 * @param WP_REST_Request $request Full details about the request.
180 *
181 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
182 */
183 public function update_item( $request ) {
184 $valid_check = $this->get_nav_menu_item( $request['id'] );
185 if ( is_wp_error( $valid_check ) ) {
186 return $valid_check;
187 }
188
189 $prepared_nav_item = $this->prepare_item_for_database( $request );
190
191 if ( is_wp_error( $prepared_nav_item ) ) {
192 return $prepared_nav_item;
193 }
194
195 $prepared_nav_item = (array) $prepared_nav_item;
196
197 $nav_menu_item_id = wp_update_nav_menu_item( $prepared_nav_item['menu-id'], $prepared_nav_item['menu-item-db-id'], $prepared_nav_item );
198
199 if ( is_wp_error( $nav_menu_item_id ) ) {
200 if ( 'db_update_error' === $nav_menu_item_id->get_error_code() ) {
201 $nav_menu_item_id->add_data( array( 'status' => 500 ) );
202 } else {
203 $nav_menu_item_id->add_data( array( 'status' => 400 ) );
204 }
205
206 return $nav_menu_item_id;
207 }
208
209 $nav_menu_item = $this->get_nav_menu_item( $nav_menu_item_id );
210 if ( is_wp_error( $nav_menu_item ) ) {
211 $nav_menu_item->add_data( array( 'status' => 404 ) );
212
213 return $nav_menu_item;
214 }
215
216 /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */
217 do_action( "rest_insert_{$this->post_type}", $nav_menu_item, $request, false );
218
219 $schema = $this->get_item_schema();
220
221 if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
222 $meta_update = $this->meta->update_value( $request['meta'], $nav_menu_item->ID );
223
224 if ( is_wp_error( $meta_update ) ) {
225 return $meta_update;
226 }
227 }
228
229 $nav_menu_item = $this->get_nav_menu_item( $nav_menu_item_id );
230 $fields_update = $this->update_additional_fields_for_object( $nav_menu_item, $request );
231
232 if ( is_wp_error( $fields_update ) ) {
233 return $fields_update;
234 }
235
236 $request->set_param( 'context', 'edit' );
237
238 /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */
239 do_action( "rest_after_insert_{$this->post_type}", $nav_menu_item, $request, false );
240
241 $response = $this->prepare_item_for_response( $nav_menu_item, $request );
242
243 return rest_ensure_response( $response );
244 }
245
246 /**
247 * Deletes a single menu item.
248 *
249 * @param WP_REST_Request $request Full details about the request.
250 * @return true|WP_Error True on success, or WP_Error object on failure.
251 */
252 public function delete_item( $request ) {
253 $menu_item = $this->get_nav_menu_item( $request['id'] );
254 if ( is_wp_error( $menu_item ) ) {
255 return $menu_item;
256 }
257
258 $force = isset( $request['force'] ) ? (bool) $request['force'] : false;
259
260 // We don't support trashing for menu items.
261 if ( ! $force ) {
262 /* translators: %s: force=true */
263 return new WP_Error( 'rest_trash_not_supported', sprintf( __( "Menu items do not support trashing. Set '%s' to delete.", 'gutenberg' ), 'force=true' ), array( 'status' => 501 ) );
264 }
265
266 $previous = $this->prepare_item_for_response( $menu_item, $request );
267
268 $result = wp_delete_post( $request['id'], true );
269
270 if ( ! $result ) {
271 return new WP_Error( 'rest_cannot_delete', __( 'The post cannot be deleted.', 'gutenberg' ), array( 'status' => 500 ) );
272 }
273
274 $response = new WP_REST_Response();
275 $response->set_data(
276 array(
277 'deleted' => true,
278 'previous' => $previous->get_data(),
279 )
280 );
281
282 /**
283 * Fires immediately after a single menu item is deleted or trashed via the REST API.
284 *
285 * They dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
286 *
287 * @param Object $menu_item The deleted or trashed menu item.
288 * @param WP_REST_Response $response The response data.
289 * @param WP_REST_Request $request The request sent to the API.
290 */
291 do_action( "rest_delete_{$this->post_type}", $menu_item, $response, $request );
292
293 return $response;
294 }
295
296 /**
297 * Prepares a single post for create or update.
298 *
299 * @param WP_REST_Request $request Request object.
300 *
301 * @return stdClass|WP_Error
302 */
303 protected function prepare_item_for_database( $request ) {
304 $menu_item_db_id = $request['id'];
305 $menu_item_obj = $this->get_nav_menu_item( $menu_item_db_id );
306 // Need to persist the menu item data. See https://core.trac.wordpress.org/ticket/28138 .
307 if ( ! is_wp_error( $menu_item_obj ) ) {
308 // Correct the menu position if this was the first item. See https://core.trac.wordpress.org/ticket/28140 .
309 $position = ( 0 === $menu_item_obj->menu_order ) ? 1 : $menu_item_obj->menu_order;
310
311 $prepared_nav_item = array(
312 'menu-item-db-id' => $menu_item_db_id,
313 'menu-item-object-id' => $menu_item_obj->object_id,
314 'menu-item-object' => $menu_item_obj->object,
315 'menu-item-parent-id' => $menu_item_obj->menu_item_parent,
316 'menu-item-position' => $position,
317 'menu-item-title' => $menu_item_obj->title,
318 'menu-item-url' => $menu_item_obj->url,
319 'menu-item-description' => $menu_item_obj->description,
320 'menu-item-attr-title' => $menu_item_obj->attr_title,
321 'menu-item-target' => $menu_item_obj->target,
322 // Stored in the database as a string.
323 'menu-item-classes' => implode( ' ', $menu_item_obj->classes ),
324 'menu-item-xfn' => $menu_item_obj->xfn,
325 'menu-item-status' => $menu_item_obj->post_status,
326 'menu-id' => $this->get_menu_id( $menu_item_db_id ),
327 );
328 } else {
329 $prepared_nav_item = array(
330 'menu-id' => 0,
331 'menu-item-db-id' => 0,
332 'menu-item-object-id' => 0,
333 'menu-item-object' => '',
334 'menu-item-parent-id' => 0,
335 'menu-item-position' => 0,
336 'menu-item-type' => 'custom',
337 'menu-item-title' => '',
338 'menu-item-url' => '',
339 'menu-item-description' => '',
340 'menu-item-attr-title' => '',
341 'menu-item-target' => '',
342 'menu-item-classes' => '',
343 'menu-item-xfn' => '',
344 'menu-item-status' => 'publish',
345 );
346 }
347
348 $mapping = array(
349 'menu-item-db-id' => 'id',
350 'menu-item-object-id' => 'object_id',
351 'menu-item-object' => 'object',
352 'menu-item-parent-id' => 'parent',
353 'menu-item-position' => 'menu_order',
354 'menu-item-type' => 'type',
355 'menu-item-url' => 'url',
356 'menu-item-description' => 'description',
357 'menu-item-attr-title' => 'attr_title',
358 'menu-item-target' => 'target',
359 'menu-item-classes' => 'classes',
360 'menu-item-xfn' => 'xfn',
361 'menu-item-status' => 'status',
362 );
363
364 $schema = $this->get_item_schema();
365
366 foreach ( $mapping as $original => $api_request ) {
367 if ( ! empty( $schema['properties'][ $api_request ] ) && isset( $request[ $api_request ] ) ) {
368 $check = rest_validate_value_from_schema( $request[ $api_request ], $schema['properties'][ $api_request ] );
369 if ( is_wp_error( $check ) ) {
370 $check->add_data( array( 'status' => 400 ) );
371 return $check;
372 }
373 $prepared_nav_item[ $original ] = rest_sanitize_value_from_schema( $request[ $api_request ], $schema['properties'][ $api_request ] );
374 }
375 }
376
377 $taxonomy = get_taxonomy( 'nav_menu' );
378 $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
379 // If menus submitted, cast to int.
380 if ( isset( $request[ $base ] ) && ! empty( $request[ $base ] ) ) {
381 $prepared_nav_item['menu-id'] = absint( $request[ $base ] );
382 }
383
384 // Nav menu title.
385 if ( ! empty( $schema['properties']['title'] ) && isset( $request['title'] ) ) {
386 if ( is_string( $request['title'] ) ) {
387 $prepared_nav_item['menu-item-title'] = $request['title'];
388 } elseif ( ! empty( $request['title']['raw'] ) ) {
389 $prepared_nav_item['menu-item-title'] = $request['title']['raw'];
390 }
391 }
392
393 // Check if object id exists before saving.
394 if ( ! $prepared_nav_item['menu-item-object'] ) {
395 // If taxonony, check if term exists.
396 if ( 'taxonomy' === $prepared_nav_item['menu-item-type'] ) {
397 $original = get_term( absint( $prepared_nav_item['menu-item-object-id'] ) );
398 if ( empty( $original ) || is_wp_error( $original ) ) {
399 return new WP_Error( 'rest_term_invalid_id', __( 'Invalid term ID.', 'gutenberg' ), array( 'status' => 400 ) );
400 }
401 $prepared_nav_item['menu-item-object'] = get_term_field( 'taxonomy', $original );
402
403 // If post, check if post object exists.
404 } elseif ( 'post_type' === $prepared_nav_item['menu-item-type'] ) {
405 $original = get_post( absint( $prepared_nav_item['menu-item-object-id'] ) );
406 if ( empty( $original ) ) {
407 return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post ID.', 'gutenberg' ), array( 'status' => 400 ) );
408 }
409 $prepared_nav_item['menu-item-object'] = get_post_type( $original );
410 }
411 }
412
413 // If post type archive, check if post type exists.
414 if ( 'post_type_archive' === $prepared_nav_item['menu-item-type'] ) {
415 $post_type = ( $prepared_nav_item['menu-item-object'] ) ? $prepared_nav_item['menu-item-object'] : false;
416 $original = get_post_type_object( $post_type );
417 if ( empty( $original ) ) {
418 return new WP_Error( 'rest_post_invalid_type', __( 'Invalid post type.', 'gutenberg' ), array( 'status' => 400 ) );
419 }
420 }
421
422 // Check if menu item is type custom, then title and url are required.
423 if ( 'custom' === $prepared_nav_item['menu-item-type'] ) {
424 if ( '' === $prepared_nav_item['menu-item-title'] ) {
425 return new WP_Error( 'rest_title_required', __( 'Title required if menu item of type custom.', 'gutenberg' ), array( 'status' => 400 ) );
426 }
427 if ( empty( $prepared_nav_item['menu-item-url'] ) ) {
428 return new WP_Error( 'rest_url_required', __( 'URL required if menu item of type custom.', 'gutenberg' ), array( 'status' => 400 ) );
429 }
430 }
431
432 // If menu id is set, valid the value of menu item position and parent id.
433 if ( ! empty( $prepared_nav_item['menu-id'] ) ) {
434 // Check if nav menu is valid.
435 if ( ! is_nav_menu( $prepared_nav_item['menu-id'] ) ) {
436 return new WP_Error( 'invalid_menu_id', __( 'Invalid menu ID.', 'gutenberg' ), array( 'status' => 400 ) );
437 }
438
439 // If menu item position is set to 0, insert as the last item in the existing menu.
440 $menu_items = wp_get_nav_menu_items( $prepared_nav_item['menu-id'], array( 'post_status' => 'publish,draft' ) );
441 if ( 0 === (int) $prepared_nav_item['menu-item-position'] ) {
442 if ( $menu_items ) {
443 $last_item = $menu_items[ count( $menu_items ) - 1 ];
444 if ( $last_item && isset( $last_item->menu_order ) ) {
445 $prepared_nav_item['menu-item-position'] = $last_item->menu_order + 1;
446 } else {
447 $prepared_nav_item['menu-item-position'] = count( $menu_items ) - 1;
448 }
449 array_push( $menu_items, $last_item );
450 } else {
451 $prepared_nav_item['menu-item-position'] = 1;
452 }
453 }
454
455 // Check if existing menu position is already in use by another menu item.
456 $menu_item_ids = array();
457 foreach ( $menu_items as $menu_item ) {
458 $menu_item_ids[] = $menu_item->ID;
459 if ( $menu_item->ID !== (int) $menu_item_db_id ) {
460 if ( (int) $prepared_nav_item['menu-item-position'] === (int) $menu_item->menu_order ) {
461 return new WP_Error( 'invalid_menu_order', __( 'Invalid menu position.', 'gutenberg' ), array( 'status' => 400 ) );
462 }
463 }
464 }
465
466 // Check if valid parent id is valid nav menu item in menu.
467 if ( $prepared_nav_item['menu-item-parent-id'] ) {
468 if ( ! is_nav_menu_item( $prepared_nav_item['menu-item-parent-id'] ) ) {
469 return new WP_Error( 'invalid_menu_item_parent', __( 'Invalid menu item parent.', 'gutenberg' ), array( 'status' => 400 ) );
470 }
471 if ( ! $menu_item_ids || ! in_array( $prepared_nav_item['menu-item-parent-id'], $menu_item_ids, true ) ) {
472 return new WP_Error( 'invalid_item_parent', __( 'Invalid menu item parent.', 'gutenberg' ), array( 'status' => 400 ) );
473 }
474 }
475 }
476
477 foreach ( array( 'menu-item-object-id', 'menu-item-parent-id' ) as $key ) {
478 // Note we need to allow negative-integer IDs for previewed objects not inserted yet.
479 $prepared_nav_item[ $key ] = intval( $prepared_nav_item[ $key ] );
480 }
481
482 foreach ( array( 'menu-item-type', 'menu-item-object', 'menu-item-target' ) as $key ) {
483 $prepared_nav_item[ $key ] = sanitize_key( $prepared_nav_item[ $key ] );
484 }
485
486 // Valid xfn and classes are an array.
487 foreach ( array( 'menu-item-xfn', 'menu-item-classes' ) as $key ) {
488 $value = $prepared_nav_item[ $key ];
489 if ( ! is_array( $value ) ) {
490 $value = wp_parse_list( $value );
491 }
492 $prepared_nav_item[ $key ] = implode( ' ', array_map( 'sanitize_html_class', $value ) );
493 }
494
495 // Apply the same filters as when calling wp_insert_post().
496
497 /** This filter is documented in wp-includes/post.php */
498 $prepared_nav_item['menu-item-title'] = wp_unslash( apply_filters( 'title_save_pre', wp_slash( $prepared_nav_item['menu-item-title'] ) ) );
499
500 /** This filter is documented in wp-includes/post.php */
501 $prepared_nav_item['menu-item-attr-title'] = wp_unslash( apply_filters( 'excerpt_save_pre', wp_slash( $prepared_nav_item['menu-item-attr-title'] ) ) );
502
503 /** This filter is documented in wp-includes/post.php */
504 $prepared_nav_item['menu-item-description'] = wp_unslash( apply_filters( 'content_save_pre', wp_slash( $prepared_nav_item['menu-item-description'] ) ) );
505
506 // Valid url.
507 if ( '' !== $prepared_nav_item['menu-item-url'] ) {
508 $prepared_nav_item['menu-item-url'] = esc_url_raw( $prepared_nav_item['menu-item-url'] );
509 if ( '' === $prepared_nav_item['menu-item-url'] ) {
510 // Fail sanitization if URL is invalid.
511 return new WP_Error( 'invalid_url', __( 'Invalid URL.', 'gutenberg' ), array( 'status' => 400 ) );
512 }
513 }
514 // Only draft / publish are valid post status for menu items.
515 if ( 'publish' !== $prepared_nav_item['menu-item-status'] ) {
516 $prepared_nav_item['menu-item-status'] = 'draft';
517 }
518
519 $prepared_nav_item = (object) $prepared_nav_item;
520
521 /**
522 * Filters a post before it is inserted via the REST API.
523 *
524 * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
525 *
526 * @param stdClass $prepared_post An object representing a single post prepared
527 * for inserting or updating the database.
528 * @param WP_REST_Request $request Request object.
529 */
530 return apply_filters( "rest_pre_insert_{$this->post_type}", $prepared_nav_item, $request );
531 }
532
533 /**
534 * Prepares a single post output for response.
535 *
536 * @param object $post Post object.
537 * @param WP_REST_Request $request Request object.
538 *
539 * @return WP_REST_Response Response object.
540 */
541 public function prepare_item_for_response( $post, $request ) {
542 $fields = $this->get_fields_for_response( $request );
543
544 // Base fields for every post.
545 $menu_item = wp_setup_nav_menu_item( $post );
546 $data = array();
547 if ( in_array( 'id', $fields, true ) ) {
548 $data['id'] = $menu_item->ID;
549 }
550
551 if ( in_array( 'title', $fields, true ) ) {
552 add_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
553
554 $data['title'] = array(
555 'raw' => $menu_item->post_title,
556 'rendered' => $menu_item->title,
557 );
558
559 remove_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
560 }
561
562 if ( in_array( 'status', $fields, true ) ) {
563 $data['status'] = $menu_item->post_status;
564 }
565
566 if ( in_array( 'url', $fields, true ) ) {
567 $data['url'] = $menu_item->url;
568 }
569
570 if ( in_array( 'attr_title', $fields, true ) ) {
571 // Same as post_excerpt.
572 $data['attr_title'] = $menu_item->attr_title;
573 }
574
575 if ( in_array( 'description', $fields, true ) ) {
576 // Same as post_content.
577 $data['description'] = $menu_item->description;
578 }
579
580 if ( in_array( 'type', $fields, true ) ) {
581 // Using 'item_type' since 'type' already exists.
582 $data['type'] = $menu_item->type;
583 }
584
585 if ( in_array( 'type_label', $fields, true ) ) {
586 // Using 'item_type_label' to match up with 'item_type' - IS READ ONLY!
587 $data['type_label'] = $menu_item->type_label;
588 }
589
590 if ( in_array( 'object', $fields, true ) ) {
591 $data['object'] = $menu_item->object;
592 }
593
594 if ( in_array( 'object_id', $fields, true ) ) {
595 // Usually is a string, but lets expose as an integer.
596 $data['object_id'] = absint( $menu_item->object_id );
597 }
598
599 if ( in_array( 'parent', $fields, true ) ) {
600 // Same as post_parent, expose as integer.
601 $data['parent'] = absint( $menu_item->menu_item_parent );
602 }
603
604 if ( in_array( 'menu_order', $fields, true ) ) {
605 // Same as post_parent, expose as integer.
606 $data['menu_order'] = absint( $menu_item->menu_order );
607 }
608
609 if ( in_array( 'menu_id', $fields, true ) ) {
610 $data['menu_id'] = $this->get_menu_id( $menu_item->ID );
611 }
612
613 if ( in_array( 'target', $fields, true ) ) {
614 $data['target'] = $menu_item->target;
615 }
616
617 if ( in_array( 'classes', $fields, true ) ) {
618 $data['classes'] = (array) $menu_item->classes;
619 }
620
621 if ( in_array( 'xfn', $fields, true ) ) {
622 $data['xfn'] = array_map( 'sanitize_html_class', explode( ' ', $menu_item->xfn ) );
623 }
624
625 if ( in_array( 'meta', $fields, true ) ) {
626 $data['meta'] = $this->meta->get_value( $menu_item->ID, $request );
627 }
628
629 $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
630
631 foreach ( $taxonomies as $taxonomy ) {
632 $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
633
634 if ( in_array( $base, $fields, true ) ) {
635 $terms = get_the_terms( $post, $taxonomy->name );
636 $data[ $base ] = $terms ? array_values( wp_list_pluck( $terms, 'term_id' ) ) : array();
637 }
638 }
639
640 $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
641 $data = $this->add_additional_fields_to_object( $data, $request );
642 $data = $this->filter_response_by_context( $data, $context );
643
644 // Wrap the data in a response object.
645 $response = rest_ensure_response( $data );
646
647 $links = $this->prepare_links( $menu_item );
648 $response->add_links( $links );
649
650 if ( ! empty( $links['self']['href'] ) ) {
651 $actions = $this->get_available_actions( $menu_item, $request );
652
653 $self = $links['self']['href'];
654
655 foreach ( $actions as $rel ) {
656 $response->add_link( $rel, $self );
657 }
658 }
659
660 /**
661 * Filters the post data for a response.
662 *
663 * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
664 *
665 * @param WP_REST_Response $response The response object.
666 * @param object $post Post object.
667 * @param WP_REST_Request $request Request object.
668 */
669 return apply_filters( "rest_prepare_{$this->post_type}", $response, $post, $request );
670 }
671
672 /**
673 * Prepares links for the request.
674 *
675 * @param object $menu_item Menu object.
676 *
677 * @return array Links for the given post.
678 */
679 protected function prepare_links( $menu_item ) {
680 $links = parent::prepare_links( $menu_item );
681
682 if ( 'post_type' === $menu_item->type && ! empty( $menu_item->object_id ) ) {
683 $post_type_object = get_post_type_object( $menu_item->object );
684 if ( $post_type_object->show_in_rest ) {
685 $rest_base = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name;
686 $url = rest_url( sprintf( 'wp/v2/%s/%d', $rest_base, $menu_item->object_id ) );
687 $links['https://api.w.org/object'][] = array(
688 'href' => $url,
689 'post_type' => $menu_item->type,
690 'embeddable' => true,
691 );
692 }
693 } elseif ( 'taxonomy' === $menu_item->type && ! empty( $menu_item->object_id ) ) {
694 $taxonomy_object = get_taxonomy( $menu_item->object );
695 if ( $taxonomy_object->show_in_rest ) {
696 $rest_base = ! empty( $taxonomy_object->rest_base ) ? $taxonomy_object->rest_base : $taxonomy_object->name;
697 $url = rest_url( sprintf( 'wp/v2/%s/%d', $rest_base, $menu_item->object_id ) );
698 $links['https://api.w.org/object'][] = array(
699 'href' => $url,
700 'taxonomy' => $menu_item->type,
701 'embeddable' => true,
702 );
703 }
704 }
705
706 return $links;
707 }
708
709 /**
710 * Retrieve Link Description Objects that should be added to the Schema for the posts collection.
711 *
712 * @return array
713 */
714 protected function get_schema_links() {
715 $links = parent::get_schema_links();
716 $href = rest_url( "{$this->namespace}/{$this->rest_base}/{id}" );
717 $links[] = array(
718 'rel' => 'https://api.w.org/object',
719 'title' => __( 'Get linked object.', 'gutenberg' ),
720 'href' => $href,
721 'targetSchema' => array(
722 'type' => 'object',
723 'properties' => array(
724 'object' => array(
725 'type' => 'integer',
726 ),
727 ),
728 ),
729 );
730
731 return $links;
732 }
733
734 /**
735 * Retrieves the term's schema, conforming to JSON Schema.
736 *
737 * @return array Item schema data.
738 */
739 public function get_item_schema() {
740 $schema = array(
741 '$schema' => 'http://json-schema.org/draft-04/schema#',
742 'title' => $this->post_type,
743 'type' => 'object',
744 );
745
746 $schema['properties']['title'] = array(
747 'description' => __( 'The title for the object.', 'gutenberg' ),
748 'type' => 'object',
749 'context' => array( 'view', 'edit', 'embed' ),
750 'arg_options' => array(
751 // Note: sanitization implemented in self::prepare_item_for_database().
752 'sanitize_callback' => null,
753 // Note: validation implemented in self::prepare_item_for_database().
754 'validate_callback' => null,
755 ),
756 'properties' => array(
757 'raw' => array(
758 'description' => __( 'Title for the object, as it exists in the database.', 'gutenberg' ),
759 'type' => 'string',
760 'context' => array( 'edit' ),
761 ),
762 'rendered' => array(
763 'description' => __( 'HTML title for the object, transformed for display.', 'gutenberg' ),
764 'type' => 'string',
765 'context' => array( 'view', 'edit', 'embed' ),
766 'readonly' => true,
767 ),
768 ),
769 );
770
771 $schema['properties']['id'] = array(
772 'description' => __( 'Unique identifier for the object.', 'gutenberg' ),
773 'type' => 'integer',
774 'default' => 0,
775 'minimum' => 0,
776 'context' => array( 'view', 'edit', 'embed' ),
777 'readonly' => true,
778 );
779
780 $schema['properties']['type_label'] = array(
781 'description' => __( 'Name of type.', 'gutenberg' ),
782 'type' => 'string',
783 'context' => array( 'view', 'edit', 'embed' ),
784 'readonly' => true,
785 );
786
787 $schema['properties']['type'] = array(
788 'description' => __( 'The family of objects originally represented, such as "post_type" or "taxonomy".', 'gutenberg' ),
789 'type' => 'string',
790 'enum' => array( 'taxonomy', 'post_type', 'post_type_archive', 'custom' ),
791 'context' => array( 'view', 'edit', 'embed' ),
792 'default' => 'custom',
793 );
794
795 $schema['properties']['status'] = array(
796 'description' => __( 'A named status for the object.', 'gutenberg' ),
797 'type' => 'string',
798 'enum' => array_keys( get_post_stati( array( 'internal' => false ) ) ),
799 'default' => 'publish',
800 'context' => array( 'view', 'edit', 'embed' ),
801 );
802
803 $schema['properties']['parent'] = array(
804 'description' => __( 'The ID for the parent of the object.', 'gutenberg' ),
805 'type' => 'integer',
806 'minimum' => 0,
807 'default' => 0,
808 'context' => array( 'view', 'edit', 'embed' ),
809 );
810
811 $schema['properties']['attr_title'] = array(
812 'description' => __( 'Text for the title attribute of the link element for this menu item.', 'gutenberg' ),
813 'type' => 'string',
814 'context' => array( 'view', 'edit', 'embed' ),
815 'arg_options' => array(
816 'sanitize_callback' => 'sanitize_text_field',
817 ),
818 );
819
820 $schema['properties']['classes'] = array(
821 'description' => __( 'Class names for the link element of this menu item.', 'gutenberg' ),
822 'type' => 'array',
823 'items' => array(
824 'type' => 'string',
825 ),
826 'context' => array( 'view', 'edit', 'embed' ),
827 'arg_options' => array(
828 'sanitize_callback' => function ( $value ) {
829 return array_map( 'sanitize_html_class', wp_parse_list( $value ) );
830 },
831 ),
832 );
833
834 $schema['properties']['description'] = array(
835 'description' => __( 'The description of this menu item.', 'gutenberg' ),
836 'type' => 'string',
837 'context' => array( 'view', 'edit', 'embed' ),
838 'arg_options' => array(
839 'sanitize_callback' => 'sanitize_text_field',
840 ),
841 );
842
843 $schema['properties']['menu_order'] = array(
844 'description' => __( 'The DB ID of the nav_menu_item that is this item\'s menu parent, if any, otherwise 0.', 'gutenberg' ),
845 'context' => array( 'view', 'edit', 'embed' ),
846 'type' => 'integer',
847 'minimum' => 0,
848 'default' => 0,
849 );
850 $schema['properties']['object'] = array(
851 'description' => __( 'The type of object originally represented, such as "category," "post", or "attachment."', 'gutenberg' ),
852 'context' => array( 'view', 'edit', 'embed' ),
853 'type' => 'string',
854 );
855
856 $schema['properties']['object_id'] = array(
857 'description' => __( 'The DB ID of the original object this menu item represents, e . g . ID for posts and term_id for categories.', 'gutenberg' ),
858 'context' => array( 'view', 'edit', 'embed' ),
859 'type' => 'integer',
860 'minimum' => 0,
861 'default' => 0,
862 );
863
864 $schema['properties']['target'] = array(
865 'description' => __( 'The target attribute of the link element for this menu item.', 'gutenberg' ),
866 'type' => 'string',
867 'context' => array( 'view', 'edit', 'embed' ),
868 'enum' => array(
869 '_blank',
870 '',
871 ),
872 );
873
874 $schema['properties']['type_label'] = array(
875 'description' => __( 'The singular label used to describe this type of menu item.', 'gutenberg' ),
876 'context' => array( 'view', 'edit', 'embed' ),
877 'type' => 'string',
878 'readonly' => true,
879 );
880
881 $schema['properties']['url'] = array(
882 'description' => __( 'The URL to which this menu item points.', 'gutenberg' ),
883 'type' => 'string',
884 'format' => 'uri',
885 'context' => array( 'view', 'edit', 'embed' ),
886 );
887
888 $schema['properties']['xfn'] = array(
889 'description' => __( 'The XFN relationship expressed in the link of this menu item.', 'gutenberg' ),
890 'type' => 'array',
891 'items' => array(
892 'type' => 'string',
893 ),
894 'context' => array( 'view', 'edit', 'embed' ),
895 'arg_options' => array(
896 'sanitize_callback' => function ( $value ) {
897 return array_map( 'sanitize_html_class', wp_parse_list( $value ) );
898 },
899 ),
900 );
901
902 $schema['properties']['_invalid'] = array(
903 'description' => __( 'Whether the menu item represents an object that no longer exists.', 'gutenberg' ),
904 'context' => array( 'view', 'edit', 'embed' ),
905 'type' => 'boolean',
906 'readonly' => true,
907 );
908
909 $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
910
911 foreach ( $taxonomies as $taxonomy ) {
912 $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
913 $schema['properties'][ $base ] = array(
914 /* translators: %s: taxonomy name */
915 'description' => sprintf( __( 'The terms assigned to the object in the %s taxonomy.', 'gutenberg' ), $taxonomy->name ),
916 'type' => 'array',
917 'items' => array(
918 'type' => 'integer',
919 ),
920 'context' => array( 'view', 'edit' ),
921 );
922
923 if ( 'nav_menu' === $taxonomy->name ) {
924 $schema['properties'][ $base ]['type'] = 'integer';
925 unset( $schema['properties'][ $base ]['items'] );
926 }
927 }
928
929 $schema['properties']['meta'] = $this->meta->get_field_schema();
930
931 $schema_links = $this->get_schema_links();
932
933 if ( $schema_links ) {
934 $schema['links'] = $schema_links;
935 }
936
937 return $this->add_additional_fields_schema( $schema );
938 }
939
940 /**
941 * Retrieves the query params for the posts collection.
942 *
943 * @return array Collection parameters.
944 */
945 public function get_collection_params() {
946 $query_params = parent::get_collection_params();
947
948 $query_params['menu_order'] = array(
949 'description' => __( 'Limit result set to posts with a specific menu_order value.', 'gutenberg' ),
950 'type' => 'integer',
951 );
952
953 $query_params['order'] = array(
954 'description' => __( 'Order sort attribute ascending or descending.', 'gutenberg' ),
955 'type' => 'string',
956 'default' => 'asc',
957 'enum' => array( 'asc', 'desc' ),
958 );
959
960 $query_params['orderby'] = array(
961 'description' => __( 'Sort collection by object attribute.', 'gutenberg' ),
962 'type' => 'string',
963 'default' => 'menu_order',
964 'enum' => array(
965 'author',
966 'date',
967 'id',
968 'include',
969 'modified',
970 'parent',
971 'relevance',
972 'slug',
973 'include_slugs',
974 'title',
975 'menu_order',
976 ),
977 );
978
979 return $query_params;
980 }
981
982 /**
983 * Determines the allowed query_vars for a get_items() response and prepares
984 * them for WP_Query.
985 *
986 * @param array $prepared_args Optional. Prepared WP_Query arguments. Default empty array.
987 * @param WP_REST_Request $request Optional. Full details about the request.
988 *
989 * @return array Items query arguments.
990 */
991 protected function prepare_items_query( $prepared_args = array(), $request = null ) {
992 $query_args = parent::prepare_items_query( $prepared_args, $request );
993
994 // Map to proper WP_Query orderby param.
995 if ( isset( $query_args['orderby'] ) && isset( $request['orderby'] ) ) {
996 $orderby_mappings = array(
997 'id' => 'ID',
998 'include' => 'post__in',
999 'slug' => 'post_name',
1000 'include_slugs' => 'post_name__in',
1001 'menu_order' => 'menu_order',
1002 );
1003
1004 if ( isset( $orderby_mappings[ $request['orderby'] ] ) ) {
1005 $query_args['orderby'] = $orderby_mappings[ $request['orderby'] ];
1006 }
1007 }
1008
1009 return $query_args;
1010 }
1011
1012 /**
1013 * Checks whether current user can assign all terms sent with the current request.
1014 *
1015 * @param WP_REST_Request $request The request object with post and terms data.
1016 *
1017 * @return bool Whether the current user can assign the provided terms.
1018 */
1019 protected function check_assign_terms_permission( $request ) {
1020 $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
1021 foreach ( $taxonomies as $taxonomy ) {
1022 $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
1023
1024 if ( ! isset( $request[ $base ] ) ) {
1025 continue;
1026 }
1027
1028 foreach ( (array) $request[ $base ] as $term_id ) {
1029 if ( ! $term_id ) {
1030 continue;
1031 }
1032
1033 // Invalid terms will be rejected later.
1034 if ( ! get_term( $term_id, $taxonomy->name ) ) {
1035 continue;
1036 };
1037
1038 if ( ! current_user_can( 'assign_term', (int) $term_id ) ) {
1039 return false;
1040 }
1041 }
1042 }
1043
1044 return true;
1045 }
1046
1047 /**
1048 * Get menu id of current menu item.
1049 *
1050 * @param int $menu_item_id Menu item id.
1051 *
1052 * @return int
1053 */
1054 protected function get_menu_id( $menu_item_id ) {
1055 $menu_ids = wp_get_post_terms( $menu_item_id, 'nav_menu', array( 'fields' => 'ids' ) );
1056 $menu_id = 0;
1057 if ( $menu_ids && ! is_wp_error( $menu_ids ) ) {
1058 $menu_id = array_shift( $menu_ids );
1059 }
1060
1061 return $menu_id;
1062 }
1063 }
1064