[ 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 $this             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       *
 341       * @return object|WP_Error
 342       */
 343  	protected function prepare_item_for_database( $request ) {
 344          $menu_item_db_id = $request['id'];
 345          $menu_item_obj   = $this->get_nav_menu_item( $menu_item_db_id );
 346          // Need to persist the menu item data. See https://core.trac.wordpress.org/ticket/28138
 347          if ( ! is_wp_error( $menu_item_obj ) ) {
 348              // Correct the menu position if this was the first item. See https://core.trac.wordpress.org/ticket/28140
 349              $position = ( 0 === $menu_item_obj->menu_order ) ? 1 : $menu_item_obj->menu_order;
 350  
 351              $prepared_nav_item = array(
 352                  'menu-item-db-id'       => $menu_item_db_id,
 353                  'menu-item-object-id'   => $menu_item_obj->object_id,
 354                  'menu-item-object'      => $menu_item_obj->object,
 355                  'menu-item-parent-id'   => $menu_item_obj->menu_item_parent,
 356                  'menu-item-position'    => $position,
 357                  'menu-item-type'        => $menu_item_obj->type,
 358                  'menu-item-title'       => $menu_item_obj->title,
 359                  'menu-item-url'         => $menu_item_obj->url,
 360                  'menu-item-description' => $menu_item_obj->description,
 361                  'menu-item-attr-title'  => $menu_item_obj->attr_title,
 362                  'menu-item-target'      => $menu_item_obj->target,
 363                  'menu-item-classes'     => $menu_item_obj->classes,
 364                  // Stored in the database as a string.
 365                  'menu-item-xfn'         => explode( ' ', $menu_item_obj->xfn ),
 366                  'menu-item-status'      => $menu_item_obj->post_status,
 367                  'menu-id'               => $this->get_menu_id( $menu_item_db_id ),
 368              );
 369          } else {
 370              $prepared_nav_item = array(
 371                  'menu-id'               => 0,
 372                  'menu-item-db-id'       => 0,
 373                  'menu-item-object-id'   => 0,
 374                  'menu-item-object'      => '',
 375                  'menu-item-parent-id'   => 0,
 376                  'menu-item-position'    => 1,
 377                  'menu-item-type'        => 'custom',
 378                  'menu-item-title'       => '',
 379                  'menu-item-url'         => '',
 380                  'menu-item-description' => '',
 381                  'menu-item-attr-title'  => '',
 382                  'menu-item-target'      => '',
 383                  'menu-item-classes'     => array(),
 384                  'menu-item-xfn'         => array(),
 385                  'menu-item-status'      => 'publish',
 386              );
 387          }
 388  
 389          $mapping = array(
 390              'menu-item-db-id'       => 'id',
 391              'menu-item-object-id'   => 'object_id',
 392              'menu-item-object'      => 'object',
 393              'menu-item-parent-id'   => 'parent',
 394              'menu-item-position'    => 'menu_order',
 395              'menu-item-type'        => 'type',
 396              'menu-item-url'         => 'url',
 397              'menu-item-description' => 'description',
 398              'menu-item-attr-title'  => 'attr_title',
 399              'menu-item-target'      => 'target',
 400              'menu-item-classes'     => 'classes',
 401              'menu-item-xfn'         => 'xfn',
 402              'menu-item-status'      => 'status',
 403          );
 404  
 405          $schema = $this->get_item_schema();
 406  
 407          foreach ( $mapping as $original => $api_request ) {
 408              if ( isset( $request[ $api_request ] ) ) {
 409                  $prepared_nav_item[ $original ] = $request[ $api_request ];
 410              }
 411          }
 412  
 413          $taxonomy = get_taxonomy( 'nav_menu' );
 414          $base     = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
 415          // If menus submitted, cast to int.
 416          if ( ! empty( $request[ $base ] ) ) {
 417              $prepared_nav_item['menu-id'] = absint( $request[ $base ] );
 418          }
 419  
 420          // Nav menu title.
 421          if ( ! empty( $schema['properties']['title'] ) && isset( $request['title'] ) ) {
 422              if ( is_string( $request['title'] ) ) {
 423                  $prepared_nav_item['menu-item-title'] = $request['title'];
 424              } elseif ( ! empty( $request['title']['raw'] ) ) {
 425                  $prepared_nav_item['menu-item-title'] = $request['title']['raw'];
 426              }
 427          }
 428  
 429          $error = new WP_Error();
 430  
 431          // Check if object id exists before saving.
 432          if ( ! $prepared_nav_item['menu-item-object'] ) {
 433              // If taxonomy, check if term exists.
 434              if ( 'taxonomy' === $prepared_nav_item['menu-item-type'] ) {
 435                  $original = get_term( absint( $prepared_nav_item['menu-item-object-id'] ) );
 436                  if ( empty( $original ) || is_wp_error( $original ) ) {
 437                      $error->add( 'rest_term_invalid_id', __( 'Invalid term ID.' ), array( 'status' => 400 ) );
 438                  } else {
 439                      $prepared_nav_item['menu-item-object'] = get_term_field( 'taxonomy', $original );
 440                  }
 441                  // If post, check if post object exists.
 442              } elseif ( 'post_type' === $prepared_nav_item['menu-item-type'] ) {
 443                  $original = get_post( absint( $prepared_nav_item['menu-item-object-id'] ) );
 444                  if ( empty( $original ) ) {
 445                      $error->add( 'rest_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 400 ) );
 446                  } else {
 447                      $prepared_nav_item['menu-item-object'] = get_post_type( $original );
 448                  }
 449              }
 450          }
 451  
 452          // If post type archive, check if post type exists.
 453          if ( 'post_type_archive' === $prepared_nav_item['menu-item-type'] ) {
 454              $post_type = $prepared_nav_item['menu-item-object'] ? $prepared_nav_item['menu-item-object'] : false;
 455              $original  = get_post_type_object( $post_type );
 456              if ( ! $original ) {
 457                  $error->add( 'rest_post_invalid_type', __( 'Invalid post type.' ), array( 'status' => 400 ) );
 458              }
 459          }
 460  
 461          // Check if menu item is type custom, then title and url are required.
 462          if ( 'custom' === $prepared_nav_item['menu-item-type'] ) {
 463              if ( '' === $prepared_nav_item['menu-item-title'] ) {
 464                  $error->add( 'rest_title_required', __( 'The title is required when using a custom menu item type.' ), array( 'status' => 400 ) );
 465              }
 466              if ( empty( $prepared_nav_item['menu-item-url'] ) ) {
 467                  $error->add( 'rest_url_required', __( 'The url is required when using a custom menu item type.' ), array( 'status' => 400 ) );
 468              }
 469          }
 470  
 471          if ( $error->has_errors() ) {
 472              return $error;
 473          }
 474  
 475          // The xfn and classes properties are arrays, but passed to wp_update_nav_menu_item as a string.
 476          foreach ( array( 'menu-item-xfn', 'menu-item-classes' ) as $key ) {
 477              $prepared_nav_item[ $key ] = implode( ' ', $prepared_nav_item[ $key ] );
 478          }
 479  
 480          // Only draft / publish are valid post status for menu items.
 481          if ( 'publish' !== $prepared_nav_item['menu-item-status'] ) {
 482              $prepared_nav_item['menu-item-status'] = 'draft';
 483          }
 484  
 485          $prepared_nav_item = (object) $prepared_nav_item;
 486  
 487          /**
 488           * Filters a menu item before it is inserted via the REST API.
 489           *
 490           * @since 5.9.0
 491           *
 492           * @param object          $prepared_nav_item An object representing a single menu item prepared
 493           *                                           for inserting or updating the database.
 494           * @param WP_REST_Request $request           Request object.
 495           */
 496          return apply_filters( 'rest_pre_insert_nav_menu_item', $prepared_nav_item, $request );
 497      }
 498  
 499      /**
 500       * Prepares a single nav menu item output for response.
 501       *
 502       * @since 5.9.0
 503       *
 504       * @param WP_Post         $item    Post object.
 505       * @param WP_REST_Request $request Request object.
 506       * @return WP_REST_Response Response object.
 507       */
 508  	public function prepare_item_for_response( $item, $request ) {
 509          // Base fields for every post.
 510          $fields    = $this->get_fields_for_response( $request );
 511          $menu_item = $this->get_nav_menu_item( $item->ID );
 512          $data      = array();
 513  
 514          if ( rest_is_field_included( 'id', $fields ) ) {
 515              $data['id'] = $menu_item->ID;
 516          }
 517  
 518          if ( rest_is_field_included( 'title', $fields ) ) {
 519              $data['title'] = array();
 520          }
 521  
 522          if ( rest_is_field_included( 'title.raw', $fields ) ) {
 523              $data['title']['raw'] = $menu_item->title;
 524          }
 525  
 526          if ( rest_is_field_included( 'title.rendered', $fields ) ) {
 527              add_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
 528              add_filter( 'private_title_format', array( $this, 'protected_title_format' ) );
 529  
 530              /** This filter is documented in wp-includes/post-template.php */
 531              $title = apply_filters( 'the_title', $menu_item->title, $menu_item->ID );
 532  
 533              $data['title']['rendered'] = $title;
 534  
 535              remove_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
 536              remove_filter( 'private_title_format', array( $this, 'protected_title_format' ) );
 537          }
 538  
 539          if ( rest_is_field_included( 'status', $fields ) ) {
 540              $data['status'] = $menu_item->post_status;
 541          }
 542  
 543          if ( rest_is_field_included( 'url', $fields ) ) {
 544              $data['url'] = $menu_item->url;
 545          }
 546  
 547          if ( rest_is_field_included( 'attr_title', $fields ) ) {
 548              // Same as post_excerpt.
 549              $data['attr_title'] = $menu_item->attr_title;
 550          }
 551  
 552          if ( rest_is_field_included( 'description', $fields ) ) {
 553              // Same as post_content.
 554              $data['description'] = $menu_item->description;
 555          }
 556  
 557          if ( rest_is_field_included( 'type', $fields ) ) {
 558              $data['type'] = $menu_item->type;
 559          }
 560  
 561          if ( rest_is_field_included( 'type_label', $fields ) ) {
 562              $data['type_label'] = $menu_item->type_label;
 563          }
 564  
 565          if ( rest_is_field_included( 'object', $fields ) ) {
 566              $data['object'] = $menu_item->object;
 567          }
 568  
 569          if ( rest_is_field_included( 'object_id', $fields ) ) {
 570              // It is stored as a string, but should be exposed as an integer.
 571              $data['object_id'] = absint( $menu_item->object_id );
 572          }
 573  
 574          if ( rest_is_field_included( 'parent', $fields ) ) {
 575              // Same as post_parent, exposed as an integer.
 576              $data['parent'] = (int) $menu_item->menu_item_parent;
 577          }
 578  
 579          if ( rest_is_field_included( 'menu_order', $fields ) ) {
 580              // Same as post_parent, exposed as an integer.
 581              $data['menu_order'] = (int) $menu_item->menu_order;
 582          }
 583  
 584          if ( rest_is_field_included( 'target', $fields ) ) {
 585              $data['target'] = $menu_item->target;
 586          }
 587  
 588          if ( rest_is_field_included( 'classes', $fields ) ) {
 589              $data['classes'] = (array) $menu_item->classes;
 590          }
 591  
 592          if ( rest_is_field_included( 'xfn', $fields ) ) {
 593              $data['xfn'] = array_map( 'sanitize_html_class', explode( ' ', $menu_item->xfn ) );
 594          }
 595  
 596          if ( rest_is_field_included( 'invalid', $fields ) ) {
 597              $data['invalid'] = (bool) $menu_item->_invalid;
 598          }
 599  
 600          if ( rest_is_field_included( 'meta', $fields ) ) {
 601              $data['meta'] = $this->meta->get_value( $menu_item->ID, $request );
 602          }
 603  
 604          $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
 605  
 606          foreach ( $taxonomies as $taxonomy ) {
 607              $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
 608  
 609              if ( rest_is_field_included( $base, $fields ) ) {
 610                  $terms = get_the_terms( $item, $taxonomy->name );
 611                  if ( ! is_array( $terms ) ) {
 612                      continue;
 613                  }
 614                  $term_ids = $terms ? array_values( wp_list_pluck( $terms, 'term_id' ) ) : array();
 615                  if ( 'nav_menu' === $taxonomy->name ) {
 616                      $data[ $base ] = $term_ids ? array_shift( $term_ids ) : 0;
 617                  } else {
 618                      $data[ $base ] = $term_ids;
 619                  }
 620              }
 621          }
 622  
 623          $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
 624          $data    = $this->add_additional_fields_to_object( $data, $request );
 625          $data    = $this->filter_response_by_context( $data, $context );
 626  
 627          // Wrap the data in a response object.
 628          $response = rest_ensure_response( $data );
 629  
 630          if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
 631              $links = $this->prepare_links( $item );
 632              $response->add_links( $links );
 633  
 634              if ( ! empty( $links['self']['href'] ) ) {
 635                  $actions = $this->get_available_actions( $item, $request );
 636  
 637                  $self = $links['self']['href'];
 638  
 639                  foreach ( $actions as $rel ) {
 640                      $response->add_link( $rel, $self );
 641                  }
 642              }
 643          }
 644  
 645          /**
 646           * Filters the menu item data for a REST API response.
 647           *
 648           * @since 5.9.0
 649           *
 650           * @param WP_REST_Response $response  The response object.
 651           * @param object           $menu_item Menu item setup by {@see wp_setup_nav_menu_item()}.
 652           * @param WP_REST_Request  $request   Request object.
 653           */
 654          return apply_filters( 'rest_prepare_nav_menu_item', $response, $menu_item, $request );
 655      }
 656  
 657      /**
 658       * Prepares links for the request.
 659       *
 660       * @since 5.9.0
 661       *
 662       * @param WP_Post $post Post object.
 663       * @return array Links for the given post.
 664       */
 665  	protected function prepare_links( $post ) {
 666          $links     = parent::prepare_links( $post );
 667          $menu_item = $this->get_nav_menu_item( $post->ID );
 668  
 669          if ( empty( $menu_item->object_id ) ) {
 670              return $links;
 671          }
 672  
 673          $path = '';
 674          $type = '';
 675          $key  = $menu_item->type;
 676          if ( 'post_type' === $menu_item->type ) {
 677              $path = rest_get_route_for_post( $menu_item->object_id );
 678              $type = get_post_type( $menu_item->object_id );
 679          } elseif ( 'taxonomy' === $menu_item->type ) {
 680              $path = rest_get_route_for_term( $menu_item->object_id );
 681              $type = get_term_field( 'taxonomy', $menu_item->object_id );
 682          }
 683  
 684          if ( $path && $type ) {
 685              $links['https://api.w.org/menu-item-object'][] = array(
 686                  'href'       => rest_url( $path ),
 687                  $key         => $type,
 688                  'embeddable' => true,
 689              );
 690          }
 691  
 692          return $links;
 693      }
 694  
 695      /**
 696       * Retrieves Link Description Objects that should be added to the Schema for the nav menu items collection.
 697       *
 698       * @since 5.9.0
 699       *
 700       * @return array
 701       */
 702  	protected function get_schema_links() {
 703          $links   = parent::get_schema_links();
 704          $href    = rest_url( "{$this->namespace}/{$this->rest_base}/{id}" );
 705          $links[] = array(
 706              'rel'          => 'https://api.w.org/menu-item-object',
 707              'title'        => __( 'Get linked object.' ),
 708              'href'         => $href,
 709              'targetSchema' => array(
 710                  'type'       => 'object',
 711                  'properties' => array(
 712                      'object' => array(
 713                          'type' => 'integer',
 714                      ),
 715                  ),
 716              ),
 717          );
 718  
 719          return $links;
 720      }
 721  
 722      /**
 723       * Retrieves the nav menu item's schema, conforming to JSON Schema.
 724       *
 725       * @since 5.9.0
 726       *
 727       * @return array Item schema data.
 728       */
 729  	public function get_item_schema() {
 730          if ( $this->schema ) {
 731              return $this->add_additional_fields_schema( $this->schema );
 732          }
 733  
 734          $schema = array(
 735              '$schema' => 'http://json-schema.org/draft-04/schema#',
 736              'title'   => $this->post_type,
 737              'type'    => 'object',
 738          );
 739  
 740          $schema['properties']['title'] = array(
 741              'description' => __( 'The title for the object.' ),
 742              'type'        => array( 'string', 'object' ),
 743              'context'     => array( 'view', 'edit', 'embed' ),
 744              'properties'  => array(
 745                  'raw'      => array(
 746                      'description' => __( 'Title for the object, as it exists in the database.' ),
 747                      'type'        => 'string',
 748                      'context'     => array( 'edit' ),
 749                  ),
 750                  'rendered' => array(
 751                      'description' => __( 'HTML title for the object, transformed for display.' ),
 752                      'type'        => 'string',
 753                      'context'     => array( 'view', 'edit', 'embed' ),
 754                      'readonly'    => true,
 755                  ),
 756              ),
 757          );
 758  
 759          $schema['properties']['id'] = array(
 760              'description' => __( 'Unique identifier for the object.' ),
 761              'type'        => 'integer',
 762              'default'     => 0,
 763              'minimum'     => 0,
 764              'context'     => array( 'view', 'edit', 'embed' ),
 765              'readonly'    => true,
 766          );
 767  
 768          $schema['properties']['type_label'] = array(
 769              'description' => __( 'The singular label used to describe this type of menu item.' ),
 770              'type'        => 'string',
 771              'context'     => array( 'view', 'edit', 'embed' ),
 772              'readonly'    => true,
 773          );
 774  
 775          $schema['properties']['type'] = array(
 776              'description' => __( 'The family of objects originally represented, such as "post_type" or "taxonomy".' ),
 777              'type'        => 'string',
 778              'enum'        => array( 'taxonomy', 'post_type', 'post_type_archive', 'custom' ),
 779              'context'     => array( 'view', 'edit', 'embed' ),
 780              'default'     => 'custom',
 781          );
 782  
 783          $schema['properties']['status'] = array(
 784              'description' => __( 'A named status for the object.' ),
 785              'type'        => 'string',
 786              'enum'        => array_keys( get_post_stati( array( 'internal' => false ) ) ),
 787              'default'     => 'publish',
 788              'context'     => array( 'view', 'edit', 'embed' ),
 789          );
 790  
 791          $schema['properties']['parent'] = array(
 792              'description' => __( 'The ID for the parent of the object.' ),
 793              'type'        => 'integer',
 794              'minimum'     => 0,
 795              'default'     => 0,
 796              'context'     => array( 'view', 'edit', 'embed' ),
 797          );
 798  
 799          $schema['properties']['attr_title'] = array(
 800              'description' => __( 'Text for the title attribute of the link element for this menu item.' ),
 801              'type'        => 'string',
 802              'context'     => array( 'view', 'edit', 'embed' ),
 803              'arg_options' => array(
 804                  'sanitize_callback' => 'sanitize_text_field',
 805              ),
 806          );
 807  
 808          $schema['properties']['classes'] = array(
 809              'description' => __( 'Class names for the link element of this menu item.' ),
 810              'type'        => 'array',
 811              'items'       => array(
 812                  'type' => 'string',
 813              ),
 814              'context'     => array( 'view', 'edit', 'embed' ),
 815              'arg_options' => array(
 816                  'sanitize_callback' => static function ( $value ) {
 817                      return array_map( 'sanitize_html_class', wp_parse_list( $value ) );
 818                  },
 819              ),
 820          );
 821  
 822          $schema['properties']['description'] = array(
 823              'description' => __( 'The description of this menu item.' ),
 824              'type'        => 'string',
 825              'context'     => array( 'view', 'edit', 'embed' ),
 826              'arg_options' => array(
 827                  'sanitize_callback' => 'sanitize_text_field',
 828              ),
 829          );
 830  
 831          $schema['properties']['menu_order'] = array(
 832              'description' => __( 'The DB ID of the nav_menu_item that is this item\'s menu parent, if any, otherwise 0.' ),
 833              'context'     => array( 'view', 'edit', 'embed' ),
 834              'type'        => 'integer',
 835              'minimum'     => 1,
 836              'default'     => 1,
 837          );
 838  
 839          $schema['properties']['object'] = array(
 840              'description' => __( 'The type of object originally represented, such as "category", "post", or "attachment".' ),
 841              'context'     => array( 'view', 'edit', 'embed' ),
 842              'type'        => 'string',
 843              'arg_options' => array(
 844                  'sanitize_callback' => 'sanitize_key',
 845              ),
 846          );
 847  
 848          $schema['properties']['object_id'] = array(
 849              'description' => __( 'The database ID of the original object this menu item represents, for example the ID for posts or the term_id for categories.' ),
 850              'context'     => array( 'view', 'edit', 'embed' ),
 851              'type'        => 'integer',
 852              'minimum'     => 0,
 853              'default'     => 0,
 854          );
 855  
 856          $schema['properties']['target'] = array(
 857              'description' => __( 'The target attribute of the link element for this menu item.' ),
 858              'type'        => 'string',
 859              'context'     => array( 'view', 'edit', 'embed' ),
 860              'enum'        => array(
 861                  '_blank',
 862                  '',
 863              ),
 864          );
 865  
 866          $schema['properties']['url'] = array(
 867              'description' => __( 'The URL to which this menu item points.' ),
 868              'type'        => 'string',
 869              'format'      => 'uri',
 870              'context'     => array( 'view', 'edit', 'embed' ),
 871              'arg_options' => array(
 872                  'validate_callback' => static function ( $url ) {
 873                      if ( '' === $url ) {
 874                          return true;
 875                      }
 876  
 877                      if ( sanitize_url( $url ) ) {
 878                          return true;
 879                      }
 880  
 881                      return new WP_Error(
 882                          'rest_invalid_url',
 883                          __( 'Invalid URL.' )
 884                      );
 885                  },
 886              ),
 887          );
 888  
 889          $schema['properties']['xfn'] = array(
 890              'description' => __( 'The XFN relationship expressed in the link of this menu item.' ),
 891              'type'        => 'array',
 892              'items'       => array(
 893                  'type' => 'string',
 894              ),
 895              'context'     => array( 'view', 'edit', 'embed' ),
 896              'arg_options' => array(
 897                  'sanitize_callback' => static function ( $value ) {
 898                      return array_map( 'sanitize_html_class', wp_parse_list( $value ) );
 899                  },
 900              ),
 901          );
 902  
 903          $schema['properties']['invalid'] = array(
 904              'description' => __( 'Whether the menu item represents an object that no longer exists.' ),
 905              'context'     => array( 'view', 'edit', 'embed' ),
 906              'type'        => 'boolean',
 907              'readonly'    => true,
 908          );
 909  
 910          $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
 911  
 912          foreach ( $taxonomies as $taxonomy ) {
 913              $base                          = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
 914              $schema['properties'][ $base ] = array(
 915                  /* translators: %s: taxonomy name */
 916                  'description' => sprintf( __( 'The terms assigned to the object in the %s taxonomy.' ), $taxonomy->name ),
 917                  'type'        => 'array',
 918                  'items'       => array(
 919                      'type' => 'integer',
 920                  ),
 921                  'context'     => array( 'view', 'edit' ),
 922              );
 923  
 924              if ( 'nav_menu' === $taxonomy->name ) {
 925                  $schema['properties'][ $base ]['type'] = 'integer';
 926                  unset( $schema['properties'][ $base ]['items'] );
 927              }
 928          }
 929  
 930          $schema['properties']['meta'] = $this->meta->get_field_schema();
 931  
 932          $schema_links = $this->get_schema_links();
 933  
 934          if ( $schema_links ) {
 935              $schema['links'] = $schema_links;
 936          }
 937  
 938          $this->schema = $schema;
 939  
 940          return $this->add_additional_fields_schema( $this->schema );
 941      }
 942  
 943      /**
 944       * Retrieves the query params for the nav menu items collection.
 945       *
 946       * @since 5.9.0
 947       *
 948       * @return array Collection parameters.
 949       */
 950  	public function get_collection_params() {
 951          $query_params = parent::get_collection_params();
 952  
 953          $query_params['menu_order'] = array(
 954              'description' => __( 'Limit result set to posts with a specific menu_order value.' ),
 955              'type'        => 'integer',
 956          );
 957  
 958          $query_params['order'] = array(
 959              'description' => __( 'Order sort attribute ascending or descending.' ),
 960              'type'        => 'string',
 961              'default'     => 'asc',
 962              'enum'        => array( 'asc', 'desc' ),
 963          );
 964  
 965          $query_params['orderby'] = array(
 966              'description' => __( 'Sort collection by object attribute.' ),
 967              'type'        => 'string',
 968              'default'     => 'menu_order',
 969              'enum'        => array(
 970                  'author',
 971                  'date',
 972                  'id',
 973                  'include',
 974                  'modified',
 975                  'parent',
 976                  'relevance',
 977                  'slug',
 978                  'include_slugs',
 979                  'title',
 980                  'menu_order',
 981              ),
 982          );
 983          // Change default to 100 items.
 984          $query_params['per_page']['default'] = 100;
 985  
 986          return $query_params;
 987      }
 988  
 989      /**
 990       * Determines the allowed query_vars for a get_items() response and prepares
 991       * them for WP_Query.
 992       *
 993       * @since 5.9.0
 994       *
 995       * @param array           $prepared_args Optional. Prepared WP_Query arguments. Default empty array.
 996       * @param WP_REST_Request $request       Optional. Full details about the request.
 997       * @return array Items query arguments.
 998       */
 999  	protected function prepare_items_query( $prepared_args = array(), $request = null ) {
1000          $query_args = parent::prepare_items_query( $prepared_args, $request );
1001  
1002          // Map to proper WP_Query orderby param.
1003          if ( isset( $query_args['orderby'], $request['orderby'] ) ) {
1004              $orderby_mappings = array(
1005                  'id'            => 'ID',
1006                  'include'       => 'post__in',
1007                  'slug'          => 'post_name',
1008                  'include_slugs' => 'post_name__in',
1009                  'menu_order'    => 'menu_order',
1010              );
1011  
1012              if ( isset( $orderby_mappings[ $request['orderby'] ] ) ) {
1013                  $query_args['orderby'] = $orderby_mappings[ $request['orderby'] ];
1014              }
1015          }
1016  
1017          $query_args['update_menu_item_cache'] = true;
1018  
1019          return $query_args;
1020      }
1021  
1022      /**
1023       * Gets the id of the menu that the given menu item belongs to.
1024       *
1025       * @since 5.9.0
1026       *
1027       * @param int $menu_item_id Menu item id.
1028       * @return int
1029       */
1030  	protected function get_menu_id( $menu_item_id ) {
1031          $menu_ids = wp_get_post_terms( $menu_item_id, 'nav_menu', array( 'fields' => 'ids' ) );
1032          $menu_id  = 0;
1033          if ( $menu_ids && ! is_wp_error( $menu_ids ) ) {
1034              $menu_id = array_shift( $menu_ids );
1035          }
1036  
1037          return $menu_id;
1038      }
1039  }


Generated : Fri Feb 21 08:20:01 2025 Cross-referenced by PHPXref