[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/rest-api/endpoints/ -> class-wp-rest-menu-items-controller.php (source)

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


Generated : Thu Aug 27 08:20:25 2026 Cross-referenced by PHPXref