PluginProbe
Gutenberg / 8.9.2
Gutenberg v8.9.2
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 7.4.0 All 402 releases
gutenberg / lib / class-wp-rest-menu-items-controller.php

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

1,125 lines 39.0 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-content' => $menu_item_obj->menu_item_content,
321 'menu-item-attr-title' => $menu_item_obj->attr_title,
322 'menu-item-target' => $menu_item_obj->target,
323 // Stored in the database as a string.
324 'menu-item-classes' => implode( ' ', $menu_item_obj->classes ),
325 'menu-item-xfn' => $menu_item_obj->xfn,
326 'menu-item-status' => $menu_item_obj->post_status,
327 'menu-id' => $this->get_menu_id( $menu_item_db_id ),
328 );
329 } else {
330 $prepared_nav_item = array(
331 'menu-id' => 0,
332 'menu-item-db-id' => 0,
333 'menu-item-object-id' => 0,
334 'menu-item-object' => '',
335 'menu-item-parent-id' => 0,
336 'menu-item-position' => 0,
337 'menu-item-type' => 'custom',
338 'menu-item-title' => '',
339 'menu-item-url' => '',
340 'menu-item-description' => '',
341 'menu-item-content' => '',
342 'menu-item-attr-title' => '',
343 'menu-item-target' => '',
344 'menu-item-classes' => '',
345 'menu-item-xfn' => '',
346 'menu-item-status' => 'publish',
347 );
348 }
349
350 $mapping = array(
351 'menu-item-db-id' => 'id',
352 'menu-item-object-id' => 'object_id',
353 'menu-item-object' => 'object',
354 'menu-item-parent-id' => 'parent',
355 'menu-item-position' => 'menu_order',
356 'menu-item-type' => 'type',
357 'menu-item-url' => 'url',
358 'menu-item-description' => 'description',
359 'menu-item-attr-title' => 'attr_title',
360 'menu-item-target' => 'target',
361 'menu-item-classes' => 'classes',
362 'menu-item-xfn' => 'xfn',
363 'menu-item-status' => 'status',
364 );
365
366 $schema = $this->get_item_schema();
367
368 foreach ( $mapping as $original => $api_request ) {
369 if ( ! empty( $schema['properties'][ $api_request ] ) && isset( $request[ $api_request ] ) ) {
370 $check = rest_validate_value_from_schema( $request[ $api_request ], $schema['properties'][ $api_request ] );
371 if ( is_wp_error( $check ) ) {
372 $check->add_data( array( 'status' => 400 ) );
373 return $check;
374 }
375 $prepared_nav_item[ $original ] = rest_sanitize_value_from_schema( $request[ $api_request ], $schema['properties'][ $api_request ] );
376 }
377 }
378
379 $taxonomy = get_taxonomy( 'nav_menu' );
380 $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
381 // If menus submitted, cast to int.
382 if ( isset( $request[ $base ] ) && ! empty( $request[ $base ] ) ) {
383 $prepared_nav_item['menu-id'] = absint( $request[ $base ] );
384 }
385
386 // Nav menu title.
387 if ( ! empty( $schema['properties']['title'] ) && isset( $request['title'] ) ) {
388 if ( is_string( $request['title'] ) ) {
389 $prepared_nav_item['menu-item-title'] = $request['title'];
390 } elseif ( ! empty( $request['title']['raw'] ) ) {
391 $prepared_nav_item['menu-item-title'] = $request['title']['raw'];
392 }
393 }
394
395 // Nav menu content.
396 if ( ! empty( $schema['properties']['content'] ) && isset( $request['content'] ) ) {
397 if ( is_string( $request['content'] ) ) {
398 $prepared_nav_item['menu-item-content'] = $request['content'];
399 } elseif ( isset( $request['content']['raw'] ) ) {
400 $prepared_nav_item['menu-item-content'] = $request['content']['raw'];
401 }
402 }
403
404 // Check if object id exists before saving.
405 if ( ! $prepared_nav_item['menu-item-object'] ) {
406 // If taxonony, check if term exists.
407 if ( 'taxonomy' === $prepared_nav_item['menu-item-type'] ) {
408 $original = get_term( absint( $prepared_nav_item['menu-item-object-id'] ) );
409 if ( empty( $original ) || is_wp_error( $original ) ) {
410 return new WP_Error( 'rest_term_invalid_id', __( 'Invalid term ID.', 'gutenberg' ), array( 'status' => 400 ) );
411 }
412 $prepared_nav_item['menu-item-object'] = get_term_field( 'taxonomy', $original );
413
414 // If post, check if post object exists.
415 } elseif ( 'post_type' === $prepared_nav_item['menu-item-type'] ) {
416 $original = get_post( absint( $prepared_nav_item['menu-item-object-id'] ) );
417 if ( empty( $original ) ) {
418 return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post ID.', 'gutenberg' ), array( 'status' => 400 ) );
419 }
420 $prepared_nav_item['menu-item-object'] = get_post_type( $original );
421 }
422 }
423
424 // If post type archive, check if post type exists.
425 if ( 'post_type_archive' === $prepared_nav_item['menu-item-type'] ) {
426 $post_type = ( $prepared_nav_item['menu-item-object'] ) ? $prepared_nav_item['menu-item-object'] : false;
427 $original = get_post_type_object( $post_type );
428 if ( empty( $original ) ) {
429 return new WP_Error( 'rest_post_invalid_type', __( 'Invalid post type.', 'gutenberg' ), array( 'status' => 400 ) );
430 }
431 }
432
433 // Check if menu item is type custom, then title and url are required.
434 if ( 'custom' === $prepared_nav_item['menu-item-type'] ) {
435 if ( '' === $prepared_nav_item['menu-item-title'] ) {
436 return new WP_Error( 'rest_title_required', __( 'Title required if menu item of type custom.', 'gutenberg' ), array( 'status' => 400 ) );
437 }
438 if ( empty( $prepared_nav_item['menu-item-url'] ) ) {
439 return new WP_Error( 'rest_url_required', __( 'URL required if menu item of type custom.', 'gutenberg' ), array( 'status' => 400 ) );
440 }
441 }
442
443 // If menu item is type block, then content is required.
444 if ( 'block' === $prepared_nav_item['menu-item-type'] ) {
445 if ( empty( $prepared_nav_item['menu-item-content'] ) ) {
446 return new WP_Error( 'rest_content_required', __( 'Content required if menu item of type block.', 'gutenberg' ), array( 'status' => 400 ) );
447 }
448 }
449
450 // If menu id is set, valid the value of menu item position and parent id.
451 if ( ! empty( $prepared_nav_item['menu-id'] ) ) {
452 // Check if nav menu is valid.
453 if ( ! is_nav_menu( $prepared_nav_item['menu-id'] ) ) {
454 return new WP_Error( 'invalid_menu_id', __( 'Invalid menu ID.', 'gutenberg' ), array( 'status' => 400 ) );
455 }
456
457 // If menu item position is set to 0, insert as the last item in the existing menu.
458 $menu_items = wp_get_nav_menu_items( $prepared_nav_item['menu-id'], array( 'post_status' => 'publish,draft' ) );
459 if ( 0 === (int) $prepared_nav_item['menu-item-position'] ) {
460 if ( $menu_items ) {
461 $last_item = $menu_items[ count( $menu_items ) - 1 ];
462 if ( $last_item && isset( $last_item->menu_order ) ) {
463 $prepared_nav_item['menu-item-position'] = $last_item->menu_order + 1;
464 } else {
465 $prepared_nav_item['menu-item-position'] = count( $menu_items ) - 1;
466 }
467 array_push( $menu_items, $last_item );
468 } else {
469 $prepared_nav_item['menu-item-position'] = 1;
470 }
471 }
472
473 // Check if existing menu position is already in use by another menu item.
474 $menu_item_ids = array();
475 foreach ( $menu_items as $menu_item ) {
476 $menu_item_ids[] = $menu_item->ID;
477 if ( $menu_item->ID !== (int) $menu_item_db_id ) {
478 if ( (int) $prepared_nav_item['menu-item-position'] === (int) $menu_item->menu_order ) {
479 return new WP_Error( 'invalid_menu_order', __( 'Invalid menu position.', 'gutenberg' ), array( 'status' => 400 ) );
480 }
481 }
482 }
483
484 // Check if valid parent id is valid nav menu item in menu.
485 if ( $prepared_nav_item['menu-item-parent-id'] ) {
486 if ( ! is_nav_menu_item( $prepared_nav_item['menu-item-parent-id'] ) ) {
487 return new WP_Error( 'invalid_menu_item_parent', __( 'Invalid menu item parent.', 'gutenberg' ), array( 'status' => 400 ) );
488 }
489 if ( ! $menu_item_ids || ! in_array( $prepared_nav_item['menu-item-parent-id'], $menu_item_ids, true ) ) {
490 return new WP_Error( 'invalid_item_parent', __( 'Invalid menu item parent.', 'gutenberg' ), array( 'status' => 400 ) );
491 }
492 }
493 }
494
495 foreach ( array( 'menu-item-object-id', 'menu-item-parent-id' ) as $key ) {
496 // Note we need to allow negative-integer IDs for previewed objects not inserted yet.
497 $prepared_nav_item[ $key ] = intval( $prepared_nav_item[ $key ] );
498 }
499
500 foreach ( array( 'menu-item-type', 'menu-item-object', 'menu-item-target' ) as $key ) {
501 $prepared_nav_item[ $key ] = sanitize_key( $prepared_nav_item[ $key ] );
502 }
503
504 // Valid xfn and classes are an array.
505 foreach ( array( 'menu-item-xfn', 'menu-item-classes' ) as $key ) {
506 $value = $prepared_nav_item[ $key ];
507 if ( ! is_array( $value ) ) {
508 $value = wp_parse_list( $value );
509 }
510 $prepared_nav_item[ $key ] = implode( ' ', array_map( 'sanitize_html_class', $value ) );
511 }
512
513 // Apply the same filters as when calling wp_insert_post().
514
515 /** This filter is documented in wp-includes/post.php */
516 $prepared_nav_item['menu-item-title'] = wp_unslash( apply_filters( 'title_save_pre', wp_slash( $prepared_nav_item['menu-item-title'] ) ) );
517
518 /** This filter is documented in wp-includes/post.php */
519 $prepared_nav_item['menu-item-attr-title'] = wp_unslash( apply_filters( 'excerpt_save_pre', wp_slash( $prepared_nav_item['menu-item-attr-title'] ) ) );
520
521 /** This filter is documented in wp-includes/post.php */
522 $prepared_nav_item['menu-item-description'] = wp_unslash( apply_filters( 'content_save_pre', wp_slash( $prepared_nav_item['menu-item-description'] ) ) );
523
524 // Valid url.
525 if ( '' !== $prepared_nav_item['menu-item-url'] ) {
526 $prepared_nav_item['menu-item-url'] = esc_url_raw( $prepared_nav_item['menu-item-url'] );
527 if ( '' === $prepared_nav_item['menu-item-url'] ) {
528 // Fail sanitization if URL is invalid.
529 return new WP_Error( 'invalid_url', __( 'Invalid URL.', 'gutenberg' ), array( 'status' => 400 ) );
530 }
531 }
532 // Only draft / publish are valid post status for menu items.
533 if ( 'publish' !== $prepared_nav_item['menu-item-status'] ) {
534 $prepared_nav_item['menu-item-status'] = 'draft';
535 }
536
537 $prepared_nav_item = (object) $prepared_nav_item;
538
539 /**
540 * Filters a post before it is inserted via the REST API.
541 *
542 * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
543 *
544 * @param stdClass $prepared_post An object representing a single post prepared
545 * for inserting or updating the database.
546 * @param WP_REST_Request $request Request object.
547 */
548 return apply_filters( "rest_pre_insert_{$this->post_type}", $prepared_nav_item, $request );
549 }
550
551 /**
552 * Prepares a single post output for response.
553 *
554 * @param object $post Post object.
555 * @param WP_REST_Request $request Request object.
556 *
557 * @return WP_REST_Response Response object.
558 */
559 public function prepare_item_for_response( $post, $request ) {
560 $fields = $this->get_fields_for_response( $request );
561
562 // Base fields for every post.
563 $menu_item = wp_setup_nav_menu_item( $post );
564 $data = array();
565 if ( in_array( 'id', $fields, true ) ) {
566 $data['id'] = $menu_item->ID;
567 }
568
569 if ( in_array( 'title', $fields, true ) ) {
570 add_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
571
572 $data['title'] = array(
573 'raw' => $menu_item->post_title,
574 'rendered' => $menu_item->title,
575 );
576
577 remove_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
578 }
579
580 if ( in_array( 'status', $fields, true ) ) {
581 $data['status'] = $menu_item->post_status;
582 }
583
584 if ( in_array( 'url', $fields, true ) ) {
585 $data['url'] = $menu_item->url;
586 }
587
588 if ( in_array( 'attr_title', $fields, true ) ) {
589 // Same as post_excerpt.
590 $data['attr_title'] = $menu_item->attr_title;
591 }
592
593 if ( in_array( 'description', $fields, true ) ) {
594 // Same as post_content.
595 $data['description'] = $menu_item->description;
596 }
597
598 if ( in_array( 'type', $fields, true ) ) {
599 // Using 'item_type' since 'type' already exists.
600 $data['type'] = $menu_item->type;
601 }
602
603 if ( in_array( 'type_label', $fields, true ) ) {
604 // Using 'item_type_label' to match up with 'item_type' - IS READ ONLY!
605 $data['type_label'] = $menu_item->type_label;
606 }
607
608 if ( in_array( 'object', $fields, true ) ) {
609 $data['object'] = $menu_item->object;
610 }
611
612 if ( in_array( 'object_id', $fields, true ) ) {
613 // Usually is a string, but lets expose as an integer.
614 $data['object_id'] = absint( $menu_item->object_id );
615 }
616
617 if ( rest_is_field_included( 'content', $fields ) ) {
618 $data['content'] = array();
619 }
620 if ( rest_is_field_included( 'content.raw', $fields ) ) {
621 $data['content']['raw'] = $menu_item->content;
622 }
623 if ( rest_is_field_included( 'content.rendered', $fields ) ) {
624 /** This filter is documented in wp-includes/post-template.php */
625 $data['content']['rendered'] = apply_filters( 'the_content', $menu_item->content );
626 }
627 if ( rest_is_field_included( 'content.block_version', $fields ) ) {
628 $data['content']['block_version'] = block_version( $menu_item->content );
629 }
630
631 if ( in_array( 'parent', $fields, true ) ) {
632 // Same as post_parent, expose as integer.
633 $data['parent'] = absint( $menu_item->menu_item_parent );
634 }
635
636 if ( in_array( 'menu_order', $fields, true ) ) {
637 // Same as post_parent, expose as integer.
638 $data['menu_order'] = absint( $menu_item->menu_order );
639 }
640
641 if ( in_array( 'menu_id', $fields, true ) ) {
642 $data['menu_id'] = $this->get_menu_id( $menu_item->ID );
643 }
644
645 if ( in_array( 'target', $fields, true ) ) {
646 $data['target'] = $menu_item->target;
647 }
648
649 if ( in_array( 'classes', $fields, true ) ) {
650 $data['classes'] = (array) $menu_item->classes;
651 }
652
653 if ( in_array( 'xfn', $fields, true ) ) {
654 $data['xfn'] = array_map( 'sanitize_html_class', explode( ' ', $menu_item->xfn ) );
655 }
656
657 if ( in_array( 'meta', $fields, true ) ) {
658 $data['meta'] = $this->meta->get_value( $menu_item->ID, $request );
659 }
660
661 $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
662
663 foreach ( $taxonomies as $taxonomy ) {
664 $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
665
666 if ( in_array( $base, $fields, true ) ) {
667 $terms = get_the_terms( $post, $taxonomy->name );
668 $data[ $base ] = $terms ? array_values( wp_list_pluck( $terms, 'term_id' ) ) : array();
669 }
670 }
671
672 $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
673 $data = $this->add_additional_fields_to_object( $data, $request );
674 $data = $this->filter_response_by_context( $data, $context );
675
676 // Wrap the data in a response object.
677 $response = rest_ensure_response( $data );
678
679 $links = $this->prepare_links( $menu_item );
680 $response->add_links( $links );
681
682 if ( ! empty( $links['self']['href'] ) ) {
683 $actions = $this->get_available_actions( $menu_item, $request );
684
685 $self = $links['self']['href'];
686
687 foreach ( $actions as $rel ) {
688 $response->add_link( $rel, $self );
689 }
690 }
691
692 /**
693 * Filters the post data for a response.
694 *
695 * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
696 *
697 * @param WP_REST_Response $response The response object.
698 * @param object $post Post object.
699 * @param WP_REST_Request $request Request object.
700 */
701 return apply_filters( "rest_prepare_{$this->post_type}", $response, $post, $request );
702 }
703
704 /**
705 * Prepares links for the request.
706 *
707 * @param object $menu_item Menu object.
708 *
709 * @return array Links for the given post.
710 */
711 protected function prepare_links( $menu_item ) {
712 $links = parent::prepare_links( $menu_item );
713
714 if ( 'post_type' === $menu_item->type && ! empty( $menu_item->object_id ) ) {
715 $post_type_object = get_post_type_object( $menu_item->object );
716 if ( $post_type_object->show_in_rest ) {
717 $rest_base = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name;
718 $url = rest_url( sprintf( 'wp/v2/%s/%d', $rest_base, $menu_item->object_id ) );
719 $links['https://api.w.org/object'][] = array(
720 'href' => $url,
721 'post_type' => $menu_item->type,
722 'embeddable' => true,
723 );
724 }
725 } elseif ( 'taxonomy' === $menu_item->type && ! empty( $menu_item->object_id ) ) {
726 $taxonomy_object = get_taxonomy( $menu_item->object );
727 if ( $taxonomy_object->show_in_rest ) {
728 $rest_base = ! empty( $taxonomy_object->rest_base ) ? $taxonomy_object->rest_base : $taxonomy_object->name;
729 $url = rest_url( sprintf( 'wp/v2/%s/%d', $rest_base, $menu_item->object_id ) );
730 $links['https://api.w.org/object'][] = array(
731 'href' => $url,
732 'taxonomy' => $menu_item->type,
733 'embeddable' => true,
734 );
735 }
736 }
737
738 return $links;
739 }
740
741 /**
742 * Retrieve Link Description Objects that should be added to the Schema for the posts collection.
743 *
744 * @return array
745 */
746 protected function get_schema_links() {
747 $links = parent::get_schema_links();
748 $href = rest_url( "{$this->namespace}/{$this->rest_base}/{id}" );
749 $links[] = array(
750 'rel' => 'https://api.w.org/object',
751 'title' => __( 'Get linked object.', 'gutenberg' ),
752 'href' => $href,
753 'targetSchema' => array(
754 'type' => 'object',
755 'properties' => array(
756 'object' => array(
757 'type' => 'integer',
758 ),
759 ),
760 ),
761 );
762
763 return $links;
764 }
765
766 /**
767 * Retrieves the term's schema, conforming to JSON Schema.
768 *
769 * @return array Item schema data.
770 */
771 public function get_item_schema() {
772 $schema = array(
773 '$schema' => 'http://json-schema.org/draft-04/schema#',
774 'title' => $this->post_type,
775 'type' => 'object',
776 );
777
778 $schema['properties']['title'] = array(
779 'description' => __( 'The title for the object.', 'gutenberg' ),
780 'type' => 'object',
781 'context' => array( 'view', 'edit', 'embed' ),
782 'arg_options' => array(
783 // Note: sanitization implemented in self::prepare_item_for_database().
784 'sanitize_callback' => null,
785 // Note: validation implemented in self::prepare_item_for_database().
786 'validate_callback' => null,
787 ),
788 'properties' => array(
789 'raw' => array(
790 'description' => __( 'Title for the object, as it exists in the database.', 'gutenberg' ),
791 'type' => 'string',
792 'context' => array( 'edit' ),
793 ),
794 'rendered' => array(
795 'description' => __( 'HTML title for the object, transformed for display.', 'gutenberg' ),
796 'type' => 'string',
797 'context' => array( 'view', 'edit', 'embed' ),
798 'readonly' => true,
799 ),
800 ),
801 );
802
803 $schema['properties']['id'] = array(
804 'description' => __( 'Unique identifier for the object.', 'gutenberg' ),
805 'type' => 'integer',
806 'default' => 0,
807 'minimum' => 0,
808 'context' => array( 'view', 'edit', 'embed' ),
809 'readonly' => true,
810 );
811
812 $schema['properties']['type_label'] = array(
813 'description' => __( 'Name of type.', 'gutenberg' ),
814 'type' => 'string',
815 'context' => array( 'view', 'edit', 'embed' ),
816 'readonly' => true,
817 );
818
819 $schema['properties']['type'] = array(
820 'description' => __( 'The family of objects originally represented, such as "post_type" or "taxonomy".', 'gutenberg' ),
821 'type' => 'string',
822 'enum' => array( 'taxonomy', 'post_type', 'post_type_archive', 'custom', 'block' ),
823 'context' => array( 'view', 'edit', 'embed' ),
824 'default' => 'custom',
825 );
826
827 $schema['properties']['status'] = array(
828 'description' => __( 'A named status for the object.', 'gutenberg' ),
829 'type' => 'string',
830 'enum' => array_keys( get_post_stati( array( 'internal' => false ) ) ),
831 'default' => 'publish',
832 'context' => array( 'view', 'edit', 'embed' ),
833 );
834
835 $schema['properties']['parent'] = array(
836 'description' => __( 'The ID for the parent of the object.', 'gutenberg' ),
837 'type' => 'integer',
838 'minimum' => 0,
839 'default' => 0,
840 'context' => array( 'view', 'edit', 'embed' ),
841 );
842
843 $schema['properties']['attr_title'] = array(
844 'description' => __( 'Text for the title attribute of the link element for this menu item.', 'gutenberg' ),
845 'type' => 'string',
846 'context' => array( 'view', 'edit', 'embed' ),
847 'arg_options' => array(
848 'sanitize_callback' => 'sanitize_text_field',
849 ),
850 );
851
852 $schema['properties']['classes'] = array(
853 'description' => __( 'Class names for the link element of this menu item.', 'gutenberg' ),
854 'type' => 'array',
855 'items' => array(
856 'type' => 'string',
857 ),
858 'context' => array( 'view', 'edit', 'embed' ),
859 'arg_options' => array(
860 'sanitize_callback' => function ( $value ) {
861 return array_map( 'sanitize_html_class', wp_parse_list( $value ) );
862 },
863 ),
864 );
865
866 $schema['properties']['description'] = array(
867 'description' => __( 'The description of this menu item.', 'gutenberg' ),
868 'type' => 'string',
869 'context' => array( 'view', 'edit', 'embed' ),
870 'arg_options' => array(
871 'sanitize_callback' => 'sanitize_text_field',
872 ),
873 );
874
875 $schema['properties']['menu_order'] = array(
876 'description' => __( 'The DB ID of the nav_menu_item that is this item\'s menu parent, if any, otherwise 0.', 'gutenberg' ),
877 'context' => array( 'view', 'edit', 'embed' ),
878 'type' => 'integer',
879 'minimum' => 0,
880 'default' => 0,
881 );
882 $schema['properties']['object'] = array(
883 'description' => __( 'The type of object originally represented, such as "category," "post", or "attachment."', 'gutenberg' ),
884 'context' => array( 'view', 'edit', 'embed' ),
885 'type' => 'string',
886 );
887
888 $schema['properties']['object_id'] = array(
889 'description' => __( 'The DB ID of the original object this menu item represents, e . g . ID for posts and term_id for categories.', 'gutenberg' ),
890 'context' => array( 'view', 'edit', 'embed' ),
891 'type' => 'integer',
892 'minimum' => 0,
893 'default' => 0,
894 );
895
896 $schema['properties']['content'] = array(
897 'description' => __( 'HTML content to display for this block menu item.', 'gutenberg' ),
898 'context' => array( 'view', 'edit', 'embed' ),
899 'type' => 'object',
900 'arg_options' => array(
901 'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database().
902 'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database().
903 ),
904 'properties' => array(
905 'raw' => array(
906 'description' => __( 'HTML content, as it exists in the database.', 'gutenberg' ),
907 'type' => 'string',
908 'context' => array( 'edit' ),
909 ),
910 'rendered' => array(
911 'description' => __( 'HTML content, transformed for display.', 'gutenberg' ),
912 'type' => 'string',
913 'context' => array( 'view', 'edit' ),
914 'readonly' => true,
915 ),
916 'block_version' => array(
917 'description' => __( 'Version of the block format used in the HTML content.', 'gutenberg' ),
918 'type' => 'integer',
919 'context' => array( 'edit' ),
920 'readonly' => true,
921 ),
922 ),
923 );
924
925 $schema['properties']['target'] = array(
926 'description' => __( 'The target attribute of the link element for this menu item.', 'gutenberg' ),
927 'type' => 'string',
928 'context' => array( 'view', 'edit', 'embed' ),
929 'enum' => array(
930 '_blank',
931 '',
932 ),
933 );
934
935 $schema['properties']['type_label'] = array(
936 'description' => __( 'The singular label used to describe this type of menu item.', 'gutenberg' ),
937 'context' => array( 'view', 'edit', 'embed' ),
938 'type' => 'string',
939 'readonly' => true,
940 );
941
942 $schema['properties']['url'] = array(
943 'description' => __( 'The URL to which this menu item points.', 'gutenberg' ),
944 'type' => 'string',
945 'format' => 'uri',
946 'context' => array( 'view', 'edit', 'embed' ),
947 );
948
949 $schema['properties']['xfn'] = array(
950 'description' => __( 'The XFN relationship expressed in the link of this menu item.', 'gutenberg' ),
951 'type' => 'array',
952 'items' => array(
953 'type' => 'string',
954 ),
955 'context' => array( 'view', 'edit', 'embed' ),
956 'arg_options' => array(
957 'sanitize_callback' => function ( $value ) {
958 return array_map( 'sanitize_html_class', wp_parse_list( $value ) );
959 },
960 ),
961 );
962
963 $schema['properties']['_invalid'] = array(
964 'description' => __( 'Whether the menu item represents an object that no longer exists.', 'gutenberg' ),
965 'context' => array( 'view', 'edit', 'embed' ),
966 'type' => 'boolean',
967 'readonly' => true,
968 );
969
970 $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
971
972 foreach ( $taxonomies as $taxonomy ) {
973 $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
974 $schema['properties'][ $base ] = array(
975 /* translators: %s: taxonomy name */
976 'description' => sprintf( __( 'The terms assigned to the object in the %s taxonomy.', 'gutenberg' ), $taxonomy->name ),
977 'type' => 'array',
978 'items' => array(
979 'type' => 'integer',
980 ),
981 'context' => array( 'view', 'edit' ),
982 );
983
984 if ( 'nav_menu' === $taxonomy->name ) {
985 $schema['properties'][ $base ]['type'] = 'integer';
986 unset( $schema['properties'][ $base ]['items'] );
987 }
988 }
989
990 $schema['properties']['meta'] = $this->meta->get_field_schema();
991
992 $schema_links = $this->get_schema_links();
993
994 if ( $schema_links ) {
995 $schema['links'] = $schema_links;
996 }
997
998 return $this->add_additional_fields_schema( $schema );
999 }
1000
1001 /**
1002 * Retrieves the query params for the posts collection.
1003 *
1004 * @return array Collection parameters.
1005 */
1006 public function get_collection_params() {
1007 $query_params = parent::get_collection_params();
1008
1009 $query_params['menu_order'] = array(
1010 'description' => __( 'Limit result set to posts with a specific menu_order value.', 'gutenberg' ),
1011 'type' => 'integer',
1012 );
1013
1014 $query_params['order'] = array(
1015 'description' => __( 'Order sort attribute ascending or descending.', 'gutenberg' ),
1016 'type' => 'string',
1017 'default' => 'asc',
1018 'enum' => array( 'asc', 'desc' ),
1019 );
1020
1021 $query_params['orderby'] = array(
1022 'description' => __( 'Sort collection by object attribute.', 'gutenberg' ),
1023 'type' => 'string',
1024 'default' => 'menu_order',
1025 'enum' => array(
1026 'author',
1027 'date',
1028 'id',
1029 'include',
1030 'modified',
1031 'parent',
1032 'relevance',
1033 'slug',
1034 'include_slugs',
1035 'title',
1036 'menu_order',
1037 ),
1038 );
1039
1040 return $query_params;
1041 }
1042
1043 /**
1044 * Determines the allowed query_vars for a get_items() response and prepares
1045 * them for WP_Query.
1046 *
1047 * @param array $prepared_args Optional. Prepared WP_Query arguments. Default empty array.
1048 * @param WP_REST_Request $request Optional. Full details about the request.
1049 *
1050 * @return array Items query arguments.
1051 */
1052 protected function prepare_items_query( $prepared_args = array(), $request = null ) {
1053 $query_args = parent::prepare_items_query( $prepared_args, $request );
1054
1055 // Map to proper WP_Query orderby param.
1056 if ( isset( $query_args['orderby'] ) && isset( $request['orderby'] ) ) {
1057 $orderby_mappings = array(
1058 'id' => 'ID',
1059 'include' => 'post__in',
1060 'slug' => 'post_name',
1061 'include_slugs' => 'post_name__in',
1062 'menu_order' => 'menu_order',
1063 );
1064
1065 if ( isset( $orderby_mappings[ $request['orderby'] ] ) ) {
1066 $query_args['orderby'] = $orderby_mappings[ $request['orderby'] ];
1067 }
1068 }
1069
1070 return $query_args;
1071 }
1072
1073 /**
1074 * Checks whether current user can assign all terms sent with the current request.
1075 *
1076 * @param WP_REST_Request $request The request object with post and terms data.
1077 *
1078 * @return bool Whether the current user can assign the provided terms.
1079 */
1080 protected function check_assign_terms_permission( $request ) {
1081 $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
1082 foreach ( $taxonomies as $taxonomy ) {
1083 $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
1084
1085 if ( ! isset( $request[ $base ] ) ) {
1086 continue;
1087 }
1088
1089 foreach ( (array) $request[ $base ] as $term_id ) {
1090 if ( ! $term_id ) {
1091 continue;
1092 }
1093
1094 // Invalid terms will be rejected later.
1095 if ( ! get_term( $term_id, $taxonomy->name ) ) {
1096 continue;
1097 };
1098
1099 if ( ! current_user_can( 'assign_term', (int) $term_id ) ) {
1100 return false;
1101 }
1102 }
1103 }
1104
1105 return true;
1106 }
1107
1108 /**
1109 * Get menu id of current menu item.
1110 *
1111 * @param int $menu_item_id Menu item id.
1112 *
1113 * @return int
1114 */
1115 protected function get_menu_id( $menu_item_id ) {
1116 $menu_ids = wp_get_post_terms( $menu_item_id, 'nav_menu', array( 'fields' => 'ids' ) );
1117 $menu_id = 0;
1118 if ( $menu_ids && ! is_wp_error( $menu_ids ) ) {
1119 $menu_id = array_shift( $menu_ids );
1120 }
1121
1122 return $menu_id;
1123 }
1124 }
1125