[ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * REST API: WP_REST_Attachments_Controller class 4 * 5 * @package WordPress 6 * @subpackage REST_API 7 * @since 4.7.0 8 */ 9 10 /** 11 * Core controller used to access attachments via the REST API. 12 * 13 * @since 4.7.0 14 * 15 * @see WP_REST_Posts_Controller 16 */ 17 class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller { 18 19 /** 20 * Registers the routes for attachments. 21 * 22 * @since 5.3.0 23 * 24 * @see register_rest_route() 25 */ 26 public function register_routes() { 27 parent::register_routes(); 28 register_rest_route( 29 $this->namespace, 30 '/' . $this->rest_base . '/(?P<id>[\d]+)/post-process', 31 array( 32 'methods' => WP_REST_Server::CREATABLE, 33 'callback' => array( $this, 'post_process_item' ), 34 'permission_callback' => array( $this, 'post_process_item_permissions_check' ), 35 'args' => array( 36 'id' => array( 37 'description' => __( 'Unique identifier for the object.' ), 38 'type' => 'integer', 39 ), 40 'action' => array( 41 'type' => 'string', 42 'enum' => array( 'create-image-subsizes' ), 43 'required' => true, 44 ), 45 ), 46 ) 47 ); 48 register_rest_route( 49 $this->namespace, 50 '/' . $this->rest_base . '/(?P<id>[\d]+)/edit', 51 array( 52 'methods' => WP_REST_Server::CREATABLE, 53 'callback' => array( $this, 'edit_media_item' ), 54 'permission_callback' => array( $this, 'edit_media_item_permissions_check' ), 55 'args' => $this->get_edit_media_item_args(), 56 ) 57 ); 58 } 59 60 /** 61 * Determines the allowed query_vars for a get_items() response and 62 * prepares for WP_Query. 63 * 64 * @since 4.7.0 65 * 66 * @param array $prepared_args Optional. Array of prepared arguments. Default empty array. 67 * @param WP_REST_Request $request Optional. Request to prepare items for. 68 * @return array Array of query arguments. 69 */ 70 protected function prepare_items_query( $prepared_args = array(), $request = null ) { 71 $query_args = parent::prepare_items_query( $prepared_args, $request ); 72 73 if ( empty( $query_args['post_status'] ) ) { 74 $query_args['post_status'] = 'inherit'; 75 } 76 77 $media_types = $this->get_media_types(); 78 79 if ( ! empty( $request['media_type'] ) && isset( $media_types[ $request['media_type'] ] ) ) { 80 $query_args['post_mime_type'] = $media_types[ $request['media_type'] ]; 81 } 82 83 if ( ! empty( $request['mime_type'] ) ) { 84 $parts = explode( '/', $request['mime_type'] ); 85 if ( isset( $media_types[ $parts[0] ] ) && in_array( $request['mime_type'], $media_types[ $parts[0] ], true ) ) { 86 $query_args['post_mime_type'] = $request['mime_type']; 87 } 88 } 89 90 // Filter query clauses to include filenames. 91 if ( isset( $query_args['s'] ) ) { 92 add_filter( 'posts_clauses', '_filter_query_attachment_filenames' ); 93 } 94 95 return $query_args; 96 } 97 98 /** 99 * Checks if a given request has access to create an attachment. 100 * 101 * @since 4.7.0 102 * 103 * @param WP_REST_Request $request Full details about the request. 104 * @return true|WP_Error Boolean true if the attachment may be created, or a WP_Error if not. 105 */ 106 public function create_item_permissions_check( $request ) { 107 $ret = parent::create_item_permissions_check( $request ); 108 109 if ( ! $ret || is_wp_error( $ret ) ) { 110 return $ret; 111 } 112 113 if ( ! current_user_can( 'upload_files' ) ) { 114 return new WP_Error( 115 'rest_cannot_create', 116 __( 'Sorry, you are not allowed to upload media on this site.' ), 117 array( 'status' => 400 ) 118 ); 119 } 120 121 // Attaching media to a post requires ability to edit said post. 122 if ( ! empty( $request['post'] ) && ! current_user_can( 'edit_post', (int) $request['post'] ) ) { 123 return new WP_Error( 124 'rest_cannot_edit', 125 __( 'Sorry, you are not allowed to upload media to this post.' ), 126 array( 'status' => rest_authorization_required_code() ) 127 ); 128 } 129 130 return true; 131 } 132 133 /** 134 * Creates a single attachment. 135 * 136 * @since 4.7.0 137 * 138 * @param WP_REST_Request $request Full details about the request. 139 * @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure. 140 */ 141 public function create_item( $request ) { 142 if ( ! empty( $request['post'] ) && in_array( get_post_type( $request['post'] ), array( 'revision', 'attachment' ), true ) ) { 143 return new WP_Error( 144 'rest_invalid_param', 145 __( 'Invalid parent type.' ), 146 array( 'status' => 400 ) 147 ); 148 } 149 150 $insert = $this->insert_attachment( $request ); 151 152 if ( is_wp_error( $insert ) ) { 153 return $insert; 154 } 155 156 $schema = $this->get_item_schema(); 157 158 // Extract by name. 159 $attachment_id = $insert['attachment_id']; 160 $file = $insert['file']; 161 162 if ( isset( $request['alt_text'] ) ) { 163 update_post_meta( $attachment_id, '_wp_attachment_image_alt', sanitize_text_field( $request['alt_text'] ) ); 164 } 165 166 if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) { 167 $meta_update = $this->meta->update_value( $request['meta'], $attachment_id ); 168 169 if ( is_wp_error( $meta_update ) ) { 170 return $meta_update; 171 } 172 } 173 174 $attachment = get_post( $attachment_id ); 175 $fields_update = $this->update_additional_fields_for_object( $attachment, $request ); 176 177 if ( is_wp_error( $fields_update ) ) { 178 return $fields_update; 179 } 180 181 $request->set_param( 'context', 'edit' ); 182 183 /** 184 * Fires after a single attachment is completely created or updated via the REST API. 185 * 186 * @since 5.0.0 187 * 188 * @param WP_Post $attachment Inserted or updated attachment object. 189 * @param WP_REST_Request $request Request object. 190 * @param bool $creating True when creating an attachment, false when updating. 191 */ 192 do_action( 'rest_after_insert_attachment', $attachment, $request, true ); 193 194 wp_after_insert_post( $attachment, false, null ); 195 196 if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { 197 // Set a custom header with the attachment_id. 198 // Used by the browser/client to resume creating image sub-sizes after a PHP fatal error. 199 header( 'X-WP-Upload-Attachment-ID: ' . $attachment_id ); 200 } 201 202 // Include media and image functions to get access to wp_generate_attachment_metadata(). 203 require_once ABSPATH . 'wp-admin/includes/media.php'; 204 require_once ABSPATH . 'wp-admin/includes/image.php'; 205 206 // Post-process the upload (create image sub-sizes, make PDF thumbnails, etc.) and insert attachment meta. 207 // At this point the server may run out of resources and post-processing of uploaded images may fail. 208 wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $file ) ); 209 210 $response = $this->prepare_item_for_response( $attachment, $request ); 211 $response = rest_ensure_response( $response ); 212 $response->set_status( 201 ); 213 $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $attachment_id ) ) ); 214 215 return $response; 216 } 217 218 /** 219 * Inserts the attachment post in the database. Does not update the attachment meta. 220 * 221 * @since 5.3.0 222 * 223 * @param WP_REST_Request $request 224 * @return array|WP_Error 225 */ 226 protected function insert_attachment( $request ) { 227 // Get the file via $_FILES or raw data. 228 $files = $request->get_file_params(); 229 $headers = $request->get_headers(); 230 231 if ( ! empty( $files ) ) { 232 $file = $this->upload_from_file( $files, $headers ); 233 } else { 234 $file = $this->upload_from_data( $request->get_body(), $headers ); 235 } 236 237 if ( is_wp_error( $file ) ) { 238 return $file; 239 } 240 241 $name = wp_basename( $file['file'] ); 242 $name_parts = pathinfo( $name ); 243 $name = trim( substr( $name, 0, -( 1 + strlen( $name_parts['extension'] ) ) ) ); 244 245 $url = $file['url']; 246 $type = $file['type']; 247 $file = $file['file']; 248 249 // Include image functions to get access to wp_read_image_metadata(). 250 require_once ABSPATH . 'wp-admin/includes/image.php'; 251 252 // Use image exif/iptc data for title and caption defaults if possible. 253 $image_meta = wp_read_image_metadata( $file ); 254 255 if ( ! empty( $image_meta ) ) { 256 if ( empty( $request['title'] ) && trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) { 257 $request['title'] = $image_meta['title']; 258 } 259 260 if ( empty( $request['caption'] ) && trim( $image_meta['caption'] ) ) { 261 $request['caption'] = $image_meta['caption']; 262 } 263 } 264 265 $attachment = $this->prepare_item_for_database( $request ); 266 267 $attachment->post_mime_type = $type; 268 $attachment->guid = $url; 269 270 if ( empty( $attachment->post_title ) ) { 271 $attachment->post_title = preg_replace( '/\.[^.]+$/', '', wp_basename( $file ) ); 272 } 273 274 // $post_parent is inherited from $attachment['post_parent']. 275 $id = wp_insert_attachment( wp_slash( (array) $attachment ), $file, 0, true, false ); 276 277 if ( is_wp_error( $id ) ) { 278 if ( 'db_update_error' === $id->get_error_code() ) { 279 $id->add_data( array( 'status' => 500 ) ); 280 } else { 281 $id->add_data( array( 'status' => 400 ) ); 282 } 283 284 return $id; 285 } 286 287 $attachment = get_post( $id ); 288 289 /** 290 * Fires after a single attachment is created or updated via the REST API. 291 * 292 * @since 4.7.0 293 * 294 * @param WP_Post $attachment Inserted or updated attachment 295 * object. 296 * @param WP_REST_Request $request The request sent to the API. 297 * @param bool $creating True when creating an attachment, false when updating. 298 */ 299 do_action( 'rest_insert_attachment', $attachment, $request, true ); 300 301 return array( 302 'attachment_id' => $id, 303 'file' => $file, 304 ); 305 } 306 307 /** 308 * Updates a single attachment. 309 * 310 * @since 4.7.0 311 * 312 * @param WP_REST_Request $request Full details about the request. 313 * @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure. 314 */ 315 public function update_item( $request ) { 316 if ( ! empty( $request['post'] ) && in_array( get_post_type( $request['post'] ), array( 'revision', 'attachment' ), true ) ) { 317 return new WP_Error( 318 'rest_invalid_param', 319 __( 'Invalid parent type.' ), 320 array( 'status' => 400 ) 321 ); 322 } 323 324 $attachment_before = get_post( $request['id'] ); 325 $response = parent::update_item( $request ); 326 327 if ( is_wp_error( $response ) ) { 328 return $response; 329 } 330 331 $response = rest_ensure_response( $response ); 332 $data = $response->get_data(); 333 334 if ( isset( $request['alt_text'] ) ) { 335 update_post_meta( $data['id'], '_wp_attachment_image_alt', $request['alt_text'] ); 336 } 337 338 $attachment = get_post( $request['id'] ); 339 340 $fields_update = $this->update_additional_fields_for_object( $attachment, $request ); 341 342 if ( is_wp_error( $fields_update ) ) { 343 return $fields_update; 344 } 345 346 $request->set_param( 'context', 'edit' ); 347 348 /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php */ 349 do_action( 'rest_after_insert_attachment', $attachment, $request, false ); 350 351 wp_after_insert_post( $attachment, true, $attachment_before ); 352 353 $response = $this->prepare_item_for_response( $attachment, $request ); 354 $response = rest_ensure_response( $response ); 355 356 return $response; 357 } 358 359 /** 360 * Performs post processing on an attachment. 361 * 362 * @since 5.3.0 363 * 364 * @param WP_REST_Request $request Full details about the request. 365 * @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure. 366 */ 367 public function post_process_item( $request ) { 368 switch ( $request['action'] ) { 369 case 'create-image-subsizes': 370 require_once ABSPATH . 'wp-admin/includes/image.php'; 371 wp_update_image_subsizes( $request['id'] ); 372 break; 373 } 374 375 $request['context'] = 'edit'; 376 377 return $this->prepare_item_for_response( get_post( $request['id'] ), $request ); 378 } 379 380 /** 381 * Checks if a given request can perform post processing on an attachment. 382 * 383 * @since 5.3.0 384 * 385 * @param WP_REST_Request $request Full details about the request. 386 * @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise. 387 */ 388 public function post_process_item_permissions_check( $request ) { 389 return $this->update_item_permissions_check( $request ); 390 } 391 392 /** 393 * Checks if a given request has access to editing media. 394 * 395 * @since 5.5.0 396 * 397 * @param WP_REST_Request $request Full details about the request. 398 * @return true|WP_Error True if the request has read access, WP_Error object otherwise. 399 */ 400 public function edit_media_item_permissions_check( $request ) { 401 if ( ! current_user_can( 'upload_files' ) ) { 402 return new WP_Error( 403 'rest_cannot_edit_image', 404 __( 'Sorry, you are not allowed to upload media on this site.' ), 405 array( 'status' => rest_authorization_required_code() ) 406 ); 407 } 408 409 return $this->update_item_permissions_check( $request ); 410 } 411 412 /** 413 * Applies edits to a media item and creates a new attachment record. 414 * 415 * @since 5.5.0 416 * 417 * @param WP_REST_Request $request Full details about the request. 418 * @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure. 419 */ 420 public function edit_media_item( $request ) { 421 require_once ABSPATH . 'wp-admin/includes/image.php'; 422 423 $attachment_id = $request['id']; 424 425 // This also confirms the attachment is an image. 426 $image_file = wp_get_original_image_path( $attachment_id ); 427 $image_meta = wp_get_attachment_metadata( $attachment_id ); 428 429 if ( 430 ! $image_meta || 431 ! $image_file || 432 ! wp_image_file_matches_image_meta( $request['src'], $image_meta, $attachment_id ) 433 ) { 434 return new WP_Error( 435 'rest_unknown_attachment', 436 __( 'Unable to get meta information for file.' ), 437 array( 'status' => 404 ) 438 ); 439 } 440 441 $supported_types = array( 'image/jpeg', 'image/png', 'image/gif' ); 442 $mime_type = get_post_mime_type( $attachment_id ); 443 if ( ! in_array( $mime_type, $supported_types, true ) ) { 444 return new WP_Error( 445 'rest_cannot_edit_file_type', 446 __( 'This type of file cannot be edited.' ), 447 array( 'status' => 400 ) 448 ); 449 } 450 451 // Check if we need to do anything. 452 $rotate = 0; 453 $crop = false; 454 455 if ( ! empty( $request['rotation'] ) ) { 456 // Rotation direction: clockwise vs. counter clockwise. 457 $rotate = 0 - (int) $request['rotation']; 458 } 459 460 if ( isset( $request['x'], $request['y'], $request['width'], $request['height'] ) ) { 461 $crop = true; 462 } 463 464 if ( ! $rotate && ! $crop ) { 465 return new WP_Error( 466 'rest_image_not_edited', 467 __( 'The image was not edited. Edit the image before applying the changes.' ), 468 array( 'status' => 400 ) 469 ); 470 } 471 472 /* 473 * If the file doesn't exist, attempt a URL fopen on the src link. 474 * This can occur with certain file replication plugins. 475 * Keep the original file path to get a modified name later. 476 */ 477 $image_file_to_edit = $image_file; 478 if ( ! file_exists( $image_file_to_edit ) ) { 479 $image_file_to_edit = _load_image_to_edit_path( $attachment_id ); 480 } 481 482 $image_editor = wp_get_image_editor( $image_file_to_edit ); 483 484 if ( is_wp_error( $image_editor ) ) { 485 return new WP_Error( 486 'rest_unknown_image_file_type', 487 __( 'Unable to edit this image.' ), 488 array( 'status' => 500 ) 489 ); 490 } 491 492 if ( 0 !== $rotate ) { 493 $result = $image_editor->rotate( $rotate ); 494 495 if ( is_wp_error( $result ) ) { 496 return new WP_Error( 497 'rest_image_rotation_failed', 498 __( 'Unable to rotate this image.' ), 499 array( 'status' => 500 ) 500 ); 501 } 502 } 503 504 if ( $crop ) { 505 $size = $image_editor->get_size(); 506 507 $crop_x = round( ( $size['width'] * (float) $request['x'] ) / 100.0 ); 508 $crop_y = round( ( $size['height'] * (float) $request['y'] ) / 100.0 ); 509 $width = round( ( $size['width'] * (float) $request['width'] ) / 100.0 ); 510 $height = round( ( $size['height'] * (float) $request['height'] ) / 100.0 ); 511 512 $result = $image_editor->crop( $crop_x, $crop_y, $width, $height ); 513 514 if ( is_wp_error( $result ) ) { 515 return new WP_Error( 516 'rest_image_crop_failed', 517 __( 'Unable to crop this image.' ), 518 array( 'status' => 500 ) 519 ); 520 } 521 } 522 523 // Calculate the file name. 524 $image_ext = pathinfo( $image_file, PATHINFO_EXTENSION ); 525 $image_name = wp_basename( $image_file, ".{$image_ext}" ); 526 527 // Do not append multiple `-edited` to the file name. 528 // The user may be editing a previously edited image. 529 if ( preg_match( '/-edited(-\d+)?$/', $image_name ) ) { 530 // Remove any `-1`, `-2`, etc. `wp_unique_filename()` will add the proper number. 531 $image_name = preg_replace( '/-edited(-\d+)?$/', '-edited', $image_name ); 532 } else { 533 // Append `-edited` before the extension. 534 $image_name .= '-edited'; 535 } 536 537 $filename = "{$image_name}.{$image_ext}"; 538 539 // Create the uploads sub-directory if needed. 540 $uploads = wp_upload_dir(); 541 542 // Make the file name unique in the (new) upload directory. 543 $filename = wp_unique_filename( $uploads['path'], $filename ); 544 545 // Save to disk. 546 $saved = $image_editor->save( $uploads['path'] . "/$filename" ); 547 548 if ( is_wp_error( $saved ) ) { 549 return $saved; 550 } 551 552 // Create new attachment post. 553 $new_attachment_post = array( 554 'post_mime_type' => $saved['mime-type'], 555 'guid' => $uploads['url'] . "/$filename", 556 'post_title' => $image_name, 557 'post_content' => '', 558 ); 559 560 // Copy post_content, post_excerpt, and post_title from the edited image's attachment post. 561 $attachment_post = get_post( $attachment_id ); 562 563 if ( $attachment_post ) { 564 $new_attachment_post['post_content'] = $attachment_post->post_content; 565 $new_attachment_post['post_excerpt'] = $attachment_post->post_excerpt; 566 $new_attachment_post['post_title'] = $attachment_post->post_title; 567 } 568 569 $new_attachment_id = wp_insert_attachment( wp_slash( $new_attachment_post ), $saved['path'], 0, true ); 570 571 if ( is_wp_error( $new_attachment_id ) ) { 572 if ( 'db_update_error' === $new_attachment_id->get_error_code() ) { 573 $new_attachment_id->add_data( array( 'status' => 500 ) ); 574 } else { 575 $new_attachment_id->add_data( array( 'status' => 400 ) ); 576 } 577 578 return $new_attachment_id; 579 } 580 581 // Copy the image alt text from the edited image. 582 $image_alt = get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ); 583 584 if ( ! empty( $image_alt ) ) { 585 // update_post_meta() expects slashed. 586 update_post_meta( $new_attachment_id, '_wp_attachment_image_alt', wp_slash( $image_alt ) ); 587 } 588 589 if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { 590 // Set a custom header with the attachment_id. 591 // Used by the browser/client to resume creating image sub-sizes after a PHP fatal error. 592 header( 'X-WP-Upload-Attachment-ID: ' . $new_attachment_id ); 593 } 594 595 // Generate image sub-sizes and meta. 596 $new_image_meta = wp_generate_attachment_metadata( $new_attachment_id, $saved['path'] ); 597 598 // Copy the EXIF metadata from the original attachment if not generated for the edited image. 599 if ( isset( $image_meta['image_meta'] ) && isset( $new_image_meta['image_meta'] ) && is_array( $new_image_meta['image_meta'] ) ) { 600 // Merge but skip empty values. 601 foreach ( (array) $image_meta['image_meta'] as $key => $value ) { 602 if ( empty( $new_image_meta['image_meta'][ $key ] ) && ! empty( $value ) ) { 603 $new_image_meta['image_meta'][ $key ] = $value; 604 } 605 } 606 } 607 608 // Reset orientation. At this point the image is edited and orientation is correct. 609 if ( ! empty( $new_image_meta['image_meta']['orientation'] ) ) { 610 $new_image_meta['image_meta']['orientation'] = 1; 611 } 612 613 // The attachment_id may change if the site is exported and imported. 614 $new_image_meta['parent_image'] = array( 615 'attachment_id' => $attachment_id, 616 // Path to the originally uploaded image file relative to the uploads directory. 617 'file' => _wp_relative_upload_path( $image_file ), 618 ); 619 620 /** 621 * Filters the meta data for the new image created by editing an existing image. 622 * 623 * @since 5.5.0 624 * 625 * @param array $new_image_meta Meta data for the new image. 626 * @param int $new_attachment_id Attachment post ID for the new image. 627 * @param int $attachment_id Attachment post ID for the edited (parent) image. 628 */ 629 $new_image_meta = apply_filters( 'wp_edited_image_metadata', $new_image_meta, $new_attachment_id, $attachment_id ); 630 631 wp_update_attachment_metadata( $new_attachment_id, $new_image_meta ); 632 633 $response = $this->prepare_item_for_response( get_post( $new_attachment_id ), $request ); 634 $response->set_status( 201 ); 635 $response->header( 'Location', rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $new_attachment_id ) ) ); 636 637 return $response; 638 } 639 640 /** 641 * Prepares a single attachment for create or update. 642 * 643 * @since 4.7.0 644 * 645 * @param WP_REST_Request $request Request object. 646 * @return stdClass|WP_Error Post object. 647 */ 648 protected function prepare_item_for_database( $request ) { 649 $prepared_attachment = parent::prepare_item_for_database( $request ); 650 651 // Attachment caption (post_excerpt internally). 652 if ( isset( $request['caption'] ) ) { 653 if ( is_string( $request['caption'] ) ) { 654 $prepared_attachment->post_excerpt = $request['caption']; 655 } elseif ( isset( $request['caption']['raw'] ) ) { 656 $prepared_attachment->post_excerpt = $request['caption']['raw']; 657 } 658 } 659 660 // Attachment description (post_content internally). 661 if ( isset( $request['description'] ) ) { 662 if ( is_string( $request['description'] ) ) { 663 $prepared_attachment->post_content = $request['description']; 664 } elseif ( isset( $request['description']['raw'] ) ) { 665 $prepared_attachment->post_content = $request['description']['raw']; 666 } 667 } 668 669 if ( isset( $request['post'] ) ) { 670 $prepared_attachment->post_parent = (int) $request['post']; 671 } 672 673 return $prepared_attachment; 674 } 675 676 /** 677 * Prepares a single attachment output for response. 678 * 679 * @since 4.7.0 680 * 681 * @param WP_Post $post Attachment object. 682 * @param WP_REST_Request $request Request object. 683 * @return WP_REST_Response Response object. 684 */ 685 public function prepare_item_for_response( $post, $request ) { 686 $response = parent::prepare_item_for_response( $post, $request ); 687 $fields = $this->get_fields_for_response( $request ); 688 $data = $response->get_data(); 689 690 if ( in_array( 'description', $fields, true ) ) { 691 $data['description'] = array( 692 'raw' => $post->post_content, 693 /** This filter is documented in wp-includes/post-template.php */ 694 'rendered' => apply_filters( 'the_content', $post->post_content ), 695 ); 696 } 697 698 if ( in_array( 'caption', $fields, true ) ) { 699 /** This filter is documented in wp-includes/post-template.php */ 700 $caption = apply_filters( 'get_the_excerpt', $post->post_excerpt, $post ); 701 702 /** This filter is documented in wp-includes/post-template.php */ 703 $caption = apply_filters( 'the_excerpt', $caption ); 704 705 $data['caption'] = array( 706 'raw' => $post->post_excerpt, 707 'rendered' => $caption, 708 ); 709 } 710 711 if ( in_array( 'alt_text', $fields, true ) ) { 712 $data['alt_text'] = get_post_meta( $post->ID, '_wp_attachment_image_alt', true ); 713 } 714 715 if ( in_array( 'media_type', $fields, true ) ) { 716 $data['media_type'] = wp_attachment_is_image( $post->ID ) ? 'image' : 'file'; 717 } 718 719 if ( in_array( 'mime_type', $fields, true ) ) { 720 $data['mime_type'] = $post->post_mime_type; 721 } 722 723 if ( in_array( 'media_details', $fields, true ) ) { 724 $data['media_details'] = wp_get_attachment_metadata( $post->ID ); 725 726 // Ensure empty details is an empty object. 727 if ( empty( $data['media_details'] ) ) { 728 $data['media_details'] = new stdClass; 729 } elseif ( ! empty( $data['media_details']['sizes'] ) ) { 730 731 foreach ( $data['media_details']['sizes'] as $size => &$size_data ) { 732 733 if ( isset( $size_data['mime-type'] ) ) { 734 $size_data['mime_type'] = $size_data['mime-type']; 735 unset( $size_data['mime-type'] ); 736 } 737 738 // Use the same method image_downsize() does. 739 $image_src = wp_get_attachment_image_src( $post->ID, $size ); 740 if ( ! $image_src ) { 741 continue; 742 } 743 744 $size_data['source_url'] = $image_src[0]; 745 } 746 747 $full_src = wp_get_attachment_image_src( $post->ID, 'full' ); 748 749 if ( ! empty( $full_src ) ) { 750 $data['media_details']['sizes']['full'] = array( 751 'file' => wp_basename( $full_src[0] ), 752 'width' => $full_src[1], 753 'height' => $full_src[2], 754 'mime_type' => $post->post_mime_type, 755 'source_url' => $full_src[0], 756 ); 757 } 758 } else { 759 $data['media_details']['sizes'] = new stdClass; 760 } 761 } 762 763 if ( in_array( 'post', $fields, true ) ) { 764 $data['post'] = ! empty( $post->post_parent ) ? (int) $post->post_parent : null; 765 } 766 767 if ( in_array( 'source_url', $fields, true ) ) { 768 $data['source_url'] = wp_get_attachment_url( $post->ID ); 769 } 770 771 if ( in_array( 'missing_image_sizes', $fields, true ) ) { 772 require_once ABSPATH . 'wp-admin/includes/image.php'; 773 $data['missing_image_sizes'] = array_keys( wp_get_missing_image_subsizes( $post->ID ) ); 774 } 775 776 $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; 777 778 $data = $this->filter_response_by_context( $data, $context ); 779 780 $links = $response->get_links(); 781 782 // Wrap the data in a response object. 783 $response = rest_ensure_response( $data ); 784 785 foreach ( $links as $rel => $rel_links ) { 786 foreach ( $rel_links as $link ) { 787 $response->add_link( $rel, $link['href'], $link['attributes'] ); 788 } 789 } 790 791 /** 792 * Filters an attachment returned from the REST API. 793 * 794 * Allows modification of the attachment right before it is returned. 795 * 796 * @since 4.7.0 797 * 798 * @param WP_REST_Response $response The response object. 799 * @param WP_Post $post The original attachment post. 800 * @param WP_REST_Request $request Request used to generate the response. 801 */ 802 return apply_filters( 'rest_prepare_attachment', $response, $post, $request ); 803 } 804 805 /** 806 * Retrieves the attachment's schema, conforming to JSON Schema. 807 * 808 * @since 4.7.0 809 * 810 * @return array Item schema as an array. 811 */ 812 public function get_item_schema() { 813 if ( $this->schema ) { 814 return $this->add_additional_fields_schema( $this->schema ); 815 } 816 817 $schema = parent::get_item_schema(); 818 819 $schema['properties']['alt_text'] = array( 820 'description' => __( 'Alternative text to display when attachment is not displayed.' ), 821 'type' => 'string', 822 'context' => array( 'view', 'edit', 'embed' ), 823 'arg_options' => array( 824 'sanitize_callback' => 'sanitize_text_field', 825 ), 826 ); 827 828 $schema['properties']['caption'] = array( 829 'description' => __( 'The attachment caption.' ), 830 'type' => 'object', 831 'context' => array( 'view', 'edit', 'embed' ), 832 'arg_options' => array( 833 'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database(). 834 'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database(). 835 ), 836 'properties' => array( 837 'raw' => array( 838 'description' => __( 'Caption for the attachment, as it exists in the database.' ), 839 'type' => 'string', 840 'context' => array( 'edit' ), 841 ), 842 'rendered' => array( 843 'description' => __( 'HTML caption for the attachment, transformed for display.' ), 844 'type' => 'string', 845 'context' => array( 'view', 'edit', 'embed' ), 846 'readonly' => true, 847 ), 848 ), 849 ); 850 851 $schema['properties']['description'] = array( 852 'description' => __( 'The attachment description.' ), 853 'type' => 'object', 854 'context' => array( 'view', 'edit' ), 855 'arg_options' => array( 856 'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database(). 857 'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database(). 858 ), 859 'properties' => array( 860 'raw' => array( 861 'description' => __( 'Description for the object, as it exists in the database.' ), 862 'type' => 'string', 863 'context' => array( 'edit' ), 864 ), 865 'rendered' => array( 866 'description' => __( 'HTML description for the object, transformed for display.' ), 867 'type' => 'string', 868 'context' => array( 'view', 'edit' ), 869 'readonly' => true, 870 ), 871 ), 872 ); 873 874 $schema['properties']['media_type'] = array( 875 'description' => __( 'Attachment type.' ), 876 'type' => 'string', 877 'enum' => array( 'image', 'file' ), 878 'context' => array( 'view', 'edit', 'embed' ), 879 'readonly' => true, 880 ); 881 882 $schema['properties']['mime_type'] = array( 883 'description' => __( 'The attachment MIME type.' ), 884 'type' => 'string', 885 'context' => array( 'view', 'edit', 'embed' ), 886 'readonly' => true, 887 ); 888 889 $schema['properties']['media_details'] = array( 890 'description' => __( 'Details about the media file, specific to its type.' ), 891 'type' => 'object', 892 'context' => array( 'view', 'edit', 'embed' ), 893 'readonly' => true, 894 ); 895 896 $schema['properties']['post'] = array( 897 'description' => __( 'The ID for the associated post of the attachment.' ), 898 'type' => 'integer', 899 'context' => array( 'view', 'edit' ), 900 ); 901 902 $schema['properties']['source_url'] = array( 903 'description' => __( 'URL to the original attachment file.' ), 904 'type' => 'string', 905 'format' => 'uri', 906 'context' => array( 'view', 'edit', 'embed' ), 907 'readonly' => true, 908 ); 909 910 $schema['properties']['missing_image_sizes'] = array( 911 'description' => __( 'List of the missing image sizes of the attachment.' ), 912 'type' => 'array', 913 'items' => array( 'type' => 'string' ), 914 'context' => array( 'edit' ), 915 'readonly' => true, 916 ); 917 918 unset( $schema['properties']['password'] ); 919 920 $this->schema = $schema; 921 922 return $this->add_additional_fields_schema( $this->schema ); 923 } 924 925 /** 926 * Handles an upload via raw POST data. 927 * 928 * @since 4.7.0 929 * 930 * @param array $data Supplied file data. 931 * @param array $headers HTTP headers from the request. 932 * @return array|WP_Error Data from wp_handle_sideload(). 933 */ 934 protected function upload_from_data( $data, $headers ) { 935 if ( empty( $data ) ) { 936 return new WP_Error( 937 'rest_upload_no_data', 938 __( 'No data supplied.' ), 939 array( 'status' => 400 ) 940 ); 941 } 942 943 if ( empty( $headers['content_type'] ) ) { 944 return new WP_Error( 945 'rest_upload_no_content_type', 946 __( 'No Content-Type supplied.' ), 947 array( 'status' => 400 ) 948 ); 949 } 950 951 if ( empty( $headers['content_disposition'] ) ) { 952 return new WP_Error( 953 'rest_upload_no_content_disposition', 954 __( 'No Content-Disposition supplied.' ), 955 array( 'status' => 400 ) 956 ); 957 } 958 959 $filename = self::get_filename_from_disposition( $headers['content_disposition'] ); 960 961 if ( empty( $filename ) ) { 962 return new WP_Error( 963 'rest_upload_invalid_disposition', 964 __( 'Invalid Content-Disposition supplied. Content-Disposition needs to be formatted as `attachment; filename="image.png"` or similar.' ), 965 array( 'status' => 400 ) 966 ); 967 } 968 969 if ( ! empty( $headers['content_md5'] ) ) { 970 $content_md5 = array_shift( $headers['content_md5'] ); 971 $expected = trim( $content_md5 ); 972 $actual = md5( $data ); 973 974 if ( $expected !== $actual ) { 975 return new WP_Error( 976 'rest_upload_hash_mismatch', 977 __( 'Content hash did not match expected.' ), 978 array( 'status' => 412 ) 979 ); 980 } 981 } 982 983 // Get the content-type. 984 $type = array_shift( $headers['content_type'] ); 985 986 // Include filesystem functions to get access to wp_tempnam() and wp_handle_sideload(). 987 require_once ABSPATH . 'wp-admin/includes/file.php'; 988 989 // Save the file. 990 $tmpfname = wp_tempnam( $filename ); 991 992 $fp = fopen( $tmpfname, 'w+' ); 993 994 if ( ! $fp ) { 995 return new WP_Error( 996 'rest_upload_file_error', 997 __( 'Could not open file handle.' ), 998 array( 'status' => 500 ) 999 ); 1000 } 1001 1002 fwrite( $fp, $data ); 1003 fclose( $fp ); 1004 1005 // Now, sideload it in. 1006 $file_data = array( 1007 'error' => null, 1008 'tmp_name' => $tmpfname, 1009 'name' => $filename, 1010 'type' => $type, 1011 ); 1012 1013 $size_check = self::check_upload_size( $file_data ); 1014 if ( is_wp_error( $size_check ) ) { 1015 return $size_check; 1016 } 1017 1018 $overrides = array( 1019 'test_form' => false, 1020 ); 1021 1022 $sideloaded = wp_handle_sideload( $file_data, $overrides ); 1023 1024 if ( isset( $sideloaded['error'] ) ) { 1025 @unlink( $tmpfname ); 1026 1027 return new WP_Error( 1028 'rest_upload_sideload_error', 1029 $sideloaded['error'], 1030 array( 'status' => 500 ) 1031 ); 1032 } 1033 1034 return $sideloaded; 1035 } 1036 1037 /** 1038 * Parses filename from a Content-Disposition header value. 1039 * 1040 * As per RFC6266: 1041 * 1042 * content-disposition = "Content-Disposition" ":" 1043 * disposition-type *( ";" disposition-parm ) 1044 * 1045 * disposition-type = "inline" | "attachment" | disp-ext-type 1046 * ; case-insensitive 1047 * disp-ext-type = token 1048 * 1049 * disposition-parm = filename-parm | disp-ext-parm 1050 * 1051 * filename-parm = "filename" "=" value 1052 * | "filename*" "=" ext-value 1053 * 1054 * disp-ext-parm = token "=" value 1055 * | ext-token "=" ext-value 1056 * ext-token = <the characters in token, followed by "*"> 1057 * 1058 * @since 4.7.0 1059 * 1060 * @link https://tools.ietf.org/html/rfc2388 1061 * @link https://tools.ietf.org/html/rfc6266 1062 * 1063 * @param string[] $disposition_header List of Content-Disposition header values. 1064 * @return string|null Filename if available, or null if not found. 1065 */ 1066 public static function get_filename_from_disposition( $disposition_header ) { 1067 // Get the filename. 1068 $filename = null; 1069 1070 foreach ( $disposition_header as $value ) { 1071 $value = trim( $value ); 1072 1073 if ( strpos( $value, ';' ) === false ) { 1074 continue; 1075 } 1076 1077 list( $type, $attr_parts ) = explode( ';', $value, 2 ); 1078 1079 $attr_parts = explode( ';', $attr_parts ); 1080 $attributes = array(); 1081 1082 foreach ( $attr_parts as $part ) { 1083 if ( strpos( $part, '=' ) === false ) { 1084 continue; 1085 } 1086 1087 list( $key, $value ) = explode( '=', $part, 2 ); 1088 1089 $attributes[ trim( $key ) ] = trim( $value ); 1090 } 1091 1092 if ( empty( $attributes['filename'] ) ) { 1093 continue; 1094 } 1095 1096 $filename = trim( $attributes['filename'] ); 1097 1098 // Unquote quoted filename, but after trimming. 1099 if ( substr( $filename, 0, 1 ) === '"' && substr( $filename, -1, 1 ) === '"' ) { 1100 $filename = substr( $filename, 1, -1 ); 1101 } 1102 } 1103 1104 return $filename; 1105 } 1106 1107 /** 1108 * Retrieves the query params for collections of attachments. 1109 * 1110 * @since 4.7.0 1111 * 1112 * @return array Query parameters for the attachment collection as an array. 1113 */ 1114 public function get_collection_params() { 1115 $params = parent::get_collection_params(); 1116 $params['status']['default'] = 'inherit'; 1117 $params['status']['items']['enum'] = array( 'inherit', 'private', 'trash' ); 1118 $media_types = $this->get_media_types(); 1119 1120 $params['media_type'] = array( 1121 'default' => null, 1122 'description' => __( 'Limit result set to attachments of a particular media type.' ), 1123 'type' => 'string', 1124 'enum' => array_keys( $media_types ), 1125 ); 1126 1127 $params['mime_type'] = array( 1128 'default' => null, 1129 'description' => __( 'Limit result set to attachments of a particular MIME type.' ), 1130 'type' => 'string', 1131 ); 1132 1133 return $params; 1134 } 1135 1136 /** 1137 * Handles an upload via multipart/form-data ($_FILES). 1138 * 1139 * @since 4.7.0 1140 * 1141 * @param array $files Data from the `$_FILES` superglobal. 1142 * @param array $headers HTTP headers from the request. 1143 * @return array|WP_Error Data from wp_handle_upload(). 1144 */ 1145 protected function upload_from_file( $files, $headers ) { 1146 if ( empty( $files ) ) { 1147 return new WP_Error( 1148 'rest_upload_no_data', 1149 __( 'No data supplied.' ), 1150 array( 'status' => 400 ) 1151 ); 1152 } 1153 1154 // Verify hash, if given. 1155 if ( ! empty( $headers['content_md5'] ) ) { 1156 $content_md5 = array_shift( $headers['content_md5'] ); 1157 $expected = trim( $content_md5 ); 1158 $actual = md5_file( $files['file']['tmp_name'] ); 1159 1160 if ( $expected !== $actual ) { 1161 return new WP_Error( 1162 'rest_upload_hash_mismatch', 1163 __( 'Content hash did not match expected.' ), 1164 array( 'status' => 412 ) 1165 ); 1166 } 1167 } 1168 1169 // Pass off to WP to handle the actual upload. 1170 $overrides = array( 1171 'test_form' => false, 1172 ); 1173 1174 // Bypasses is_uploaded_file() when running unit tests. 1175 if ( defined( 'DIR_TESTDATA' ) && DIR_TESTDATA ) { 1176 $overrides['action'] = 'wp_handle_mock_upload'; 1177 } 1178 1179 $size_check = self::check_upload_size( $files['file'] ); 1180 if ( is_wp_error( $size_check ) ) { 1181 return $size_check; 1182 } 1183 1184 // Include filesystem functions to get access to wp_handle_upload(). 1185 require_once ABSPATH . 'wp-admin/includes/file.php'; 1186 1187 $file = wp_handle_upload( $files['file'], $overrides ); 1188 1189 if ( isset( $file['error'] ) ) { 1190 return new WP_Error( 1191 'rest_upload_unknown_error', 1192 $file['error'], 1193 array( 'status' => 500 ) 1194 ); 1195 } 1196 1197 return $file; 1198 } 1199 1200 /** 1201 * Retrieves the supported media types. 1202 * 1203 * Media types are considered the MIME type category. 1204 * 1205 * @since 4.7.0 1206 * 1207 * @return array Array of supported media types. 1208 */ 1209 protected function get_media_types() { 1210 $media_types = array(); 1211 1212 foreach ( get_allowed_mime_types() as $mime_type ) { 1213 $parts = explode( '/', $mime_type ); 1214 1215 if ( ! isset( $media_types[ $parts[0] ] ) ) { 1216 $media_types[ $parts[0] ] = array(); 1217 } 1218 1219 $media_types[ $parts[0] ][] = $mime_type; 1220 } 1221 1222 return $media_types; 1223 } 1224 1225 /** 1226 * Determine if uploaded file exceeds space quota on multisite. 1227 * 1228 * Replicates check_upload_size(). 1229 * 1230 * @since 4.9.8 1231 * 1232 * @param array $file $_FILES array for a given file. 1233 * @return true|WP_Error True if can upload, error for errors. 1234 */ 1235 protected function check_upload_size( $file ) { 1236 if ( ! is_multisite() ) { 1237 return true; 1238 } 1239 1240 if ( get_site_option( 'upload_space_check_disabled' ) ) { 1241 return true; 1242 } 1243 1244 $space_left = get_upload_space_available(); 1245 1246 $file_size = filesize( $file['tmp_name'] ); 1247 1248 if ( $space_left < $file_size ) { 1249 return new WP_Error( 1250 'rest_upload_limited_space', 1251 /* translators: %s: Required disk space in kilobytes. */ 1252 sprintf( __( 'Not enough space to upload. %s KB needed.' ), number_format( ( $file_size - $space_left ) / KB_IN_BYTES ) ), 1253 array( 'status' => 400 ) 1254 ); 1255 } 1256 1257 if ( $file_size > ( KB_IN_BYTES * get_site_option( 'fileupload_maxk', 1500 ) ) ) { 1258 return new WP_Error( 1259 'rest_upload_file_too_big', 1260 /* translators: %s: Maximum allowed file size in kilobytes. */ 1261 sprintf( __( 'This file is too big. Files must be less than %s KB in size.' ), get_site_option( 'fileupload_maxk', 1500 ) ), 1262 array( 'status' => 400 ) 1263 ); 1264 } 1265 1266 // Include multisite admin functions to get access to upload_is_user_over_quota(). 1267 require_once ABSPATH . 'wp-admin/includes/ms.php'; 1268 1269 if ( upload_is_user_over_quota( false ) ) { 1270 return new WP_Error( 1271 'rest_upload_user_quota_exceeded', 1272 __( 'You have used your space quota. Please delete files before uploading.' ), 1273 array( 'status' => 400 ) 1274 ); 1275 } 1276 1277 return true; 1278 } 1279 1280 /** 1281 * Gets the request args for the edit item route. 1282 * 1283 * @since 5.5.0 1284 * 1285 * @return array 1286 */ 1287 protected function get_edit_media_item_args() { 1288 return array( 1289 'rotation' => array( 1290 'description' => __( 'The amount to rotate the image clockwise in degrees.' ), 1291 'type' => 'integer', 1292 'minimum' => 0, 1293 'exclusiveMinimum' => true, 1294 'maximum' => 360, 1295 'exclusiveMaximum' => true, 1296 ), 1297 'x' => array( 1298 'description' => __( 'As a percentage of the image, the x position to start the crop from.' ), 1299 'type' => 'number', 1300 'minimum' => 0, 1301 'maximum' => 100, 1302 ), 1303 'y' => array( 1304 'description' => __( 'As a percentage of the image, the y position to start the crop from.' ), 1305 'type' => 'number', 1306 'minimum' => 0, 1307 'maximum' => 100, 1308 ), 1309 'width' => array( 1310 'description' => __( 'As a percentage of the image, the width to crop the image to.' ), 1311 'type' => 'number', 1312 'minimum' => 0, 1313 'maximum' => 100, 1314 ), 1315 'height' => array( 1316 'description' => __( 'As a percentage of the image, the height to crop the image to.' ), 1317 'type' => 'number', 1318 'minimum' => 0, 1319 'maximum' => 100, 1320 ), 1321 'src' => array( 1322 'description' => __( 'URL to the edited image file.' ), 1323 'type' => 'string', 1324 'format' => 'uri', 1325 'required' => true, 1326 ), 1327 ); 1328 } 1329 1330 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated : Wed Jan 27 08:20:02 2021 | Cross-referenced by PHPXref |