PluginProbe
Gutenberg / 8.2.1
Gutenberg v8.2.1
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 8.2.1, at lib/class-wp-rest-menus-controller.php

491 lines 15.2 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
81 return $nav_term;
82 }
83
84 /**
85 * Checks if a request has access to create a term.
86 * Also check if request can assign menu locations.
87 *
88 * @param WP_REST_Request $request Full details about the request.
89 *
90 * @return bool|WP_Error True if the request has access to create items, false or WP_Error object otherwise.
91 */
92 public function create_item_permissions_check( $request ) {
93 $check = $this->check_assign_locations_permission( $request );
94 if ( is_wp_error( $check ) ) {
95 return $check;
96 }
97
98 return parent::create_item_permissions_check( $request );
99 }
100
101 /**
102 * Checks if a request has access to update the specified term.
103 *
104 * @param WP_REST_Request $request Full details about the request.
105 *
106 * @return bool|WP_Error True if the request has access to update the item, false or WP_Error object otherwise.
107 */
108 public function update_item_permissions_check( $request ) {
109 $check = $this->check_assign_locations_permission( $request );
110 if ( is_wp_error( $check ) ) {
111 return $check;
112 }
113
114 return parent::update_item_permissions_check( $request );
115 }
116
117 /**
118 * Checks whether current user can assign all locations sent with the current request.
119 *
120 * @param WP_REST_Request $request The request object with post and locations data.
121 *
122 * @return bool Whether the current user can assign the provided terms.
123 */
124 protected function check_assign_locations_permission( $request ) {
125 if ( ! isset( $request['locations'] ) ) {
126 return true;
127 }
128
129 if ( ! current_user_can( 'edit_theme_options' ) ) {
130 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() ) );
131 }
132
133 foreach ( $request['locations'] as $location ) {
134 if ( ! array_key_exists( $location, get_registered_nav_menus() ) ) {
135 return new WP_Error(
136 'rest_menu_location_invalid',
137 __( 'Invalid menu location.', 'gutenberg' ),
138 array(
139 'status' => 400,
140 'location' => $location,
141 )
142 );
143 }
144 }
145
146 return true;
147 }
148
149 /**
150 * Prepares a single term output for response.
151 *
152 * @param obj $term Term object.
153 * @param WP_REST_Request $request Request object.
154 *
155 * @return WP_REST_Response $response Response object.
156 */
157 public function prepare_item_for_response( $term, $request ) {
158 $nav_menu = wp_get_nav_menu_object( $term );
159
160 return parent::prepare_item_for_response( $nav_menu, $request );
161 }
162
163 /**
164 * Prepares links for the request.
165 *
166 * @param object $term Term object.
167 *
168 * @return array Links for the given term.
169 */
170 protected function prepare_links( $term ) {
171 $links = parent::prepare_links( $term );
172
173 $locations = get_nav_menu_locations();
174 $rest_base = 'menu-locations';
175 foreach ( $locations as $menu_name => $menu_id ) {
176 if ( $term->term_id === $menu_id ) {
177 $url = rest_url( sprintf( '__experimental/%s/%s', $rest_base, $menu_name ) );
178 $links['https://api.w.org/menu-location'][] = array(
179 'href' => $url,
180 'embeddable' => true,
181 );
182 }
183 }
184
185 return $links;
186 }
187
188 /**
189 * Prepares a single term for create or update.
190 *
191 * @param WP_REST_Request $request Request object.
192 *
193 * @return array $prepared_term Term object.
194 */
195 public function prepare_item_for_database( $request ) {
196 $prepared_term = parent::prepare_item_for_database( $request );
197
198 $prepared_term = (array) $prepared_term;
199 $schema = $this->get_item_schema();
200 if ( isset( $request['name'] ) && ! empty( $schema['properties']['name'] ) ) {
201 $prepared_term['menu-name'] = $request['name'];
202 }
203
204 return $prepared_term;
205 }
206
207 /**
208 * Creates a single term in a taxonomy.
209 *
210 * @param WP_REST_Request $request Full details about the request.
211 *
212 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
213 */
214 public function create_item( $request ) {
215 if ( isset( $request['parent'] ) ) {
216 if ( ! is_taxonomy_hierarchical( $this->taxonomy ) ) {
217 return new WP_Error( 'rest_taxonomy_not_hierarchical', __( 'Cannot set parent term, taxonomy is not hierarchical.', 'gutenberg' ), array( 'status' => 400 ) );
218 }
219
220 $parent = wp_get_nav_menu_object( (int) $request['parent'] );
221
222 if ( ! $parent ) {
223 return new WP_Error( 'rest_term_invalid', __( 'Parent term does not exist.', 'gutenberg' ), array( 'status' => 400 ) );
224 }
225 }
226
227 $prepared_term = $this->prepare_item_for_database( $request );
228
229 $term = wp_update_nav_menu_object( 0, wp_slash( (array) $prepared_term ) );
230
231 if ( is_wp_error( $term ) ) {
232 /*
233 * If we're going to inform the client that the term already exists,
234 * give them the identifier for future use.
235 */
236 $term_id = $term->get_error_data( 'term_exists' );
237 if ( $term_id ) {
238 $existing_term = get_term( $term_id, $this->taxonomy );
239 $term->add_data( $existing_term->term_id, 'term_exists' );
240 $term->add_data(
241 array(
242 'status' => 400,
243 'term_id' => $term_id,
244 )
245 );
246 }
247
248 return $term;
249 }
250
251 $term = $this->get_term( $term );
252
253 /**
254 * Fires after a single term is created or updated via the REST API.
255 *
256 * The dynamic portion of the hook name, `$this->taxonomy`, refers to the taxonomy slug.
257 *
258 * @param WP_Term $term Inserted or updated term object.
259 * @param WP_REST_Request $request Request object.
260 * @param bool $creating True when creating a term, false when updating.
261 */
262 do_action( "rest_insert_{$this->taxonomy}", $term, $request, true );
263
264 $schema = $this->get_item_schema();
265 if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
266 $meta_update = $this->meta->update_value( $request['meta'], $term->term_id );
267
268 if ( is_wp_error( $meta_update ) ) {
269 return $meta_update;
270 }
271 }
272
273 $locations_update = $this->handle_locations( $term->term_id, $request );
274
275 if ( is_wp_error( $locations_update ) ) {
276 return $locations_update;
277 }
278
279 $fields_update = $this->update_additional_fields_for_object( $term, $request );
280
281 if ( is_wp_error( $fields_update ) ) {
282 return $fields_update;
283 }
284
285 $request->set_param( 'context', 'view' );
286
287 /**
288 * Fires after a single term is completely created or updated via the REST API.
289 *
290 * The dynamic portion of the hook name, `$this->taxonomy`, refers to the taxonomy slug.
291 *
292 * @param WP_Term $term Inserted or updated term object.
293 * @param WP_REST_Request $request Request object.
294 * @param bool $creating True when creating a term, false when updating.
295 *
296 * @since 5.0.0
297 */
298 do_action( "rest_after_insert_{$this->taxonomy}", $term, $request, true );
299
300 $response = $this->prepare_item_for_response( $term, $request );
301 $response = rest_ensure_response( $response );
302
303 $response->set_status( 201 );
304 $response->header( 'Location', rest_url( $this->namespace . '/' . $this->rest_base . '/' . $term->term_id ) );
305
306 return $response;
307 }
308
309 /**
310 * Updates a single term from a taxonomy.
311 *
312 * @param WP_REST_Request $request Full details about the request.
313 *
314 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
315 */
316 public function update_item( $request ) {
317 $term = $this->get_term( $request['id'] );
318 if ( is_wp_error( $term ) ) {
319 return $term;
320 }
321
322 if ( isset( $request['parent'] ) ) {
323 if ( ! is_taxonomy_hierarchical( $this->taxonomy ) ) {
324 return new WP_Error( 'rest_taxonomy_not_hierarchical', __( 'Cannot set parent term, taxonomy is not hierarchical.', 'gutenberg' ), array( 'status' => 400 ) );
325 }
326
327 $parent = get_term( (int) $request['parent'], $this->taxonomy );
328
329 if ( ! $parent ) {
330 return new WP_Error( 'rest_term_invalid', __( 'Parent term does not exist.', 'gutenberg' ), array( 'status' => 400 ) );
331 }
332 }
333
334 $prepared_term = $this->prepare_item_for_database( $request );
335
336 // Only update the term if we haz something to update.
337 if ( ! empty( $prepared_term ) ) {
338 $update = wp_update_nav_menu_object( $term->term_id, wp_slash( (array) $prepared_term ) );
339
340 if ( is_wp_error( $update ) ) {
341 return $update;
342 }
343 }
344
345 $term = get_term( $term->term_id, $this->taxonomy );
346
347 /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php */
348 do_action( "rest_insert_{$this->taxonomy}", $term, $request, false );
349
350 $schema = $this->get_item_schema();
351 if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
352 $meta_update = $this->meta->update_value( $request['meta'], $term->term_id );
353
354 if ( is_wp_error( $meta_update ) ) {
355 return $meta_update;
356 }
357 }
358
359 $locations_update = $this->handle_locations( $term->term_id, $request );
360
361 if ( is_wp_error( $locations_update ) ) {
362 return $locations_update;
363 }
364
365 $fields_update = $this->update_additional_fields_for_object( $term, $request );
366
367 if ( is_wp_error( $fields_update ) ) {
368 return $fields_update;
369 }
370
371 $request->set_param( 'context', 'view' );
372
373 /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php */
374 do_action( "rest_after_insert_{$this->taxonomy}", $term, $request, false );
375
376 $response = $this->prepare_item_for_response( $term, $request );
377
378 return rest_ensure_response( $response );
379 }
380
381 /**
382 * Deletes a single term from a taxonomy.
383 *
384 * @param WP_REST_Request $request Full details about the request.
385 *
386 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
387 */
388 public function delete_item( $request ) {
389 $term = $this->get_term( $request['id'] );
390 if ( is_wp_error( $term ) ) {
391 return $term;
392 }
393
394 $force = isset( $request['force'] ) ? (bool) $request['force'] : false;
395
396 // We don't support trashing for terms.
397 if ( ! $force ) {
398 /* translators: %s: force=true */
399 return new WP_Error( 'rest_trash_not_supported', sprintf( __( "Terms do not support trashing. Set '%s' to delete.", 'gutenberg' ), 'force=true' ), array( 'status' => 501 ) );
400 }
401
402 $request->set_param( 'context', 'view' );
403
404 $previous = $this->prepare_item_for_response( $term, $request );
405
406 $retval = wp_delete_nav_menu( $term );
407
408 if ( ! $retval ) {
409 return new WP_Error( 'rest_cannot_delete', __( 'The term cannot be deleted.', 'gutenberg' ), array( 'status' => 500 ) );
410 }
411
412 $response = new WP_REST_Response();
413 $response->set_data(
414 array(
415 'deleted' => true,
416 'previous' => $previous->get_data(),
417 )
418 );
419
420 /**
421 * Fires after a single term is deleted via the REST API.
422 *
423 * The dynamic portion of the hook name, `$this->taxonomy`, refers to the taxonomy slug.
424 *
425 * @param WP_Term $term The deleted term.
426 * @param WP_REST_Response $response The response data.
427 * @param WP_REST_Request $request The request sent to the API.
428 */
429 do_action( "rest_delete_{$this->taxonomy}", $term, $response, $request );
430
431 return $response;
432 }
433
434 /**
435 * Updates the menu's locations from a REST request.
436 *
437 * @param int $menu_id The menu id to update the location form.
438 * @param WP_REST_Request $request The request object with menu and locations data.
439 *
440 * @return true|WP_Error WP_Error on an error assigning any of the locations, otherwise null.
441 */
442 protected function handle_locations( $menu_id, $request ) {
443 if ( ! isset( $request['locations'] ) ) {
444 return true;
445 }
446
447 $menu_locations = get_registered_nav_menus();
448 $menu_locations = array_keys( $menu_locations );
449 $new_locations = array();
450 foreach ( $request['locations'] as $location ) {
451 if ( ! in_array( $location, $menu_locations, true ) ) {
452 return new WP_Error( 'invalid_menu_location', __( 'Menu location does not exist.', 'gutenberg' ), array( 'status' => 400 ) );
453 }
454 $new_locations[ $location ] = $menu_id;
455 }
456 $assigned_menu = get_nav_menu_locations();
457 foreach ( $assigned_menu as $location => $term_id ) {
458 if ( $term_id === $menu_id ) {
459 unset( $assigned_menu[ $location ] );
460 }
461 }
462 $new_assignments = array_merge( $assigned_menu, $new_locations );
463 set_theme_mod( 'nav_menu_locations', $new_assignments );
464
465 return true;
466 }
467
468 /**
469 * Retrieves the term's schema, conforming to JSON Schema.
470 *
471 * @return array Item schema data.
472 */
473 public function get_item_schema() {
474 $schema = parent::get_item_schema();
475 unset( $schema['properties']['count'] );
476 unset( $schema['properties']['link'] );
477 unset( $schema['properties']['taxonomy'] );
478
479 $schema['properties']['locations'] = array(
480 'description' => __( 'The locations assigned to the menu.', 'gutenberg' ),
481 'type' => 'array',
482 'items' => array(
483 'type' => 'string',
484 ),
485 'context' => array( 'view', 'edit' ),
486 );
487
488 return $schema;
489 }
490 }
491