← All changes
|
json-endpoints/class.wpcom-json-api-update-media-v1-1-endpoint.php
+74
-11
12.3.2
→
16.3-a.7
View file →
| @@ -4,8 +4,12 @@ | ||
| 4 | 4 | * |
| 5 | 5 | * Endpoint: v1.1/sites/%s/media/%d |
| 6 | 6 | */ |
| 7 | 7 | |
| 8 | +if ( ! defined( 'ABSPATH' ) ) { | |
| 9 | + exit( 0 ); | |
| 10 | +} | |
| 11 | + | |
| 8 | 12 | new WPCOM_JSON_API_Update_Media_v1_1_Endpoint( |
| 9 | 13 | array( |
| 10 | 14 | 'description' => 'Edit basic information about a media item.', |
| 11 | 15 | 'group' => 'media', |
| @@ -74,11 +78,56 @@ | ||
| 74 | 78 | |
| 75 | 79 | // phpcs:disable PEAR.NamingConventions.ValidClassName.Invalid |
| 76 | 80 | /** |
| 77 | 81 | * Update media item info v1.1 class. |
| 82 | + * | |
| 83 | + * @phan-constructor-used-for-side-effects | |
| 78 | 84 | */ |
| 79 | 85 | class WPCOM_JSON_API_Update_Media_v1_1_Endpoint extends WPCOM_JSON_API_Endpoint { |
| 80 | 86 | /** |
| 87 | + * Whether the current user may edit the given media item. | |
| 88 | + * | |
| 89 | + * `upload_files` is a primitive capability and ignores any object passed to it, | |
| 90 | + * so it only tells us the caller may upload something, never that they may edit | |
| 91 | + * this particular item. A missing item is passed through so the caller receives | |
| 92 | + * the endpoint's own 404 rather than a 403. A userless request gets no exemption: | |
| 93 | + * `edit_post` fails closed for user 0 like any other caller. | |
| 94 | + * | |
| 95 | + * Non-attachments are refused outright. `get_post()` resolves any post type, so | |
| 96 | + * without this test a media endpoint edits ordinary posts, pages and revisions. | |
| 97 | + * The post-type test must stay below the missing-post passthrough: that branch | |
| 98 | + * returns true, so testing there would skip `edit_post` for ordinary posts. | |
| 99 | + * | |
| 100 | + * A non-attachment yields 403, not the 404 a missing item gets. This is a boolean | |
| 101 | + * gate, and `get_media_item*()` resolves any post type, so a passthrough would | |
| 102 | + * return 200 rather than 404. Revisit if clients conflate it with an auth failure. | |
| 103 | + * | |
| 104 | + * Do not move this into a trait: this file instantiates the endpoint above the | |
| 105 | + * class declaration, and `use Trait;` disables PHP early binding, which makes the | |
| 106 | + * file fatal with "Class not found". | |
| 107 | + * | |
| 108 | + * @param int $media_id Media post ID. | |
| 109 | + * @return bool | |
| 110 | + */ | |
| 111 | + protected function current_user_can_edit_media_item( $media_id ) { | |
| 112 | + if ( ! current_user_can( 'upload_files' ) ) { | |
| 113 | + return false; | |
| 114 | + } | |
| 115 | + | |
| 116 | + $post = get_post( $media_id ); | |
| 117 | + | |
| 118 | + if ( ! $post ) { | |
| 119 | + return true; | |
| 120 | + } | |
| 121 | + | |
| 122 | + if ( 'attachment' !== $post->post_type ) { | |
| 123 | + return false; | |
| 124 | + } | |
| 125 | + | |
| 126 | + return current_user_can( 'edit_post', $media_id ); | |
| 127 | + } | |
| 128 | + | |
| 129 | + /** | |
| 81 | 130 | * Update media item info API v1.1 callback. |
| 82 | 131 | * |
| 83 | 132 | * @param string $path API path. |
| 84 | 133 | * @param int $blog_id Blog ID. |
| @@ -91,10 +140,10 @@ | ||
| 91 | 140 | if ( is_wp_error( $blog_id ) ) { |
| 92 | 141 | return $blog_id; |
| 93 | 142 | } |
| 94 | 143 | |
| 95 | - if ( ! current_user_can( 'upload_files', $media_id ) ) { | |
| 96 | - return new WP_Error( 'unauthorized', 'User cannot view media', 403 ); | |
| 144 | + if ( ! $this->current_user_can_edit_media_item( $media_id ) ) { | |
| 145 | + return new WP_Error( 'unauthorized', 'User cannot edit media', 403 ); | |
| 97 | 146 | } |
| 98 | 147 | |
| 99 | 148 | $item = $this->get_media_item_v1_1( $media_id ); |
| 100 | 149 | |
| @@ -117,9 +166,23 @@ | ||
| 117 | 166 | $insert['post_content'] = $input['description']; |
| 118 | 167 | } |
| 119 | 168 | |
| 120 | 169 | if ( isset( $input['parent_id'] ) ) { |
| 121 | - $insert['post_parent'] = $input['parent_id']; | |
| 170 | + $parent_id = (int) $input['parent_id']; | |
| 171 | + | |
| 172 | + /* | |
| 173 | + * Attaching media to a post is an edit of that post, so it takes `edit_post` on | |
| 174 | + * the target, as core's WP_REST_Attachments_Controller does for the same field. | |
| 175 | + * Without this a caller attaches their own media to any post on the site. | |
| 176 | + * | |
| 177 | + * Zero is exempt: it detaches the item rather than naming a target, and | |
| 178 | + * `edit_post` fails closed on 0, which would make detaching impossible. | |
| 179 | + */ | |
| 180 | + if ( $parent_id && ! current_user_can( 'edit_post', $parent_id ) ) { | |
| 181 | + return new WP_Error( 'unauthorized', 'User cannot edit the parent post', 403 ); | |
| 182 | + } | |
| 183 | + | |
| 184 | + $insert['post_parent'] = $parent_id; | |
| 122 | 185 | } |
| 123 | 186 | |
| 124 | 187 | if ( isset( $input['alt'] ) ) { |
| 125 | 188 | $alt = wp_strip_all_tags( $input['alt'], true ); |
| @@ -126,9 +189,9 @@ | ||
| 126 | 189 | update_post_meta( $media_id, '_wp_attachment_image_alt', $alt ); |
| 127 | 190 | } |
| 128 | 191 | |
| 129 | 192 | // audio only artist/album info. |
| 130 | - if ( 0 === strpos( $item->mime_type, 'audio/' ) ) { | |
| 193 | + if ( str_starts_with( $item->mime_type, 'audio/' ) ) { | |
| 131 | 194 | $changed = false; |
| 132 | 195 | $id3data = wp_get_attachment_metadata( $media_id ); |
| 133 | 196 | |
| 134 | 197 | if ( ! is_array( $id3data ) ) { |
| @@ -187,14 +250,14 @@ | ||
| 187 | 250 | |
| 188 | 251 | return \Videopress_Attachment_Metadata::persist_metadata( |
| 189 | 252 | $media_id, |
| 190 | 253 | $item->videopress_guid, |
| 191 | - isset( $input['title'] ) ? $input['title'] : null, | |
| 192 | - isset( $input['caption'] ) ? $input['caption'] : null, | |
| 193 | - isset( $input['description'] ) ? $input['description'] : null, | |
| 194 | - isset( $input['rating'] ) ? $input['rating'] : null, | |
| 195 | - isset( $input['display_embed'] ) ? $input['display_embed'] : null, | |
| 196 | - isset( $input['allow_download'] ) ? $input['allow_download'] : null, | |
| 197 | - isset( $input['privacy_setting'] ) ? $input['privacy_setting'] : null | |
| 254 | + $input['title'] ?? null, | |
| 255 | + $input['caption'] ?? null, | |
| 256 | + $input['description'] ?? null, | |
| 257 | + $input['rating'] ?? null, | |
| 258 | + $input['display_embed'] ?? null, | |
| 259 | + $input['allow_download'] ?? null, | |
| 260 | + $input['privacy_setting'] ?? null | |
| 198 | 261 | ); |
| 199 | 262 | } |
| 200 | 263 | } |