PluginProbe
Gutenberg / 9.8.0
Gutenberg v9.8.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 9.8.0, at lib/class-wp-rest-menu-items-controller.php

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