← All changes
|
json-endpoints/class.wpcom-json-api-upload-media-v1-1-endpoint.php
+64
-8
12.1.3
→
16.3-beta
View file →
| @@ -4,8 +4,12 @@ | ||
| 4 | 4 | * |
| 5 | 5 | * Endpoint: /sites/%s/media/new |
| 6 | 6 | */ |
| 7 | 7 | |
| 8 | +if ( ! defined( 'ABSPATH' ) ) { | |
| 9 | + exit( 0 ); | |
| 10 | +} | |
| 11 | + | |
| 8 | 12 | new WPCOM_JSON_API_Upload_Media_v1_1_Endpoint( |
| 9 | 13 | array( |
| 10 | 14 | 'description' => 'Upload a new piece of media.', |
| 11 | 15 | 'allow_cross_origin_request' => true, |
| @@ -24,9 +28,9 @@ | ||
| 24 | 28 | 'media' => '(media) An array of media to attach to the post. To upload media, the entire request should be multipart/form-data encoded. Accepts jpg, jpeg, png, gif, pdf, doc, ppt, odt, pptx, docx, pps, ppsx, xls, xlsx, key. Audio and Video may also be available. See <code>allowed_file_types</code> in the options response of the site endpoint.<br /><br /><strong>Example</strong>:<br />' . |
| 25 | 29 | "<code>curl \<br />--form 'media[]=@/path/to/file.jpg' \<br />-H 'Authorization: BEARER your-token' \<br />'https://public-api.wordpress.com/rest/v1/sites/123/media/new'</code>", |
| 26 | 30 | 'media_urls' => '(array) An array of URLs to upload to the post. Errors produced by media uploads, if any, will be in `media_errors` in the response.', |
| 27 | 31 | 'attrs' => '(array) An array of attributes (`title`, `description`, `caption` `alt` for images, `artist` for audio, `album` for audio, and `parent_id`) are supported to assign to the media uploaded via the `media` or `media_urls` properties. You must use a numeric index for the keys of `attrs` which follows the same sequence as `media` and `media_urls`. <br /><br /><strong>Example</strong>:<br />' . |
| 28 | - "<code>curl \<br />--form 'media[]=@/path/to/file1.jpg' \<br />--form 'media_urls[]=http://example.com/file2.jpg' \<br /> \<br />--form 'attrs[0][caption]=This will be the caption for file1.jpg' \<br />--form 'attrs[1][title]=This will be the title for file2.jpg' \<br />-H 'Authorization: BEARER your-token' \<br />'https://public-api.wordpress.com/rest/v1/sites/123/posts/new'</code>", | |
| 32 | + "<code>curl \<br />--form 'media[]=@/path/to/file1.jpg' \<br />--form 'media_urls[]=http://example.com/file2.jpg' \<br /> \<br />--form 'attrs[0][caption]=This will be the caption for file1.jpg' \<br />--form 'attrs[1][title]=This will be the title for file2.jpg' \<br />-H 'Authorization: BEARER your-token' \<br />'https://public-api.wordpress.com/rest/v1/sites/123/media/new'</code>", | |
| 29 | 33 | ), |
| 30 | 34 | |
| 31 | 35 | 'response_format' => array( |
| 32 | 36 | 'media' => '(array) Array of uploaded media objects', |
| @@ -47,8 +51,10 @@ | ||
| 47 | 51 | |
| 48 | 52 | // phpcs:disable PEAR.NamingConventions.ValidClassName.Invalid |
| 49 | 53 | /** |
| 50 | 54 | * Upload media item API class v1.1 |
| 55 | + * | |
| 56 | + * @phan-constructor-used-for-side-effects | |
| 51 | 57 | */ |
| 52 | 58 | class WPCOM_JSON_API_Upload_Media_v1_1_Endpoint extends WPCOM_JSON_API_Endpoint { |
| 53 | 59 | /** |
| 54 | 60 | * Upload media item API endpoint callback v1.1 |
| @@ -77,8 +83,53 @@ | ||
| 77 | 83 | if ( empty( $media_files ) && empty( $media_urls ) ) { |
| 78 | 84 | return new WP_Error( 'invalid_input', 'No media provided in input.' ); |
| 79 | 85 | } |
| 80 | 86 | |
| 87 | + /* | |
| 88 | + * Attaching an upload to a post is an edit of that post, so a caller-supplied | |
| 89 | + * `parent_id` takes `edit_post` on the target. `upload_files` above says only that | |
| 90 | + * the caller may upload something, never where they may put it. | |
| 91 | + * | |
| 92 | + * Every target is checked here, before the first file is written. Refusing from | |
| 93 | + * inside `handle_media_creation_v1_1()` would leave the items it already created | |
| 94 | + * on disk and in the database, and a retry would upload them again. | |
| 95 | + * | |
| 96 | + * An upload-token request drops `parent_id` instead of being refused. Such a request | |
| 97 | + * runs with no logged-in user by construction -- `is_authorized_with_upload_token()` | |
| 98 | + * fails as soon as `get_current_user_id()` is non-zero -- so it can never | |
| 99 | + * demonstrate `edit_post` on any target, and refusing would break any client that | |
| 100 | + * pairs a token with `parent_id` for no security gain. Dropping lands the item at | |
| 101 | + * `post_parent` 0, the same place `absint()` already puts unusable input. The token | |
| 102 | + * is not treated as trusted here: it is an opaque bearer credential mintable by any | |
| 103 | + * logged-in user via `/sites/%s/media/token`, so it must not buy an attach. | |
| 104 | + * | |
| 105 | + * Zero is exempt: it names no target, and `edit_post` fails closed on 0. | |
| 106 | + */ | |
| 107 | + if ( $this->api->is_authorized_with_upload_token() ) { | |
| 108 | + foreach ( $media_attrs as $i => $media_attr ) { | |
| 109 | + $media_attr = (array) $media_attr; | |
| 110 | + unset( $media_attr['parent_id'] ); | |
| 111 | + $media_attrs[ $i ] = $media_attr; | |
| 112 | + } | |
| 113 | + } else { | |
| 114 | + foreach ( $media_attrs as $media_attr ) { | |
| 115 | + // An entry may arrive as an object or as something that is neither; casting | |
| 116 | + // keeps a string entry from being indexed as an array. | |
| 117 | + $media_attr = (array) $media_attr; | |
| 118 | + | |
| 119 | + if ( empty( $media_attr['parent_id'] ) ) { | |
| 120 | + continue; | |
| 121 | + } | |
| 122 | + | |
| 123 | + $parent_id = absint( $media_attr['parent_id'] ); | |
| 124 | + | |
| 125 | + if ( $parent_id && ! current_user_can( 'edit_post', $parent_id ) ) { | |
| 126 | + return new WP_Error( 'unauthorized', 'User cannot edit the parent post', 403 ); | |
| 127 | + } | |
| 128 | + } | |
| 129 | + } | |
| 130 | + | |
| 131 | + $jetpack_sync = null; | |
| 81 | 132 | $is_jetpack_site = false; |
| 82 | 133 | if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { |
| 83 | 134 | // For jetpack sites, we send the media via a different method, because the sync is very different. |
| 84 | 135 | $jetpack_sync = Jetpack_Media_Sync::summon( $blog_id ); |
| @@ -91,9 +142,9 @@ | ||
| 91 | 142 | $errors = array(); |
| 92 | 143 | |
| 93 | 144 | // We're splitting out videos for Jetpack sites. |
| 94 | 145 | foreach ( $media_files as $media_item ) { |
| 95 | - if ( preg_match( '@^video/@', $media_item['type'] ) && $is_jetpack_site ) { | |
| 146 | + if ( isset( $media_item['type'] ) && preg_match( '@^video/@', $media_item['type'] ) && $is_jetpack_site ) { | |
| 96 | 147 | if ( defined( 'IS_WPCOM' ) && IS_WPCOM && |
| 97 | 148 | defined( 'VIDEOPRESS_JETPACK_VIDEO_ENABLED' ) && VIDEOPRESS_JETPACK_VIDEO_ENABLED |
| 98 | 149 | ) { |
| 99 | 150 | // Check that video upload space is available for a Jetpack site (skipped if site is Atomic). |
| @@ -100,12 +151,12 @@ | ||
| 100 | 151 | $result = videopress_check_space_available_for_jetpack( $blog_id, $media_item['name'], $media_item['size'] ); |
| 101 | 152 | |
| 102 | 153 | if ( true !== $result ) { |
| 103 | 154 | $this->api->output_early( 400, array( 'errors' => $this->rewrite_generic_upload_error( array( $result ) ) ) ); |
| 155 | + continue; | |
| 104 | 156 | } |
| 105 | 157 | } |
| 106 | 158 | $jetpack_media_files[] = $media_item; |
| 107 | - | |
| 108 | 159 | } else { |
| 109 | 160 | $other_media_files[] = $media_item; |
| 110 | 161 | } |
| 111 | 162 | } |
| @@ -155,9 +206,9 @@ | ||
| 155 | 206 | $media_items[] = $this->get_media_item_v1_1( $media_id ); |
| 156 | 207 | } |
| 157 | 208 | } |
| 158 | 209 | |
| 159 | - if ( count( $media_items ) <= 0 ) { | |
| 210 | + if ( array() === $media_items ) { | |
| 160 | 211 | return $this->api->output_early( 400, array( 'errors' => $this->rewrite_generic_upload_error( $errors ) ) ); |
| 161 | 212 | } |
| 162 | 213 | |
| 163 | 214 | $results = array(); |
| @@ -163,9 +214,8 @@ | ||
| 163 | 214 | $results = array(); |
| 164 | 215 | foreach ( $media_items as $media_item ) { |
| 165 | 216 | if ( is_wp_error( $media_item ) ) { |
| 166 | 217 | $errors[] = array( |
| 167 | - 'file' => $media_item['ID'], | |
| 168 | 218 | 'error' => $media_item->get_error_code(), |
| 169 | 219 | 'message' => $media_item->get_error_message(), |
| 170 | 220 | ); |
| 171 | 221 | |
| @@ -175,9 +225,9 @@ | ||
| 175 | 225 | } |
| 176 | 226 | |
| 177 | 227 | $response = array( 'media' => $results ); |
| 178 | 228 | |
| 179 | - if ( count( $errors ) > 0 ) { | |
| 229 | + if ( is_countable( $errors ) && count( $errors ) > 0 ) { | |
| 180 | 230 | $response['errors'] = $this->rewrite_generic_upload_error( $errors ); |
| 181 | 231 | } |
| 182 | 232 | |
| 183 | 233 | return $response; |
| @@ -190,9 +240,9 @@ | ||
| 190 | 240 | * @return array The same array with an improved error message. |
| 191 | 241 | */ |
| 192 | 242 | public function rewrite_generic_upload_error( $errors ) { |
| 193 | 243 | foreach ( $errors as $k => $error ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable |
| 194 | - if ( 'upload_error' === $error['error'] && false !== strpos( $error['message'], '|' ) ) { | |
| 244 | + if ( 'upload_error' === $error['error'] && str_contains( $error['message'], '|' ) ) { | |
| 195 | 245 | list( $errors[ $k ]['error'], $errors[ $k ]['message'] ) = explode( '|', $error['message'], 2 ); // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable |
| 196 | 246 | } |
| 197 | 247 | } |
| 198 | 248 | return $errors; |
| @@ -216,8 +266,14 @@ | ||
| 216 | 266 | if ( isset( $file['error'] ) && $file['error'] > 0 ) { // There's already an error. Error Codes Reference: https://www.php.net/manual/en/features.file-upload.errors.php . |
| 217 | 267 | return $file; |
| 218 | 268 | } |
| 219 | 269 | |
| 270 | + // We don't know if this is an upload or a sideload, but in either case the tmp_name should be a path, not a URL. | |
| 271 | + if ( wp_parse_url( $file['tmp_name'], PHP_URL_SCHEME ) !== null ) { | |
| 272 | + $file['error'] = 'rest_upload_invalid|' . __( 'Specified file failed upload test.', 'default' ); // phpcs:ignore WordPress.WP.I18n.TextDomainMismatch | |
| 273 | + return $file; | |
| 274 | + } | |
| 275 | + | |
| 220 | 276 | if ( defined( 'WP_IMPORTING' ) ) { |
| 221 | 277 | return $file; |
| 222 | 278 | } |
| 223 | 279 | |
| @@ -267,9 +323,9 @@ | ||
| 267 | 323 | return false; |
| 268 | 324 | } |
| 269 | 325 | |
| 270 | 326 | foreach ( $media_files as $media_item ) { |
| 271 | - if ( ! preg_match( '@^video/@', $media_item['type'] ) ) { | |
| 327 | + if ( ! isset( $media_item['type'] ) || ! preg_match( '@^video/@', $media_item['type'] ) ) { | |
| 272 | 328 | return false; |
| 273 | 329 | } |
| 274 | 330 | } |
| 275 | 331 | |