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

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

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