This repository was archived by the owner on Sep 24, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 651
Convert /media to new controller pattern #904
Merged
Merged
Changes from all commits
Commits
Show all changes
37 commits
Select commit
Hold shift + click to select a range
1d0db94
Register `WP_JSON_Attachments_Controller` and define basic schema
danielbachhuber 2cff0ce
Clean up the declaration of fixed schemas
danielbachhuber 0a1ff9a
For attachments, explicitly name `caption` and `description`
danielbachhuber bb81bcc
Merge branch 'develop' into media-controller
danielbachhuber 01f8d45
Remove debug
danielbachhuber 38d76d2
Add `alt_text` to attachment schema
danielbachhuber a2e3c88
Merge branch 'develop' into media-controller
danielbachhuber 3c53c1e
Merge branch 'develop' into media-controller
danielbachhuber e5e12b1
Merge branch 'develop' into media-controller
danielbachhuber b036bad
Add `guid` to attachment response
danielbachhuber ba92811
Expose post associated with attachment as `post_id`
danielbachhuber 4f641a2
Test attachment create permissions
danielbachhuber e9890d1
Tests for media routes
danielbachhuber 38f43ce
Copy over `upload_from_file()` and `upload_from_data()` from v1.1
danielbachhuber 44501ce
Drop underscore from `$_files` and `$_headers`
danielbachhuber ef293c0
Drop unused variable
danielbachhuber cf1376e
Formatting
danielbachhuber 8d682f0
Abstract, to accommodate post types that don't use these fields
danielbachhuber 9bd307e
Captions and descriptions don't need to be filtered
danielbachhuber e20cc00
Formatting
danielbachhuber 92a0f48
Formatting
danielbachhuber 1a1b015
Fix incorrect variable names
danielbachhuber 77c93f9
Somehow this header data became arrays
danielbachhuber d3f5e14
Support for uploading files by including binary data in request body
danielbachhuber caefc47
Use a temp file so it's more reusable between tests
danielbachhuber 556210f
Test coverage for deleting an attachment
danielbachhuber 9627c52
These fields are no longer objects
danielbachhuber 9db49d4
Support for editing attachment data
danielbachhuber 1634c77
Make a note to come back to this
danielbachhuber fa005ab
Test `WP_JSON_Attachments_Controller::prepare_item_for_response()`
danielbachhuber 9d9fb8d
Check entire data set
danielbachhuber e24e1f1
Fix bugs in `upload_from_file()` with some TDD
danielbachhuber 259d03b
Test bad MD5 header for upload with files
danielbachhuber b6fc129
Basic test for `GET /wp/media`
danielbachhuber 37df4b9
Internalize canola file because we can't depend on directory existing
danielbachhuber 0a27c1e
Merge branch 'develop' into media-controller
danielbachhuber c5cd7c7
Post type is set properly now
danielbachhuber File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,337 @@ | ||
| <?php | ||
|
|
||
| class WP_JSON_Attachments_Controller extends WP_JSON_Posts_Controller { | ||
|
|
||
| /** | ||
| * Create a single attachment | ||
| * | ||
| * @param WP_JSON_Request $request Full details about the request | ||
| * @return WP_Error|WP_HTTP_ResponseInterface | ||
| */ | ||
| public function create_item( $request ) { | ||
|
|
||
| // Permissions check - Note: "upload_files" cap is returned for an attachment by $post_type_obj->cap->create_posts | ||
| $post_type_obj = get_post_type_object( $this->post_type ); | ||
| if ( ! current_user_can( $post_type_obj->cap->create_posts ) || ! current_user_can( $post_type_obj->cap->edit_posts ) ) { | ||
| return new WP_Error( 'json_cannot_create', __( 'Sorry, you are not allowed to post on this site.' ), array( 'status' => 400 ) ); | ||
| } | ||
|
|
||
| // If a user is trying to attach to a post make sure they have permissions. Bail early if post_id is not being passed | ||
| if ( ! empty( $request['post_id'] ) ) { | ||
| $parent = get_post( (int) $request['post_id'] ); | ||
| $post_parent_type = get_post_type_object( $parent->post_type ); | ||
| if ( ! current_user_can( $post_parent_type->cap->edit_post, $request['post_id'] ) ) { | ||
| return new WP_Error( 'json_cannot_edit', __( 'Sorry, you are not allowed to edit this post.' ), array( 'status' => 401 ) ); | ||
| } | ||
| } | ||
|
|
||
| // Get the file via $_FILES or raw data | ||
| $files = $request->get_file_params(); | ||
| $headers = $request->get_headers(); | ||
| if ( ! empty( $files ) ) { | ||
| $file = $this->upload_from_file( $files, $headers ); | ||
| } else { | ||
| $file = $this->upload_from_data( $request->get_body(), $headers ); | ||
| } | ||
|
|
||
| if ( is_wp_error( $file ) ) { | ||
| return $file; | ||
| } | ||
|
|
||
| $name = basename( $file['file'] ); | ||
| $name_parts = pathinfo( $name ); | ||
| $name = trim( substr( $name, 0, -(1 + strlen( $name_parts['extension'] ) ) ) ); | ||
|
|
||
| $url = $file['url']; | ||
| $type = $file['type']; | ||
| $file = $file['file']; | ||
| $title = $name; | ||
| $caption = ''; | ||
|
|
||
| // use image exif/iptc data for title and caption defaults if possible | ||
| if ( $image_meta = @wp_read_image_metadata( $file ) ) { | ||
| if ( empty( $request['title'] ) && trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) { | ||
| $title = $image_meta['title']; | ||
| } | ||
|
|
||
| if ( empty( $request['caption'] ) && trim( $image_meta['caption'] ) ) { | ||
| $caption = $image_meta['caption']; | ||
| } | ||
| } | ||
|
|
||
| $attachment = $this->prepare_item_for_database( $request ); | ||
| $attachment->file = $file; | ||
| $attachment->post_mime_type = $type; | ||
| $attachment->guid = $url; | ||
| $id = wp_insert_post( $attachment, true ); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To be explicit, would wp_insert_attachment be better? I doesn't do much right now, but seems more correct, especially if hooks are added in the future.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I used |
||
| if ( is_wp_error( $id ) ) { | ||
| return $id; | ||
| } | ||
|
|
||
| wp_update_attachment_metadata( $id, wp_generate_attachment_metadata( $id, $file ) ); | ||
|
|
||
| $response = $this->get_item( array( | ||
| 'id' => $id, | ||
| 'context' => 'edit', | ||
| ) ); | ||
| $response = json_ensure_response( $response ); | ||
| $response->set_status( 201 ); | ||
| $response->header( 'Location', json_url( '/wp/' . $this->get_post_type_base( $attachment->post_type ) . '/' . $id ) ); | ||
|
|
||
| return $response; | ||
|
|
||
| } | ||
|
|
||
| /** | ||
| * Update a single post | ||
| * | ||
| * @param WP_JSON_Request $request Full details about the request | ||
| * @return WP_JSON_Response|WP_HTTP_ResponseInterface | ||
| */ | ||
| public function update_item( $request ) { | ||
| $response = parent::update_item( $request ); | ||
| if ( is_wp_error( $response ) ) { | ||
| return $response; | ||
| } | ||
|
|
||
| $response = json_ensure_response( $response ); | ||
| $data = $response->get_data(); | ||
|
|
||
| if ( isset( $request['alt_text'] ) ) { | ||
| update_post_meta( $data['id'], '_wp_attachment_image_alt', sanitize_text_field( $request['alt_text'] ) ); | ||
| } | ||
|
|
||
| $response = $this->get_item( array( | ||
| 'id' => $data['id'], | ||
| 'context' => 'edit', | ||
| )); | ||
| $response = json_ensure_response( $response ); | ||
| $response->set_status( 201 ); | ||
| $response->header( 'Location', json_url( '/wp/' . $this->get_post_type_base( $this->post_type ) . '/' . $data['id'] ) ); | ||
| return $response; | ||
| } | ||
|
|
||
| /** | ||
| * Prepare a single attachment for create or update | ||
| * | ||
| * @param WP_JSON_Request $request Request object | ||
| * @return WP_Error|obj $prepared_attachment Post object | ||
| */ | ||
| protected function prepare_item_for_database( $request ) { | ||
| $prepared_attachment = parent::prepare_item_for_database( $request ); | ||
|
|
||
| if ( isset( $request['caption'] ) ) { | ||
| $prepared_attachment->post_content = wp_filter_post_kses( $request['caption'] ); | ||
| } | ||
|
|
||
| if ( isset( $request['description'] ) ) { | ||
| $prepared_attachment->post_excerpt = wp_filter_post_kses( $request['description'] ); | ||
| } | ||
|
|
||
| if ( isset( $request['post_id'] ) ) { | ||
| $prepared_attachment->post_parent = (int) $request['post_parent']; | ||
| } | ||
|
|
||
| return $prepared_attachment; | ||
| } | ||
|
|
||
| /** | ||
| * Prepare a single attachment output for response | ||
| * | ||
| * @param WP_Post $post Post object | ||
| * @param WP_JSON_Request $request Request object | ||
| * @return array $response | ||
| */ | ||
| public function prepare_item_for_response( $post, $request ) { | ||
| $response = parent::prepare_item_for_response( $post, $request ); | ||
|
|
||
| $response['alt_text'] = get_post_meta( $post->ID, '_wp_attachment_image_alt', true ); | ||
| $response['caption'] = $post->post_content; | ||
| $response['description'] = $post->post_excerpt; | ||
| $response['media_type'] = wp_attachment_is_image( $post->ID ) ? 'image' : 'file'; | ||
| $response['media_details'] = wp_get_attachment_metadata( $post->ID ); | ||
| $response['post_id'] = ! empty( $post->post_parent ) ? (int) $post->post_parent : null; | ||
| $response['source_url'] = wp_get_attachment_url( $post->ID ); | ||
|
|
||
| // Ensure empty details is an empty object | ||
| if ( empty( $response['media_details'] ) ) { | ||
| $response['media_details'] = new stdClass; | ||
| } elseif ( ! empty( $response['media_details']['sizes'] ) ) { | ||
| $img_url_basename = wp_basename( $response['source_url'] ); | ||
|
|
||
| foreach ( $response['media_details']['sizes'] as $size => &$size_data ) { | ||
| // Use the same method image_downsize() does | ||
| $size_data['source_url'] = str_replace( $img_url_basename, $size_data['file'], $response['source_url'] ); | ||
| } | ||
| } else { | ||
| $response['media_details']['sizes'] = new stdClass; | ||
| } | ||
|
|
||
| return $response; | ||
| } | ||
|
|
||
| /** | ||
| * Get the Attachment's schema, conforming to JSON Schema | ||
| * | ||
| * @return array | ||
| */ | ||
| public function get_item_schema() { | ||
|
|
||
| $schema = parent::get_item_schema(); | ||
|
|
||
| $schema['properties']['alt_text'] = array( | ||
| 'description' => 'Alternative text to display when attachment is not displayed.', | ||
| 'type' => 'string', | ||
| ); | ||
| $schema['properties']['caption'] = array( | ||
| 'description' => 'The caption for the attachment.', | ||
| 'type' => 'string', | ||
| ); | ||
| $schema['properties']['description'] = array( | ||
| 'description' => 'The description for the attachment.', | ||
| 'type' => 'string', | ||
| ); | ||
| $schema['properties']['media_type'] = array( | ||
| 'description' => 'Type of attachment.', | ||
| 'type' => 'string', | ||
| 'enum' => array( 'image', 'file' ), | ||
| ); | ||
| $schema['properties']['media_details'] = array( | ||
| 'description' => 'Details about the attachment file, specific to its type.', | ||
| 'type' => 'object', | ||
| ); | ||
| $schema['properties']['post_id'] = array( | ||
| 'description' => 'The ID for the associated post of the attachment.', | ||
| 'type' => 'integer', | ||
| ); | ||
| $schema['properties']['source_url'] = array( | ||
| 'description' => 'URL to the original attachment file.', | ||
| 'type' => 'string', | ||
| 'format' => 'uri', | ||
| ); | ||
| return $schema; | ||
| } | ||
|
|
||
| /** | ||
| * Handle an upload via raw POST data | ||
| * | ||
| * @param array $data Supplied file data | ||
| * @param array $headers HTTP headers from the request | ||
| * @return array|WP_Error Data from {@see wp_handle_sideload()} | ||
| */ | ||
| protected function upload_from_data( $data, $headers ) { | ||
| if ( empty( $data ) ) { | ||
| return new WP_Error( 'json_upload_no_data', __( 'No data supplied' ), array( 'status' => 400 ) ); | ||
| } | ||
|
|
||
| if ( empty( $headers['content_type'] ) ) { | ||
| return new WP_Error( 'json_upload_no_content_type', __( 'No Content-Type supplied' ), array( 'status' => 400 ) ); | ||
| } | ||
|
|
||
| if ( empty( $headers['content_disposition'] ) ) { | ||
| return new WP_Error( 'json_upload_no_content_disposition', __( 'No Content-Disposition supplied' ), array( 'status' => 400 ) ); | ||
| } | ||
|
|
||
| // Get the filename | ||
| $filename = null; | ||
|
|
||
| foreach ( $headers['content_disposition'] as $part ) { | ||
| $part = trim( $part ); | ||
|
|
||
| if ( strpos( $part, 'filename' ) !== 0 ) { | ||
| continue; | ||
| } | ||
|
|
||
| $filenameparts = explode( '=', $part ); | ||
| $filename = trim( $filenameparts[1] ); | ||
| } | ||
|
|
||
| if ( empty( $filename ) ) { | ||
| return new WP_Error( 'json_upload_invalid_disposition', __( 'Invalid Content-Disposition supplied' ), array( 'status' => 400 ) ); | ||
| } | ||
|
|
||
| if ( ! empty( $headers['content_md5'] ) ) { | ||
| $content_md5 = array_shift( $headers['content_md5'] ); | ||
| $expected = trim( $content_md5 ); | ||
| $actual = md5( $data ); | ||
|
|
||
| if ( $expected !== $actual ) { | ||
| return new WP_Error( 'json_upload_hash_mismatch', __( 'Content hash did not match expected' ), array( 'status' => 412 ) ); | ||
| } | ||
| } | ||
|
|
||
| // Get the content-type | ||
| $type = array_shift( $headers['content_type'] ); | ||
|
|
||
| // Save the file | ||
| $tmpfname = wp_tempnam( $filename ); | ||
|
|
||
| $fp = fopen( $tmpfname, 'w+' ); | ||
|
|
||
| if ( ! $fp ) { | ||
| return new WP_Error( 'json_upload_file_error', __( 'Could not open file handle' ), array( 'status' => 500 ) ); | ||
| } | ||
|
|
||
| fwrite( $fp, $data ); | ||
| fclose( $fp ); | ||
|
|
||
| // Now, sideload it in | ||
| $file_data = array( | ||
| 'error' => null, | ||
| 'tmp_name' => $tmpfname, | ||
| 'name' => $filename, | ||
| 'type' => $type, | ||
| ); | ||
| $overrides = array( | ||
| 'test_form' => false, | ||
| ); | ||
| $sideloaded = wp_handle_sideload( $file_data, $overrides ); | ||
|
|
||
| if ( isset( $sideloaded['error'] ) ) { | ||
| @unlink( $tmpfname ); | ||
| return new WP_Error( 'json_upload_sideload_error', $sideloaded['error'], array( 'status' => 500 ) ); | ||
| } | ||
|
|
||
| return $sideloaded; | ||
| } | ||
|
|
||
| /** | ||
| * Handle an upload via multipart/form-data ($_FILES) | ||
| * | ||
| * @param array $files Data from $_FILES | ||
| * @param array $headers HTTP headers from the request | ||
| * @return array|WP_Error Data from {@see wp_handle_upload()} | ||
| */ | ||
| protected function upload_from_file( $files, $headers ) { | ||
| if ( empty( $files ) ) { | ||
| return new WP_Error( 'json_upload_no_data', __( 'No data supplied' ), array( 'status' => 400 ) ); | ||
| } | ||
|
|
||
| // Verify hash, if given | ||
| if ( ! empty( $headers['CONTENT_MD5'] ) ) { | ||
| $expected = trim( $headers['CONTENT_MD5'] ); | ||
| $actual = md5_file( $files['file']['tmp_name'] ); | ||
| if ( $expected !== $actual ) { | ||
| return new WP_Error( 'json_upload_hash_mismatch', __( 'Content hash did not match expected' ), array( 'status' => 412 ) ); | ||
| } | ||
| } | ||
|
|
||
| // Pass off to WP to handle the actual upload | ||
| $overrides = array( | ||
| 'test_form' => false, | ||
| ); | ||
| // Bypasses is_uploaded_file() when running unit tests | ||
| if ( defined( 'DIR_TESTDATA' ) && DIR_TESTDATA ) { | ||
| $overrides['action'] = 'wp_handle_mock_upload'; | ||
| } | ||
|
|
||
| $file = wp_handle_upload( $files, $overrides ); | ||
|
|
||
| if ( isset( $file['error'] ) ) { | ||
| return new WP_Error( 'json_upload_unknown_error', $file['error'], array( 'status' => 500 ) ); | ||
| } | ||
|
|
||
| return $file; | ||
| } | ||
|
|
||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know we didn't merge #854 yet, but the
permissions_callbackalready exists. Maybe we just do that on a second pass though?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#922