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

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

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