class-wp-rest-attachments-controller.php 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930
  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. * Core controller used to access attachments via the REST API.
  11. *
  12. * @since 4.7.0
  13. *
  14. * @see WP_REST_Posts_Controller
  15. */
  16. class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
  17. public function register_routes() {
  18. parent::register_routes();
  19. register_rest_route(
  20. $this->namespace,
  21. '/' . $this->rest_base . '/(?P<id>[\d]+)/post-process',
  22. array(
  23. 'methods' => WP_REST_Server::CREATABLE,
  24. 'callback' => array( $this, 'post_process_item' ),
  25. 'permission_callback' => array( $this, 'post_process_item_permissions_check' ),
  26. 'args' => array(
  27. 'id' => array(
  28. 'description' => __( 'Unique identifier for the object.' ),
  29. 'type' => 'integer',
  30. ),
  31. 'action' => array(
  32. 'type' => 'string',
  33. 'enum' => array( 'create-image-subsizes' ),
  34. 'required' => true,
  35. ),
  36. ),
  37. )
  38. );
  39. }
  40. /**
  41. * Determines the allowed query_vars for a get_items() response and
  42. * prepares for WP_Query.
  43. *
  44. * @since 4.7.0
  45. *
  46. * @param array $prepared_args Optional. Array of prepared arguments. Default empty array.
  47. * @param WP_REST_Request $request Optional. Request to prepare items for.
  48. * @return array Array of query arguments.
  49. */
  50. protected function prepare_items_query( $prepared_args = array(), $request = null ) {
  51. $query_args = parent::prepare_items_query( $prepared_args, $request );
  52. if ( empty( $query_args['post_status'] ) ) {
  53. $query_args['post_status'] = 'inherit';
  54. }
  55. $media_types = $this->get_media_types();
  56. if ( ! empty( $request['media_type'] ) && isset( $media_types[ $request['media_type'] ] ) ) {
  57. $query_args['post_mime_type'] = $media_types[ $request['media_type'] ];
  58. }
  59. if ( ! empty( $request['mime_type'] ) ) {
  60. $parts = explode( '/', $request['mime_type'] );
  61. if ( isset( $media_types[ $parts[0] ] ) && in_array( $request['mime_type'], $media_types[ $parts[0] ], true ) ) {
  62. $query_args['post_mime_type'] = $request['mime_type'];
  63. }
  64. }
  65. // Filter query clauses to include filenames.
  66. if ( isset( $query_args['s'] ) ) {
  67. add_filter( 'posts_clauses', '_filter_query_attachment_filenames' );
  68. }
  69. return $query_args;
  70. }
  71. /**
  72. * Checks if a given request has access to create an attachment.
  73. *
  74. * @since 4.7.0
  75. *
  76. * @param WP_REST_Request $request Full details about the request.
  77. * @return WP_Error|true Boolean true if the attachment may be created, or a WP_Error if not.
  78. */
  79. public function create_item_permissions_check( $request ) {
  80. $ret = parent::create_item_permissions_check( $request );
  81. if ( ! $ret || is_wp_error( $ret ) ) {
  82. return $ret;
  83. }
  84. if ( ! current_user_can( 'upload_files' ) ) {
  85. return new WP_Error( 'rest_cannot_create', __( 'Sorry, you are not allowed to upload media on this site.' ), array( 'status' => 400 ) );
  86. }
  87. // Attaching media to a post requires ability to edit said post.
  88. if ( ! empty( $request['post'] ) ) {
  89. $parent = get_post( (int) $request['post'] );
  90. $post_parent_type = get_post_type_object( $parent->post_type );
  91. if ( ! current_user_can( $post_parent_type->cap->edit_post, $request['post'] ) ) {
  92. return new WP_Error( 'rest_cannot_edit', __( 'Sorry, you are not allowed to upload media to this post.' ), array( 'status' => rest_authorization_required_code() ) );
  93. }
  94. }
  95. return true;
  96. }
  97. /**
  98. * Creates a single attachment.
  99. *
  100. * @since 4.7.0
  101. *
  102. * @param WP_REST_Request $request Full details about the request.
  103. * @return WP_Error|WP_REST_Response Response object on success, WP_Error object on failure.
  104. */
  105. public function create_item( $request ) {
  106. if ( ! empty( $request['post'] ) && in_array( get_post_type( $request['post'] ), array( 'revision', 'attachment' ), true ) ) {
  107. return new WP_Error( 'rest_invalid_param', __( 'Invalid parent type.' ), array( 'status' => 400 ) );
  108. }
  109. $insert = $this->insert_attachment( $request );
  110. if ( is_wp_error( $insert ) ) {
  111. return $insert;
  112. }
  113. // Extract by name.
  114. $attachment_id = $insert['attachment_id'];
  115. $file = $insert['file'];
  116. if ( isset( $request['alt_text'] ) ) {
  117. update_post_meta( $attachment_id, '_wp_attachment_image_alt', sanitize_text_field( $request['alt_text'] ) );
  118. }
  119. $attachment = get_post( $attachment_id );
  120. $fields_update = $this->update_additional_fields_for_object( $attachment, $request );
  121. if ( is_wp_error( $fields_update ) ) {
  122. return $fields_update;
  123. }
  124. $request->set_param( 'context', 'edit' );
  125. /**
  126. * Fires after a single attachment is completely created or updated via the REST API.
  127. *
  128. * @since 5.0.0
  129. *
  130. * @param WP_Post $attachment Inserted or updated attachment object.
  131. * @param WP_REST_Request $request Request object.
  132. * @param bool $creating True when creating an attachment, false when updating.
  133. */
  134. do_action( 'rest_after_insert_attachment', $attachment, $request, true );
  135. if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
  136. // Set a custom header with the attachment_id.
  137. // Used by the browser/client to resume creating image sub-sizes after a PHP fatal error.
  138. header( 'X-WP-Upload-Attachment-ID: ' . $attachment_id );
  139. }
  140. // Include admin function to get access to wp_generate_attachment_metadata().
  141. require_once ABSPATH . 'wp-admin/includes/media.php';
  142. // Post-process the upload (create image sub-sizes, make PDF thumbnalis, etc.) and insert attachment meta.
  143. // At this point the server may run out of resources and post-processing of uploaded images may fail.
  144. wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $file ) );
  145. $response = $this->prepare_item_for_response( $attachment, $request );
  146. $response = rest_ensure_response( $response );
  147. $response->set_status( 201 );
  148. $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $attachment_id ) ) );
  149. return $response;
  150. }
  151. /**
  152. * Inserts the attachment post in the database. Does not update the attachment meta.
  153. *
  154. * @since 5.3.0
  155. *
  156. * @param WP_REST_Request $request
  157. * @return array|WP_Error
  158. */
  159. protected function insert_attachment( $request ) {
  160. // Get the file via $_FILES or raw data.
  161. $files = $request->get_file_params();
  162. $headers = $request->get_headers();
  163. if ( ! empty( $files ) ) {
  164. $file = $this->upload_from_file( $files, $headers );
  165. } else {
  166. $file = $this->upload_from_data( $request->get_body(), $headers );
  167. }
  168. if ( is_wp_error( $file ) ) {
  169. return $file;
  170. }
  171. $name = wp_basename( $file['file'] );
  172. $name_parts = pathinfo( $name );
  173. $name = trim( substr( $name, 0, -( 1 + strlen( $name_parts['extension'] ) ) ) );
  174. $url = $file['url'];
  175. $type = $file['type'];
  176. $file = $file['file'];
  177. // Include image functions to get access to wp_read_image_metadata().
  178. require_once ABSPATH . 'wp-admin/includes/image.php';
  179. // use image exif/iptc data for title and caption defaults if possible
  180. $image_meta = wp_read_image_metadata( $file );
  181. if ( ! empty( $image_meta ) ) {
  182. if ( empty( $request['title'] ) && trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) {
  183. $request['title'] = $image_meta['title'];
  184. }
  185. if ( empty( $request['caption'] ) && trim( $image_meta['caption'] ) ) {
  186. $request['caption'] = $image_meta['caption'];
  187. }
  188. }
  189. $attachment = $this->prepare_item_for_database( $request );
  190. $attachment->post_mime_type = $type;
  191. $attachment->guid = $url;
  192. if ( empty( $attachment->post_title ) ) {
  193. $attachment->post_title = preg_replace( '/\.[^.]+$/', '', wp_basename( $file ) );
  194. }
  195. // $post_parent is inherited from $attachment['post_parent'].
  196. $id = wp_insert_attachment( wp_slash( (array) $attachment ), $file, 0, true );
  197. if ( is_wp_error( $id ) ) {
  198. if ( 'db_update_error' === $id->get_error_code() ) {
  199. $id->add_data( array( 'status' => 500 ) );
  200. } else {
  201. $id->add_data( array( 'status' => 400 ) );
  202. }
  203. return $id;
  204. }
  205. $attachment = get_post( $id );
  206. /**
  207. * Fires after a single attachment is created or updated via the REST API.
  208. *
  209. * @since 4.7.0
  210. *
  211. * @param WP_Post $attachment Inserted or updated attachment
  212. * object.
  213. * @param WP_REST_Request $request The request sent to the API.
  214. * @param bool $creating True when creating an attachment, false when updating.
  215. */
  216. do_action( 'rest_insert_attachment', $attachment, $request, true );
  217. return array(
  218. 'attachment_id' => $id,
  219. 'file' => $file,
  220. );
  221. }
  222. /**
  223. * Updates a single attachment.
  224. *
  225. * @since 4.7.0
  226. *
  227. * @param WP_REST_Request $request Full details about the request.
  228. * @return WP_Error|WP_REST_Response Response object on success, WP_Error object on failure.
  229. */
  230. public function update_item( $request ) {
  231. if ( ! empty( $request['post'] ) && in_array( get_post_type( $request['post'] ), array( 'revision', 'attachment' ), true ) ) {
  232. return new WP_Error( 'rest_invalid_param', __( 'Invalid parent type.' ), array( 'status' => 400 ) );
  233. }
  234. $response = parent::update_item( $request );
  235. if ( is_wp_error( $response ) ) {
  236. return $response;
  237. }
  238. $response = rest_ensure_response( $response );
  239. $data = $response->get_data();
  240. if ( isset( $request['alt_text'] ) ) {
  241. update_post_meta( $data['id'], '_wp_attachment_image_alt', $request['alt_text'] );
  242. }
  243. $attachment = get_post( $request['id'] );
  244. $fields_update = $this->update_additional_fields_for_object( $attachment, $request );
  245. if ( is_wp_error( $fields_update ) ) {
  246. return $fields_update;
  247. }
  248. $request->set_param( 'context', 'edit' );
  249. /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php */
  250. do_action( 'rest_after_insert_attachment', $attachment, $request, false );
  251. $response = $this->prepare_item_for_response( $attachment, $request );
  252. $response = rest_ensure_response( $response );
  253. return $response;
  254. }
  255. /**
  256. * Performs post processing on an attachment.
  257. *
  258. * @since 5.3.0
  259. *
  260. * @param WP_REST_Request $request
  261. * @return WP_REST_Response|WP_Error
  262. */
  263. public function post_process_item( $request ) {
  264. switch ( $request['action'] ) {
  265. case 'create-image-subsizes':
  266. require_once ABSPATH . 'wp-admin/includes/image.php';
  267. wp_update_image_subsizes( $request['id'] );
  268. break;
  269. }
  270. $request['context'] = 'edit';
  271. return $this->prepare_item_for_response( get_post( $request['id'] ), $request );
  272. }
  273. /**
  274. * Checks if a given request can perform post processing on an attachment.
  275. *
  276. * @sicne 5.3.0
  277. *
  278. * @param WP_REST_Request $request Full details about the request.
  279. * @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise.
  280. */
  281. public function post_process_item_permissions_check( $request ) {
  282. return $this->update_item_permissions_check( $request );
  283. }
  284. /**
  285. * Prepares a single attachment for create or update.
  286. *
  287. * @since 4.7.0
  288. *
  289. * @param WP_REST_Request $request Request object.
  290. * @return WP_Error|stdClass $prepared_attachment Post object.
  291. */
  292. protected function prepare_item_for_database( $request ) {
  293. $prepared_attachment = parent::prepare_item_for_database( $request );
  294. // Attachment caption (post_excerpt internally)
  295. if ( isset( $request['caption'] ) ) {
  296. if ( is_string( $request['caption'] ) ) {
  297. $prepared_attachment->post_excerpt = $request['caption'];
  298. } elseif ( isset( $request['caption']['raw'] ) ) {
  299. $prepared_attachment->post_excerpt = $request['caption']['raw'];
  300. }
  301. }
  302. // Attachment description (post_content internally)
  303. if ( isset( $request['description'] ) ) {
  304. if ( is_string( $request['description'] ) ) {
  305. $prepared_attachment->post_content = $request['description'];
  306. } elseif ( isset( $request['description']['raw'] ) ) {
  307. $prepared_attachment->post_content = $request['description']['raw'];
  308. }
  309. }
  310. if ( isset( $request['post'] ) ) {
  311. $prepared_attachment->post_parent = (int) $request['post'];
  312. }
  313. return $prepared_attachment;
  314. }
  315. /**
  316. * Prepares a single attachment output for response.
  317. *
  318. * @since 4.7.0
  319. *
  320. * @param WP_Post $post Attachment object.
  321. * @param WP_REST_Request $request Request object.
  322. * @return WP_REST_Response Response object.
  323. */
  324. public function prepare_item_for_response( $post, $request ) {
  325. $response = parent::prepare_item_for_response( $post, $request );
  326. $fields = $this->get_fields_for_response( $request );
  327. $data = $response->get_data();
  328. if ( in_array( 'description', $fields, true ) ) {
  329. $data['description'] = array(
  330. 'raw' => $post->post_content,
  331. /** This filter is documented in wp-includes/post-template.php */
  332. 'rendered' => apply_filters( 'the_content', $post->post_content ),
  333. );
  334. }
  335. if ( in_array( 'caption', $fields, true ) ) {
  336. /** This filter is documented in wp-includes/post-template.php */
  337. $caption = apply_filters( 'the_excerpt', apply_filters( 'get_the_excerpt', $post->post_excerpt, $post ) );
  338. $data['caption'] = array(
  339. 'raw' => $post->post_excerpt,
  340. 'rendered' => $caption,
  341. );
  342. }
  343. if ( in_array( 'alt_text', $fields, true ) ) {
  344. $data['alt_text'] = get_post_meta( $post->ID, '_wp_attachment_image_alt', true );
  345. }
  346. if ( in_array( 'media_type', $fields, true ) ) {
  347. $data['media_type'] = wp_attachment_is_image( $post->ID ) ? 'image' : 'file';
  348. }
  349. if ( in_array( 'mime_type', $fields, true ) ) {
  350. $data['mime_type'] = $post->post_mime_type;
  351. }
  352. if ( in_array( 'media_details', $fields, true ) ) {
  353. $data['media_details'] = wp_get_attachment_metadata( $post->ID );
  354. // Ensure empty details is an empty object.
  355. if ( empty( $data['media_details'] ) ) {
  356. $data['media_details'] = new stdClass;
  357. } elseif ( ! empty( $data['media_details']['sizes'] ) ) {
  358. foreach ( $data['media_details']['sizes'] as $size => &$size_data ) {
  359. if ( isset( $size_data['mime-type'] ) ) {
  360. $size_data['mime_type'] = $size_data['mime-type'];
  361. unset( $size_data['mime-type'] );
  362. }
  363. // Use the same method image_downsize() does.
  364. $image_src = wp_get_attachment_image_src( $post->ID, $size );
  365. if ( ! $image_src ) {
  366. continue;
  367. }
  368. $size_data['source_url'] = $image_src[0];
  369. }
  370. $full_src = wp_get_attachment_image_src( $post->ID, 'full' );
  371. if ( ! empty( $full_src ) ) {
  372. $data['media_details']['sizes']['full'] = array(
  373. 'file' => wp_basename( $full_src[0] ),
  374. 'width' => $full_src[1],
  375. 'height' => $full_src[2],
  376. 'mime_type' => $post->post_mime_type,
  377. 'source_url' => $full_src[0],
  378. );
  379. }
  380. } else {
  381. $data['media_details']['sizes'] = new stdClass;
  382. }
  383. }
  384. if ( in_array( 'post', $fields, true ) ) {
  385. $data['post'] = ! empty( $post->post_parent ) ? (int) $post->post_parent : null;
  386. }
  387. if ( in_array( 'source_url', $fields, true ) ) {
  388. $data['source_url'] = wp_get_attachment_url( $post->ID );
  389. }
  390. if ( in_array( 'missing_image_sizes', $fields, true ) ) {
  391. require_once ABSPATH . 'wp-admin/includes/image.php';
  392. $data['missing_image_sizes'] = array_keys( wp_get_missing_image_subsizes( $post->ID ) );
  393. }
  394. $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
  395. $data = $this->filter_response_by_context( $data, $context );
  396. $links = $response->get_links();
  397. // Wrap the data in a response object.
  398. $response = rest_ensure_response( $data );
  399. foreach ( $links as $rel => $rel_links ) {
  400. foreach ( $rel_links as $link ) {
  401. $response->add_link( $rel, $link['href'], $link['attributes'] );
  402. }
  403. }
  404. /**
  405. * Filters an attachment returned from the REST API.
  406. *
  407. * Allows modification of the attachment right before it is returned.
  408. *
  409. * @since 4.7.0
  410. *
  411. * @param WP_REST_Response $response The response object.
  412. * @param WP_Post $post The original attachment post.
  413. * @param WP_REST_Request $request Request used to generate the response.
  414. */
  415. return apply_filters( 'rest_prepare_attachment', $response, $post, $request );
  416. }
  417. /**
  418. * Retrieves the attachment's schema, conforming to JSON Schema.
  419. *
  420. * @since 4.7.0
  421. *
  422. * @return array Item schema as an array.
  423. */
  424. public function get_item_schema() {
  425. if ( $this->schema ) {
  426. return $this->add_additional_fields_schema( $this->schema );
  427. }
  428. $schema = parent::get_item_schema();
  429. $schema['properties']['alt_text'] = array(
  430. 'description' => __( 'Alternative text to display when attachment is not displayed.' ),
  431. 'type' => 'string',
  432. 'context' => array( 'view', 'edit', 'embed' ),
  433. 'arg_options' => array(
  434. 'sanitize_callback' => 'sanitize_text_field',
  435. ),
  436. );
  437. $schema['properties']['caption'] = array(
  438. 'description' => __( 'The attachment caption.' ),
  439. 'type' => 'object',
  440. 'context' => array( 'view', 'edit', 'embed' ),
  441. 'arg_options' => array(
  442. 'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database()
  443. 'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database()
  444. ),
  445. 'properties' => array(
  446. 'raw' => array(
  447. 'description' => __( 'Caption for the attachment, as it exists in the database.' ),
  448. 'type' => 'string',
  449. 'context' => array( 'edit' ),
  450. ),
  451. 'rendered' => array(
  452. 'description' => __( 'HTML caption for the attachment, transformed for display.' ),
  453. 'type' => 'string',
  454. 'context' => array( 'view', 'edit', 'embed' ),
  455. 'readonly' => true,
  456. ),
  457. ),
  458. );
  459. $schema['properties']['description'] = array(
  460. 'description' => __( 'The attachment description.' ),
  461. 'type' => 'object',
  462. 'context' => array( 'view', 'edit' ),
  463. 'arg_options' => array(
  464. 'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database()
  465. 'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database()
  466. ),
  467. 'properties' => array(
  468. 'raw' => array(
  469. 'description' => __( 'Description for the object, as it exists in the database.' ),
  470. 'type' => 'string',
  471. 'context' => array( 'edit' ),
  472. ),
  473. 'rendered' => array(
  474. 'description' => __( 'HTML description for the object, transformed for display.' ),
  475. 'type' => 'string',
  476. 'context' => array( 'view', 'edit' ),
  477. 'readonly' => true,
  478. ),
  479. ),
  480. );
  481. $schema['properties']['media_type'] = array(
  482. 'description' => __( 'Attachment type.' ),
  483. 'type' => 'string',
  484. 'enum' => array( 'image', 'file' ),
  485. 'context' => array( 'view', 'edit', 'embed' ),
  486. 'readonly' => true,
  487. );
  488. $schema['properties']['mime_type'] = array(
  489. 'description' => __( 'The attachment MIME type.' ),
  490. 'type' => 'string',
  491. 'context' => array( 'view', 'edit', 'embed' ),
  492. 'readonly' => true,
  493. );
  494. $schema['properties']['media_details'] = array(
  495. 'description' => __( 'Details about the media file, specific to its type.' ),
  496. 'type' => 'object',
  497. 'context' => array( 'view', 'edit', 'embed' ),
  498. 'readonly' => true,
  499. );
  500. $schema['properties']['post'] = array(
  501. 'description' => __( 'The ID for the associated post of the attachment.' ),
  502. 'type' => 'integer',
  503. 'context' => array( 'view', 'edit' ),
  504. );
  505. $schema['properties']['source_url'] = array(
  506. 'description' => __( 'URL to the original attachment file.' ),
  507. 'type' => 'string',
  508. 'format' => 'uri',
  509. 'context' => array( 'view', 'edit', 'embed' ),
  510. 'readonly' => true,
  511. );
  512. $schema['properties']['missing_image_sizes'] = array(
  513. 'description' => __( 'List of the missing image sizes of the attachment.' ),
  514. 'type' => 'array',
  515. 'items' => array( 'type' => 'string' ),
  516. 'context' => array( 'edit' ),
  517. 'readonly' => true,
  518. );
  519. unset( $schema['properties']['password'] );
  520. $this->schema = $schema;
  521. return $this->add_additional_fields_schema( $this->schema );
  522. }
  523. /**
  524. * Handles an upload via raw POST data.
  525. *
  526. * @since 4.7.0
  527. *
  528. * @param array $data Supplied file data.
  529. * @param array $headers HTTP headers from the request.
  530. * @return array|WP_Error Data from wp_handle_sideload().
  531. */
  532. protected function upload_from_data( $data, $headers ) {
  533. if ( empty( $data ) ) {
  534. return new WP_Error( 'rest_upload_no_data', __( 'No data supplied.' ), array( 'status' => 400 ) );
  535. }
  536. if ( empty( $headers['content_type'] ) ) {
  537. return new WP_Error( 'rest_upload_no_content_type', __( 'No Content-Type supplied.' ), array( 'status' => 400 ) );
  538. }
  539. if ( empty( $headers['content_disposition'] ) ) {
  540. return new WP_Error( 'rest_upload_no_content_disposition', __( 'No Content-Disposition supplied.' ), array( 'status' => 400 ) );
  541. }
  542. $filename = self::get_filename_from_disposition( $headers['content_disposition'] );
  543. if ( empty( $filename ) ) {
  544. return new WP_Error( 'rest_upload_invalid_disposition', __( 'Invalid Content-Disposition supplied. Content-Disposition needs to be formatted as `attachment; filename="image.png"` or similar.' ), array( 'status' => 400 ) );
  545. }
  546. if ( ! empty( $headers['content_md5'] ) ) {
  547. $content_md5 = array_shift( $headers['content_md5'] );
  548. $expected = trim( $content_md5 );
  549. $actual = md5( $data );
  550. if ( $expected !== $actual ) {
  551. return new WP_Error( 'rest_upload_hash_mismatch', __( 'Content hash did not match expected.' ), array( 'status' => 412 ) );
  552. }
  553. }
  554. // Get the content-type.
  555. $type = array_shift( $headers['content_type'] );
  556. /** Include admin functions to get access to wp_tempnam() and wp_handle_sideload(). */
  557. require_once ABSPATH . 'wp-admin/includes/file.php';
  558. // Save the file.
  559. $tmpfname = wp_tempnam( $filename );
  560. $fp = fopen( $tmpfname, 'w+' );
  561. if ( ! $fp ) {
  562. return new WP_Error( 'rest_upload_file_error', __( 'Could not open file handle.' ), array( 'status' => 500 ) );
  563. }
  564. fwrite( $fp, $data );
  565. fclose( $fp );
  566. // Now, sideload it in.
  567. $file_data = array(
  568. 'error' => null,
  569. 'tmp_name' => $tmpfname,
  570. 'name' => $filename,
  571. 'type' => $type,
  572. );
  573. $size_check = self::check_upload_size( $file_data );
  574. if ( is_wp_error( $size_check ) ) {
  575. return $size_check;
  576. }
  577. $overrides = array(
  578. 'test_form' => false,
  579. );
  580. $sideloaded = wp_handle_sideload( $file_data, $overrides );
  581. if ( isset( $sideloaded['error'] ) ) {
  582. @unlink( $tmpfname );
  583. return new WP_Error( 'rest_upload_sideload_error', $sideloaded['error'], array( 'status' => 500 ) );
  584. }
  585. return $sideloaded;
  586. }
  587. /**
  588. * Parses filename from a Content-Disposition header value.
  589. *
  590. * As per RFC6266:
  591. *
  592. * content-disposition = "Content-Disposition" ":"
  593. * disposition-type *( ";" disposition-parm )
  594. *
  595. * disposition-type = "inline" | "attachment" | disp-ext-type
  596. * ; case-insensitive
  597. * disp-ext-type = token
  598. *
  599. * disposition-parm = filename-parm | disp-ext-parm
  600. *
  601. * filename-parm = "filename" "=" value
  602. * | "filename*" "=" ext-value
  603. *
  604. * disp-ext-parm = token "=" value
  605. * | ext-token "=" ext-value
  606. * ext-token = <the characters in token, followed by "*">
  607. *
  608. * @since 4.7.0
  609. *
  610. * @link http://tools.ietf.org/html/rfc2388
  611. * @link http://tools.ietf.org/html/rfc6266
  612. *
  613. * @param string[] $disposition_header List of Content-Disposition header values.
  614. * @return string|null Filename if available, or null if not found.
  615. */
  616. public static function get_filename_from_disposition( $disposition_header ) {
  617. // Get the filename.
  618. $filename = null;
  619. foreach ( $disposition_header as $value ) {
  620. $value = trim( $value );
  621. if ( strpos( $value, ';' ) === false ) {
  622. continue;
  623. }
  624. list( $type, $attr_parts ) = explode( ';', $value, 2 );
  625. $attr_parts = explode( ';', $attr_parts );
  626. $attributes = array();
  627. foreach ( $attr_parts as $part ) {
  628. if ( strpos( $part, '=' ) === false ) {
  629. continue;
  630. }
  631. list( $key, $value ) = explode( '=', $part, 2 );
  632. $attributes[ trim( $key ) ] = trim( $value );
  633. }
  634. if ( empty( $attributes['filename'] ) ) {
  635. continue;
  636. }
  637. $filename = trim( $attributes['filename'] );
  638. // Unquote quoted filename, but after trimming.
  639. if ( substr( $filename, 0, 1 ) === '"' && substr( $filename, -1, 1 ) === '"' ) {
  640. $filename = substr( $filename, 1, -1 );
  641. }
  642. }
  643. return $filename;
  644. }
  645. /**
  646. * Retrieves the query params for collections of attachments.
  647. *
  648. * @since 4.7.0
  649. *
  650. * @return array Query parameters for the attachment collection as an array.
  651. */
  652. public function get_collection_params() {
  653. $params = parent::get_collection_params();
  654. $params['status']['default'] = 'inherit';
  655. $params['status']['items']['enum'] = array( 'inherit', 'private', 'trash' );
  656. $media_types = $this->get_media_types();
  657. $params['media_type'] = array(
  658. 'default' => null,
  659. 'description' => __( 'Limit result set to attachments of a particular media type.' ),
  660. 'type' => 'string',
  661. 'enum' => array_keys( $media_types ),
  662. );
  663. $params['mime_type'] = array(
  664. 'default' => null,
  665. 'description' => __( 'Limit result set to attachments of a particular MIME type.' ),
  666. 'type' => 'string',
  667. );
  668. return $params;
  669. }
  670. /**
  671. * Handles an upload via multipart/form-data ($_FILES).
  672. *
  673. * @since 4.7.0
  674. *
  675. * @param array $files Data from the `$_FILES` superglobal.
  676. * @param array $headers HTTP headers from the request.
  677. * @return array|WP_Error Data from wp_handle_upload().
  678. */
  679. protected function upload_from_file( $files, $headers ) {
  680. if ( empty( $files ) ) {
  681. return new WP_Error( 'rest_upload_no_data', __( 'No data supplied.' ), array( 'status' => 400 ) );
  682. }
  683. // Verify hash, if given.
  684. if ( ! empty( $headers['content_md5'] ) ) {
  685. $content_md5 = array_shift( $headers['content_md5'] );
  686. $expected = trim( $content_md5 );
  687. $actual = md5_file( $files['file']['tmp_name'] );
  688. if ( $expected !== $actual ) {
  689. return new WP_Error( 'rest_upload_hash_mismatch', __( 'Content hash did not match expected.' ), array( 'status' => 412 ) );
  690. }
  691. }
  692. // Pass off to WP to handle the actual upload.
  693. $overrides = array(
  694. 'test_form' => false,
  695. );
  696. // Bypasses is_uploaded_file() when running unit tests.
  697. if ( defined( 'DIR_TESTDATA' ) && DIR_TESTDATA ) {
  698. $overrides['action'] = 'wp_handle_mock_upload';
  699. }
  700. $size_check = self::check_upload_size( $files['file'] );
  701. if ( is_wp_error( $size_check ) ) {
  702. return $size_check;
  703. }
  704. /** Include admin function to get access to wp_handle_upload(). */
  705. require_once ABSPATH . 'wp-admin/includes/file.php';
  706. $file = wp_handle_upload( $files['file'], $overrides );
  707. if ( isset( $file['error'] ) ) {
  708. return new WP_Error( 'rest_upload_unknown_error', $file['error'], array( 'status' => 500 ) );
  709. }
  710. return $file;
  711. }
  712. /**
  713. * Retrieves the supported media types.
  714. *
  715. * Media types are considered the MIME type category.
  716. *
  717. * @since 4.7.0
  718. *
  719. * @return array Array of supported media types.
  720. */
  721. protected function get_media_types() {
  722. $media_types = array();
  723. foreach ( get_allowed_mime_types() as $mime_type ) {
  724. $parts = explode( '/', $mime_type );
  725. if ( ! isset( $media_types[ $parts[0] ] ) ) {
  726. $media_types[ $parts[0] ] = array();
  727. }
  728. $media_types[ $parts[0] ][] = $mime_type;
  729. }
  730. return $media_types;
  731. }
  732. /**
  733. * Determine if uploaded file exceeds space quota on multisite.
  734. *
  735. * Replicates check_upload_size().
  736. *
  737. * @since 4.9.8
  738. *
  739. * @param array $file $_FILES array for a given file.
  740. * @return true|WP_Error True if can upload, error for errors.
  741. */
  742. protected function check_upload_size( $file ) {
  743. if ( ! is_multisite() ) {
  744. return true;
  745. }
  746. if ( get_site_option( 'upload_space_check_disabled' ) ) {
  747. return true;
  748. }
  749. $space_left = get_upload_space_available();
  750. $file_size = filesize( $file['tmp_name'] );
  751. if ( $space_left < $file_size ) {
  752. /* translators: %s: Required disk space in kilobytes. */
  753. return new WP_Error( 'rest_upload_limited_space', sprintf( __( 'Not enough space to upload. %s KB needed.' ), number_format( ( $file_size - $space_left ) / KB_IN_BYTES ) ), array( 'status' => 400 ) );
  754. }
  755. if ( $file_size > ( KB_IN_BYTES * get_site_option( 'fileupload_maxk', 1500 ) ) ) {
  756. /* translators: %s: Maximum allowed file size in kilobytes. */
  757. return new WP_Error( 'rest_upload_file_too_big', sprintf( __( 'This file is too big. Files must be less than %s KB in size.' ), get_site_option( 'fileupload_maxk', 1500 ) ), array( 'status' => 400 ) );
  758. }
  759. // Include admin function to get access to upload_is_user_over_quota().
  760. require_once ABSPATH . 'wp-admin/includes/ms.php';
  761. if ( upload_is_user_over_quota( false ) ) {
  762. return new WP_Error( 'rest_upload_user_quota_exceeded', __( 'You have used your space quota. Please delete files before uploading.' ), array( 'status' => 400 ) );
  763. }
  764. return true;
  765. }
  766. }