PluginProbe
Gutenberg / 12.6.0
Gutenberg v12.6.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-menus-controller.php

class-wp-rest-menus-controller.php in Gutenberg 12.6.0, at lib/class-wp-rest-menus-controller.php

694 lines 21.7 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_Menus_Controller class
4 *
5 * @package WordPress
6 * @subpackage REST_API
7 */
8
9 /**
10 * Core class used to managed menu terms associated via the REST API.
11 *
12 * @see WP_REST_Controller
13 */
14 class WP_REST_Menus_Controller extends WP_REST_Terms_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 11.5.0
28 *
29 * @see register_rest_route()
30 */
31 public function register_routes() {
32 register_rest_route(
33 $this->namespace,
34 '/' . $this->rest_base,
35 array(
36 array(
37 'methods' => WP_REST_Server::READABLE,
38 'callback' => array( $this, 'get_items' ),
39 'permission_callback' => array( $this, 'get_items_permissions_check' ),
40 'args' => $this->get_collection_params(),
41 ),
42 array(
43 'methods' => WP_REST_Server::CREATABLE,
44 'callback' => array( $this, 'create_item' ),
45 'permission_callback' => array( $this, 'create_item_permissions_check' ),
46 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
47 ),
48 'allow_batch' => $this->allow_batch,
49 'schema' => array( $this, 'get_public_item_schema' ),
50 )
51 );
52
53 register_rest_route(
54 $this->namespace,
55 '/' . $this->rest_base . '/(?P<id>[\d]+)',
56 array(
57 'args' => array(
58 'id' => array(
59 'description' => __( 'Unique identifier for the term.', 'default' ),
60 'type' => 'integer',
61 ),
62 ),
63 array(
64 'methods' => WP_REST_Server::READABLE,
65 'callback' => array( $this, 'get_item' ),
66 'permission_callback' => array( $this, 'get_item_permissions_check' ),
67 'args' => array(
68 'context' => $this->get_context_param( array( 'default' => 'view' ) ),
69 ),
70 ),
71 array(
72 'methods' => WP_REST_Server::EDITABLE,
73 'callback' => array( $this, 'update_item' ),
74 'permission_callback' => array( $this, 'update_item_permissions_check' ),
75 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
76 ),
77 array(
78 'methods' => WP_REST_Server::DELETABLE,
79 'callback' => array( $this, 'delete_item' ),
80 'permission_callback' => array( $this, 'delete_item_permissions_check' ),
81 'args' => array(
82 'force' => array(
83 'type' => 'boolean',
84 'default' => false,
85 'description' => __( 'Required to be true, as terms do not support trashing.', 'default' ),
86 ),
87 ),
88 ),
89 'allow_batch' => $this->allow_batch,
90 'schema' => array( $this, 'get_public_item_schema' ),
91 )
92 );
93 }
94
95
96 /**
97 * Checks if a request has access to read terms in the specified taxonomy.
98 *
99 * @param WP_REST_Request $request Full details about the request.
100 * @return bool|WP_Error True if the request has read access, otherwise false or WP_Error object.
101 */
102 public function get_items_permissions_check( $request ) {
103 $tax_obj = get_taxonomy( $this->taxonomy );
104 if ( ! $tax_obj || ! $this->check_is_taxonomy_allowed( $this->taxonomy ) ) {
105 return false;
106 }
107 if ( ! current_user_can( $tax_obj->cap->edit_terms ) ) {
108 if ( 'edit' === $request['context'] ) {
109 return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to edit terms in this taxonomy.', 'gutenberg' ), array( 'status' => rest_authorization_required_code() ) );
110 }
111 return new WP_Error( 'rest_cannot_view', __( 'Sorry, you cannot view these menus, unless you have access to permission edit them. ', 'gutenberg' ), array( 'status' => rest_authorization_required_code() ) );
112 }
113 return true;
114 }
115
116 /**
117 * Checks if a request has access to read or edit the specified menu.
118 *
119 * @param WP_REST_Request $request Full details about the request.
120 * @return bool|WP_Error True if the request has read access for the item, otherwise false or WP_Error object.
121 */
122 public function get_item_permissions_check( $request ) {
123 $term = $this->get_term( $request['id'] );
124 if ( is_wp_error( $term ) ) {
125 return $term;
126 }
127 if ( ! current_user_can( 'edit_term', $term->term_id ) ) {
128 if ( 'edit' === $request['context'] ) {
129 return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to edit this term.', 'gutenberg' ), array( 'status' => rest_authorization_required_code() ) );
130 }
131 return new WP_Error( 'rest_cannot_view', __( 'Sorry, you cannot view this menu, unless you have access to permission edit it. ', 'gutenberg' ), array( 'status' => rest_authorization_required_code() ) );
132 }
133 return true;
134 }
135
136 /**
137 * Get the term, if the ID is valid.
138 *
139 * @param int $id Supplied ID.
140 *
141 * @return WP_Term|WP_Error Term object if ID is valid, WP_Error otherwise.
142 */
143 protected function get_term( $id ) {
144 $term = parent::get_term( $id );
145
146 if ( is_wp_error( $term ) ) {
147 return $term;
148 }
149
150 $nav_term = wp_get_nav_menu_object( $term );
151 $nav_term->auto_add = $this->get_menu_auto_add( $nav_term->term_id );
152
153 return $nav_term;
154 }
155
156 /**
157 * Checks if a request has access to create a term.
158 * Also check if request can assign menu locations.
159 *
160 * @param WP_REST_Request $request Full details about the request.
161 *
162 * @return bool|WP_Error True if the request has access to create items, false or WP_Error object otherwise.
163 */
164 public function create_item_permissions_check( $request ) {
165 $check = $this->check_assign_locations_permission( $request );
166 if ( is_wp_error( $check ) ) {
167 return $check;
168 }
169 $check = $this->check_set_auto_add_permission( $request );
170 if ( is_wp_error( $check ) ) {
171 return $check;
172 }
173
174 return parent::create_item_permissions_check( $request );
175 }
176
177 /**
178 * Checks if a request has access to update the specified term.
179 *
180 * @param WP_REST_Request $request Full details about the request.
181 *
182 * @return bool|WP_Error True if the request has access to update the item, false or WP_Error object otherwise.
183 */
184 public function update_item_permissions_check( $request ) {
185 $check = $this->check_assign_locations_permission( $request );
186 if ( is_wp_error( $check ) ) {
187 return $check;
188 }
189 $check = $this->check_set_auto_add_permission( $request );
190 if ( is_wp_error( $check ) ) {
191 return $check;
192 }
193
194 return parent::update_item_permissions_check( $request );
195 }
196
197 /**
198 * Checks whether current user can assign all locations sent with the current request.
199 *
200 * @param WP_REST_Request $request The request object with post and locations data.
201 *
202 * @return bool|WP_Error Whether the current user can assign the provided terms.
203 */
204 protected function check_assign_locations_permission( $request ) {
205 if ( ! isset( $request['locations'] ) ) {
206 return true;
207 }
208
209 if ( ! current_user_can( 'edit_theme_options' ) ) {
210 return new WP_Error( 'rest_cannot_assign_location', __( 'Sorry, you are not allowed to assign the provided locations.', 'gutenberg' ), array( 'status' => rest_authorization_required_code() ) );
211 }
212
213 foreach ( $request['locations'] as $location ) {
214 if ( ! array_key_exists( $location, get_registered_nav_menus() ) ) {
215 return new WP_Error(
216 'rest_menu_location_invalid',
217 __( 'Invalid menu location.', 'gutenberg' ),
218 array(
219 'status' => 400,
220 'location' => $location,
221 )
222 );
223 }
224 }
225
226 return true;
227 }
228
229 /**
230 * Checks whether current user can set auto add pages.
231 *
232 * @param WP_REST_Request $request The request object with post and locations data.
233 *
234 * @return true|WP_Error Whether the current user can assign the provided terms.
235 */
236 protected function check_set_auto_add_permission( $request ) {
237 if ( ! isset( $request['auto_add'] ) ) {
238 return true;
239 }
240
241 if ( ! current_user_can( 'edit_theme_options' ) ) {
242 return new WP_Error( 'rest_cannot_set_auto_add', __( 'Sorry, you are not allowed to set auto add pages.', 'gutenberg' ), array( 'status' => rest_authorization_required_code() ) );
243 }
244
245 return true;
246 }
247
248 /**
249 * Prepares a single term output for response.
250 *
251 * @param obj $term Term object.
252 * @param WP_REST_Request $request Request object.
253 *
254 * @return WP_REST_Response $response Response object.
255 */
256 public function prepare_item_for_response( $term, $request ) {
257 $nav_menu = wp_get_nav_menu_object( $term );
258 $response = parent::prepare_item_for_response( $nav_menu, $request );
259
260 $fields = $this->get_fields_for_response( $request );
261 $data = $response->get_data();
262
263 if ( rest_is_field_included( 'locations', $fields ) ) {
264 $data['locations'] = $this->get_menu_locations( $nav_menu->term_id );
265 }
266
267 if ( rest_is_field_included( 'auto_add', $fields ) ) {
268 $auto_add = $this->get_menu_auto_add( $nav_menu->term_id );
269 $data['auto_add'] = $auto_add;
270 }
271
272 $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
273 $data = $this->add_additional_fields_to_object( $data, $request );
274 $data = $this->filter_response_by_context( $data, $context );
275
276 $response = rest_ensure_response( $data );
277 $response->add_links( $this->prepare_links( $term ) );
278
279 /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php */
280 return apply_filters( "rest_prepare_{$this->taxonomy}", $response, $term, $request );
281 }
282
283 /**
284 * Prepares links for the request.
285 *
286 * @param object $term Term object.
287 *
288 * @return array Links for the given term.
289 */
290 protected function prepare_links( $term ) {
291 $links = parent::prepare_links( $term );
292
293 $locations = $this->get_menu_locations( $term->term_id );
294 $rest_base = 'menu-locations';
295 foreach ( $locations as $location ) {
296 $url = rest_url( sprintf( 'wp/v2/%s/%s', $rest_base, $location ) );
297 $links['https://api.w.org/menu-location'][] = array(
298 'href' => $url,
299 'embeddable' => true,
300 );
301 }
302
303 return $links;
304 }
305
306 /**
307 * Prepares a single term for create or update.
308 *
309 * @param WP_REST_Request $request Request object.
310 *
311 * @return array $prepared_term Term object.
312 */
313 public function prepare_item_for_database( $request ) {
314 $prepared_term = parent::prepare_item_for_database( $request );
315
316 $prepared_term = (array) $prepared_term;
317 $schema = $this->get_item_schema();
318 if ( isset( $request['name'] ) && ! empty( $schema['properties']['name'] ) ) {
319 $prepared_term['menu-name'] = $request['name'];
320 }
321
322 return $prepared_term;
323 }
324
325 /**
326 * Creates a single term in a taxonomy.
327 *
328 * @param WP_REST_Request $request Full details about the request.
329 *
330 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
331 */
332 public function create_item( $request ) {
333 if ( isset( $request['parent'] ) ) {
334 if ( ! is_taxonomy_hierarchical( $this->taxonomy ) ) {
335 return new WP_Error( 'rest_taxonomy_not_hierarchical', __( 'Cannot set parent term, taxonomy is not hierarchical.', 'gutenberg' ), array( 'status' => 400 ) );
336 }
337
338 $parent = wp_get_nav_menu_object( (int) $request['parent'] );
339
340 if ( ! $parent ) {
341 return new WP_Error( 'rest_term_invalid', __( 'Parent term does not exist.', 'gutenberg' ), array( 'status' => 400 ) );
342 }
343 }
344
345 $prepared_term = $this->prepare_item_for_database( $request );
346
347 $term = wp_update_nav_menu_object( 0, wp_slash( (array) $prepared_term ) );
348
349 if ( is_wp_error( $term ) ) {
350 /*
351 * If we're going to inform the client that the term already exists,
352 * give them the identifier for future use.
353 */
354
355 if ( in_array( 'menu_exists', $term->get_error_codes(), true ) ) {
356 $existing_term = get_term_by( 'name', $prepared_term['menu-name'], $this->taxonomy );
357 $term->add_data( $existing_term->term_id, 'menu_exists' );
358 $term->add_data(
359 array(
360 'status' => 400,
361 'term_id' => $existing_term->term_id,
362 )
363 );
364 } else {
365 $term->add_data( array( 'status' => 400 ) );
366 }
367
368 return $term;
369 }
370
371 $term = $this->get_term( $term );
372
373 /**
374 * Fires after a single term is created or updated via the REST API.
375 *
376 * The dynamic portion of the hook name, `$this->taxonomy`, refers to the taxonomy slug.
377 *
378 * @param WP_Term $term Inserted or updated term object.
379 * @param WP_REST_Request $request Request object.
380 * @param bool $creating True when creating a term, false when updating.
381 */
382 do_action( "rest_insert_{$this->taxonomy}", $term, $request, true );
383
384 $schema = $this->get_item_schema();
385 if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
386 $meta_update = $this->meta->update_value( $request['meta'], $term->term_id );
387
388 if ( is_wp_error( $meta_update ) ) {
389 return $meta_update;
390 }
391 }
392
393 $locations_update = $this->handle_locations( $term->term_id, $request );
394
395 if ( is_wp_error( $locations_update ) ) {
396 return $locations_update;
397 }
398
399 $this->handle_auto_add( $term->term_id, $request );
400
401 $fields_update = $this->update_additional_fields_for_object( $term, $request );
402
403 if ( is_wp_error( $fields_update ) ) {
404 return $fields_update;
405 }
406
407 $request->set_param( 'context', 'view' );
408
409 /**
410 * Fires after a single term is completely created or updated via the REST API.
411 *
412 * The dynamic portion of the hook name, `$this->taxonomy`, refers to the taxonomy slug.
413 *
414 * @param WP_Term $term Inserted or updated term object.
415 * @param WP_REST_Request $request Request object.
416 * @param bool $creating True when creating a term, false when updating.
417 *
418 * @since 5.0.0
419 */
420 do_action( "rest_after_insert_{$this->taxonomy}", $term, $request, true );
421
422 $response = $this->prepare_item_for_response( $term, $request );
423 $response = rest_ensure_response( $response );
424
425 $response->set_status( 201 );
426 $response->header( 'Location', rest_url( $this->namespace . '/' . $this->rest_base . '/' . $term->term_id ) );
427
428 return $response;
429 }
430
431 /**
432 * Updates a single term from a taxonomy.
433 *
434 * @param WP_REST_Request $request Full details about the request.
435 *
436 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
437 */
438 public function update_item( $request ) {
439 $term = $this->get_term( $request['id'] );
440 if ( is_wp_error( $term ) ) {
441 return $term;
442 }
443
444 if ( isset( $request['parent'] ) ) {
445 if ( ! is_taxonomy_hierarchical( $this->taxonomy ) ) {
446 return new WP_Error( 'rest_taxonomy_not_hierarchical', __( 'Cannot set parent term, taxonomy is not hierarchical.', 'gutenberg' ), array( 'status' => 400 ) );
447 }
448
449 $parent = get_term( (int) $request['parent'], $this->taxonomy );
450
451 if ( ! $parent ) {
452 return new WP_Error( 'rest_term_invalid', __( 'Parent term does not exist.', 'gutenberg' ), array( 'status' => 400 ) );
453 }
454 }
455
456 $prepared_term = $this->prepare_item_for_database( $request );
457
458 // Only update the term if we haz something to update.
459 if ( ! empty( $prepared_term ) ) {
460 $update = wp_update_nav_menu_object( $term->term_id, wp_slash( (array) $prepared_term ) );
461
462 if ( is_wp_error( $update ) ) {
463 return $update;
464 }
465 }
466
467 $term = get_term( $term->term_id, $this->taxonomy );
468
469 /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php */
470 do_action( "rest_insert_{$this->taxonomy}", $term, $request, false );
471
472 $schema = $this->get_item_schema();
473 if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
474 $meta_update = $this->meta->update_value( $request['meta'], $term->term_id );
475
476 if ( is_wp_error( $meta_update ) ) {
477 return $meta_update;
478 }
479 }
480
481 $locations_update = $this->handle_locations( $term->term_id, $request );
482
483 if ( is_wp_error( $locations_update ) ) {
484 return $locations_update;
485 }
486
487 $this->handle_auto_add( $term->term_id, $request );
488
489 $fields_update = $this->update_additional_fields_for_object( $term, $request );
490
491 if ( is_wp_error( $fields_update ) ) {
492 return $fields_update;
493 }
494
495 $request->set_param( 'context', 'view' );
496
497 /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php */
498 do_action( "rest_after_insert_{$this->taxonomy}", $term, $request, false );
499
500 $response = $this->prepare_item_for_response( $term, $request );
501
502 return rest_ensure_response( $response );
503 }
504
505 /**
506 * Deletes a single term from a taxonomy.
507 *
508 * @param WP_REST_Request $request Full details about the request.
509 *
510 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
511 */
512 public function delete_item( $request ) {
513 $term = $this->get_term( $request['id'] );
514 if ( is_wp_error( $term ) ) {
515 return $term;
516 }
517
518 $force = isset( $request['force'] ) ? (bool) $request['force'] : false;
519
520 // We don't support trashing for terms.
521 if ( ! $force ) {
522 /* translators: %s: force=true */
523 return new WP_Error( 'rest_trash_not_supported', sprintf( __( "Terms do not support trashing. Set '%s' to delete.", 'gutenberg' ), 'force=true' ), array( 'status' => 501 ) );
524 }
525
526 $request->set_param( 'context', 'view' );
527
528 $previous = $this->prepare_item_for_response( $term, $request );
529
530 $retval = wp_delete_nav_menu( $term );
531
532 if ( ! $retval ) {
533 return new WP_Error( 'rest_cannot_delete', __( 'The term cannot be deleted.', 'gutenberg' ), array( 'status' => 500 ) );
534 }
535
536 $response = new WP_REST_Response();
537 $response->set_data(
538 array(
539 'deleted' => true,
540 'previous' => $previous->get_data(),
541 )
542 );
543
544 /**
545 * Fires after a single term is deleted via the REST API.
546 *
547 * The dynamic portion of the hook name, `$this->taxonomy`, refers to the taxonomy slug.
548 *
549 * @param WP_Term $term The deleted term.
550 * @param WP_REST_Response $response The response data.
551 * @param WP_REST_Request $request The request sent to the API.
552 */
553 do_action( "rest_delete_{$this->taxonomy}", $term, $response, $request );
554
555 return $response;
556 }
557
558 /**
559 * Returns the value of a menu's auto_add
560 *
561 * @param int $menu_id The menu id to update the location form.
562 *
563 * @return bool The value of auto_add.
564 */
565 function get_menu_auto_add( $menu_id ) {
566 $nav_menu_option = (array) get_option( 'nav_menu_options', array( 'auto_add' => array() ) );
567 $check = in_array( $menu_id, $nav_menu_option['auto_add'], true );
568
569 return $check;
570 }
571
572 /**
573 * Updates the menu's auto add from a REST request.
574 *
575 * @param int $menu_id The menu id to update the location form.
576 * @param WP_REST_Request $request The request object with menu and locations data.
577 *
578 * @return bool True if the auto update was successfully updated.
579 */
580 function handle_auto_add( $menu_id, $request ) {
581 if ( ! isset( $request['auto_add'] ) ) {
582 return true;
583 }
584
585 $nav_menu_option = (array) get_option( 'nav_menu_options', array( 'auto_add' => array() ) );
586
587 if ( ! isset( $nav_menu_option['auto_add'] ) ) {
588 $nav_menu_option['auto_add'] = array();
589 }
590
591 $auto_add = $request['auto_add'];
592
593 $i = array_search( $menu_id, $nav_menu_option['auto_add'], true );
594
595 if ( $auto_add && false === $i ) {
596 $nav_menu_option['auto_add'][] = $menu_id;
597 } elseif ( ! $auto_add && false !== $i ) {
598 array_splice( $nav_menu_option['auto_add'], $i, 1 );
599 }
600
601 $update = update_option( 'nav_menu_options', $nav_menu_option );
602
603 /** This action is documented in wp-includes/nav-menu.php */
604 do_action( 'wp_update_nav_menu', $menu_id );
605
606 return $update;
607 }
608
609 /**
610 * Returns names of the locations assigned to the menu.
611 *
612 * @since 5.8.0
613 *
614 * @param int $menu_id The menu id.
615 *
616 * @return string[] $menu_locations The locations assigned to the menu.
617 */
618 protected function get_menu_locations( $menu_id ) {
619 $locations = get_nav_menu_locations();
620 $menu_locations = array();
621
622 foreach ( $locations as $location => $assigned_menu_id ) {
623 if ( $menu_id === $assigned_menu_id ) {
624 $menu_locations[] = $location;
625 }
626 }
627
628 return $menu_locations;
629 }
630
631 /**
632 * Updates the menu's locations from a REST request.
633 *
634 * @param int $menu_id The menu id to update the location form.
635 * @param WP_REST_Request $request The request object with menu and locations data.
636 *
637 * @return true|WP_Error WP_Error on an error assigning any of the locations, otherwise null.
638 */
639 protected function handle_locations( $menu_id, $request ) {
640 if ( ! isset( $request['locations'] ) ) {
641 return true;
642 }
643
644 $menu_locations = get_registered_nav_menus();
645 $menu_locations = array_keys( $menu_locations );
646 $new_locations = array();
647 foreach ( $request['locations'] as $location ) {
648 if ( ! in_array( $location, $menu_locations, true ) ) {
649 return new WP_Error( 'invalid_menu_location', __( 'Menu location does not exist.', 'gutenberg' ), array( 'status' => 400 ) );
650 }
651 $new_locations[ $location ] = $menu_id;
652 }
653 $assigned_menu = get_nav_menu_locations();
654 foreach ( $assigned_menu as $location => $term_id ) {
655 if ( $term_id === $menu_id ) {
656 unset( $assigned_menu[ $location ] );
657 }
658 }
659 $new_assignments = array_merge( $assigned_menu, $new_locations );
660 set_theme_mod( 'nav_menu_locations', $new_assignments );
661
662 return true;
663 }
664
665 /**
666 * Retrieves the term's schema, conforming to JSON Schema.
667 *
668 * @return array Item schema data.
669 */
670 public function get_item_schema() {
671 $schema = parent::get_item_schema();
672 unset( $schema['properties']['count'] );
673 unset( $schema['properties']['link'] );
674 unset( $schema['properties']['taxonomy'] );
675
676 $schema['properties']['locations'] = array(
677 'description' => __( 'The locations assigned to the menu.', 'gutenberg' ),
678 'type' => 'array',
679 'items' => array(
680 'type' => 'string',
681 ),
682 'context' => array( 'view', 'edit' ),
683 );
684
685 $schema['properties']['auto_add'] = array(
686 'description' => __( 'Whether to automatically add top level pages to this menu.', 'gutenberg' ),
687 'context' => array( 'view', 'edit' ),
688 'type' => 'boolean',
689 );
690
691 return $schema;
692 }
693 }
694