diff --git a/src/wp-admin/includes/file.php b/src/wp-admin/includes/file.php index 0c6d968ea02d3..431a4078e3481 100644 --- a/src/wp-admin/includes/file.php +++ b/src/wp-admin/includes/file.php @@ -764,6 +764,7 @@ function validate_file_to_edit( $file, $allowed_files = array() ) { * * @access private * @since 4.0.0 + * @since 7.1.0 Added the `$post` parameter. * * @see wp_handle_upload_error * @@ -790,8 +791,10 @@ function validate_file_to_edit( $file, $allowed_files = array() ) { * @type bool $test_type Whether to test that the mime type of the file is as expected. * @type string[] $mimes Array of allowed mime types keyed by their file extension regex. * } - * @param string $time Time formatted in 'yyyy/mm'. - * @param string $action Expected value for `$_POST['action']`. + * @param string|null $time Time to use for the upload directory. Accepts a 'yyyy/mm' + * formatted string or a MySQL datetime. + * @param string $action Expected value for `$_POST['action']`. + * @param WP_Post|null $post Optional. Post object the upload is associated with. Default null. * @return array { * On success, returns an associative array of file attributes. * On failure, returns `$overrides['upload_error_handler']( &$file, $message )` @@ -802,7 +805,7 @@ function validate_file_to_edit( $file, $allowed_files = array() ) { * @type string $type Mime type of the newly-uploaded file. * } */ -function _wp_handle_upload( &$file, $overrides, $time, $action ) { +function _wp_handle_upload( &$file, $overrides, $time, $action, $post = null ) { // The default error handler. if ( ! function_exists( 'wp_handle_upload_error' ) ) { function wp_handle_upload_error( &$file, $message ) { @@ -975,6 +978,23 @@ function wp_handle_upload_error( &$file, $message ) { $type = ''; } + /** + * Filters the time value used to determine the directory where the file is stored. + * + * The filtered value is passed to wp_upload_dir(), where it is used to determine + * the year/month subdirectory when year/month folders are enabled. + * + * @since 7.1.0 + * + * @param string|null $time Time to use for the upload directory. Accepts a 'yyyy/mm' + * formatted string or a MySQL datetime. + * @param WP_Post|null $post Post object the upload is associated with, or null. + * @param array $file Reference to a single element from `$_FILES`. + * @param array|false $overrides An array of override parameters for this file, or boolean false. + * @param string $action Expected value for `$_POST['action']`. + */ + $time = apply_filters( 'wp_handle_upload_time', $time, $post, $file, $overrides, $action ); + /* * A writable uploads dir will pass this test. Again, there's no point * overriding this one. @@ -1083,6 +1103,7 @@ function wp_handle_upload_error( &$file, $message ) { * Passes the {@see 'wp_handle_upload'} action. * * @since 2.0.0 + * @since 7.1.0 Added the `$post` parameter. * * @see _wp_handle_upload() * @@ -1092,16 +1113,18 @@ function wp_handle_upload_error( &$file, $message ) { * @param array|false $overrides Optional. An associative array of names => values * to override default variables. Default false. * See _wp_handle_upload() for accepted values. - * @param string|null $time Optional. Time formatted in 'yyyy/mm'. Default null. + * @param string|null $time Optional. Time to use for the upload directory. Accepts a 'yyyy/mm' + * formatted string or a MySQL datetime. Default null. + * @param WP_Post|null $post Optional. Post object the upload is associated with. Default null. * @return array See _wp_handle_upload() for return value. */ -function wp_handle_upload( &$file, $overrides = false, $time = null ) { +function wp_handle_upload( &$file, $overrides = false, $time = null, $post = null ) { /* * $_POST['action'] must be set and its value must equal $overrides['action'] * or this: */ $action = $overrides['action'] ?? 'wp_handle_upload'; - return _wp_handle_upload( $file, $overrides, $time, $action ); + return _wp_handle_upload( $file, $overrides, $time, $action, $post ); } /** @@ -1110,6 +1133,7 @@ function wp_handle_upload( &$file, $overrides = false, $time = null ) { * Passes the {@see 'wp_handle_sideload'} action. * * @since 2.6.0 + * @since 7.1.0 Added the `$post` parameter. * * @see _wp_handle_upload() * @@ -1119,16 +1143,18 @@ function wp_handle_upload( &$file, $overrides = false, $time = null ) { * @param array|false $overrides Optional. An associative array of names => values * to override default variables. Default false. * See _wp_handle_upload() for accepted values. - * @param string|null $time Optional. Time formatted in 'yyyy/mm'. Default null. + * @param string|null $time Optional. Time to use for the upload directory. Accepts a 'yyyy/mm' + * formatted string or a MySQL datetime. Default null. + * @param WP_Post|null $post Optional. Post object the upload is associated with. Default null. * @return array See _wp_handle_upload() for return value. */ -function wp_handle_sideload( &$file, $overrides = false, $time = null ) { +function wp_handle_sideload( &$file, $overrides = false, $time = null, $post = null ) { /* * $_POST['action'] must be set and its value must equal $overrides['action'] * or this: */ $action = $overrides['action'] ?? 'wp_handle_sideload'; - return _wp_handle_upload( $file, $overrides, $time, $action ); + return _wp_handle_upload( $file, $overrides, $time, $action, $post ); } /** diff --git a/src/wp-admin/includes/media.php b/src/wp-admin/includes/media.php index 71ae2a9eea4cc..c3e4bfa481b7e 100644 --- a/src/wp-admin/includes/media.php +++ b/src/wp-admin/includes/media.php @@ -304,7 +304,7 @@ function media_handle_upload( $file_id, $post_id, $post_data = array(), $overrid } } - $file = wp_handle_upload( $_FILES[ $file_id ], $overrides, $time ); + $file = wp_handle_upload( $_FILES[ $file_id ], $overrides, $time, $post ); if ( isset( $file['error'] ) ) { return new WP_Error( 'upload_error', $file['error'] ); @@ -465,8 +465,14 @@ function media_handle_upload( $file_id, $post_id, $post_data = array(), $overrid function media_handle_sideload( $file_array, $post_id = 0, $desc = null, $post_data = array() ) { $overrides = array( 'test_form' => false ); + $post = null; + if ( isset( $post_data['post_date'] ) && substr( $post_data['post_date'], 0, 4 ) > 0 ) { $time = $post_data['post_date']; + + if ( $post_id ) { + $post = get_post( $post_id ); + } } else { $post = get_post( $post_id ); if ( $post && substr( $post->post_date, 0, 4 ) > 0 ) { @@ -476,7 +482,7 @@ function media_handle_sideload( $file_array, $post_id = 0, $desc = null, $post_d } } - $file = wp_handle_sideload( $file_array, $overrides, $time ); + $file = wp_handle_sideload( $file_array, $overrides, $time, $post ); if ( isset( $file['error'] ) ) { return new WP_Error( 'upload_error', $file['error'] ); diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php index 21805778ba659..77cc81a99f142 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php @@ -24,6 +24,14 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller { */ protected $allow_batch = false; + /** + * Post object associated with the current upload. + * + * @since 7.1.0 + * @var WP_Post|null + */ + private $upload_post = null; + /** * Registers the routes for attachments. * @@ -436,6 +444,7 @@ protected function insert_attachment( $request ) { $headers = $request->get_headers(); $time = null; + $post = null; // Matches logic in media_handle_upload(). if ( ! empty( $request['post'] ) ) { @@ -446,12 +455,16 @@ protected function insert_attachment( $request ) { } } + $this->upload_post = $post; + if ( ! empty( $files ) ) { $file = $this->upload_from_file( $files, $headers, $time ); } else { $file = $this->upload_from_data( $request->get_body(), $headers, $time ); } + $this->upload_post = null; + if ( is_wp_error( $file ) ) { return $file; } @@ -1479,7 +1492,7 @@ protected function upload_from_data( $data, $headers, $time = null ) { 'test_form' => false, ); - $sideloaded = wp_handle_sideload( $file_data, $overrides, $time ); + $sideloaded = wp_handle_sideload( $file_data, $overrides, $time, $this->upload_post ); if ( isset( $sideloaded['error'] ) ) { @unlink( $tmpfname ); @@ -1653,7 +1666,7 @@ protected function upload_from_file( $files, $headers, $time = null ) { // Include filesystem functions to get access to wp_handle_upload(). require_once ABSPATH . 'wp-admin/includes/file.php'; - $file = wp_handle_upload( $files['file'], $overrides, $time ); + $file = wp_handle_upload( $files['file'], $overrides, $time, $this->upload_post ); if ( isset( $file['error'] ) ) { return new WP_Error( @@ -2064,12 +2077,16 @@ public function sideload_item( WP_REST_Request $request ) { $time = $parent_post->post_date; } + $this->upload_post = $parent_post; + if ( ! empty( $files ) ) { $file = $this->upload_from_file( $files, $headers, $time ); } else { $file = $this->upload_from_data( $request->get_body(), $headers, $time ); } + $this->upload_post = null; + remove_filter( 'wp_unique_filename', $filter_filename ); remove_filter( 'image_editor_output_format', '__return_empty_array', 100 ); diff --git a/tests/phpunit/tests/media.php b/tests/phpunit/tests/media.php index c3e118ee718d5..513daf86bc1c2 100644 --- a/tests/phpunit/tests/media.php +++ b/tests/phpunit/tests/media.php @@ -3129,6 +3129,158 @@ public function test_media_handle_upload_ignores_page_parent_for_directory_date( $this->assertSame( $expected, $url ); } + /** + * @ticket 46554 + */ + public function test_media_handle_upload_time_can_be_filtered_for_page_parent() { + $iptc_file = DIR_TESTDATA . '/images/test-image-iptc.jpg'; + + // Make a copy of this file as it gets moved during the file upload. + $tmp_name = wp_tempnam( $iptc_file ); + + copy( $iptc_file, $tmp_name ); + + $_FILES['upload'] = array( + 'tmp_name' => $tmp_name, + 'name' => 'test-image-iptc.jpg', + 'type' => 'image/jpeg', + 'error' => 0, + 'size' => filesize( $iptc_file ), + ); + + $parent_id = self::factory()->post->create( + array( + 'post_date' => '2010-01-01 12:00:00', + 'post_type' => 'page', + ) + ); + + $filter_args = null; + $filter = static function ( $time, $post, $file, $overrides, $action ) use ( &$filter_args ) { + $filter_args = array( + 'time' => $time, + 'post' => $post, + 'file' => $file, + 'overrides' => $overrides, + 'action' => $action, + ); + + if ( $post instanceof WP_Post && 'page' === $post->post_type ) { + return $post->post_date; + } + + return $time; + }; + + add_filter( 'wp_handle_upload_time', $filter, 10, 5 ); + + $post_id = media_handle_upload( + 'upload', + $parent_id, + array(), + array( + 'action' => 'test_iptc_upload', + 'test_form' => false, + ) + ); + + remove_filter( 'wp_handle_upload_time', $filter, 10 ); + unset( $_FILES['upload'] ); + + $this->assertNotWPError( $post_id ); + + $url = wp_get_attachment_url( $post_id ); + $uploads_dir = wp_upload_dir( '2010/01' ); + $expected = $uploads_dir['url'] . '/test-image-iptc.jpg'; + + // Clean up. + wp_delete_attachment( $post_id, true ); + wp_delete_post( $parent_id, true ); + + $this->assertSame( $expected, $url ); + $this->assertIsString( $filter_args['time'] ); + $this->assertSame( 'test-image-iptc.jpg', $filter_args['file']['name'] ); + $this->assertFalse( $filter_args['overrides']['test_form'] ); + $this->assertSame( 'test_iptc_upload', $filter_args['action'] ); + $this->assertInstanceOf( WP_Post::class, $filter_args['post'] ); + $this->assertSame( $parent_id, $filter_args['post']->ID ); + } + + /** + * @ticket 46554 + */ + public function test_media_handle_sideload_time_can_be_filtered_for_page_parent() { + $iptc_file = DIR_TESTDATA . '/images/test-image-iptc.jpg'; + + // Make a copy of this file as it gets moved during the file sideload. + $tmp_name = wp_tempnam( $iptc_file ); + + copy( $iptc_file, $tmp_name ); + + $file_array = array( + 'tmp_name' => $tmp_name, + 'name' => 'test-image-iptc.jpg', + 'type' => 'image/jpeg', + 'error' => 0, + 'size' => filesize( $iptc_file ), + ); + + $parent_id = self::factory()->post->create( + array( + 'post_date' => '2010-01-01 12:00:00', + 'post_type' => 'page', + ) + ); + + $filter_args = null; + $filter = static function ( $time, $post, $file, $overrides, $action ) use ( &$filter_args ) { + $filter_args = array( + 'time' => $time, + 'post' => $post, + 'file' => $file, + 'overrides' => $overrides, + 'action' => $action, + ); + + if ( $post instanceof WP_Post && 'page' === $post->post_type ) { + return $post->post_date; + } + + return $time; + }; + + add_filter( 'wp_handle_upload_time', $filter, 10, 5 ); + + $post_id = media_handle_sideload( + $file_array, + $parent_id, + null, + array( + 'post_date' => '2011-02-01 12:00:00', + ) + ); + + remove_filter( 'wp_handle_upload_time', $filter, 10 ); + + $this->assertNotWPError( $post_id ); + + $url = wp_get_attachment_url( $post_id ); + $uploads_dir = wp_upload_dir( '2010/01' ); + $expected = $uploads_dir['url'] . '/test-image-iptc.jpg'; + + // Clean up. + wp_delete_attachment( $post_id, true ); + wp_delete_post( $parent_id, true ); + + $this->assertSame( $expected, $url ); + $this->assertSame( '2011-02-01 12:00:00', $filter_args['time'] ); + $this->assertSame( 'test-image-iptc.jpg', $filter_args['file']['name'] ); + $this->assertFalse( $filter_args['overrides']['test_form'] ); + $this->assertSame( 'wp_handle_sideload', $filter_args['action'] ); + $this->assertInstanceOf( WP_Post::class, $filter_args['post'] ); + $this->assertSame( $parent_id, $filter_args['post']->ID ); + } + /** * @ticket 50367 * @requires function imagejpeg diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index 79e9d23cf9dd3..2797df1ce1f40 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -1186,6 +1186,128 @@ public function test_create_item_with_files() { $this->assertSame( 201, $response->get_status() ); } + /** + * @ticket 46554 + * @requires function imagejpeg + */ + public function test_create_item_upload_time_can_be_filtered_for_page_parent() { + wp_set_current_user( self::$editor_id ); + + $parent_id = self::factory()->post->create( + array( + 'post_date' => '2010-01-01 12:00:00', + 'post_type' => 'page', + ) + ); + + $filter_args = null; + $filter = static function ( $time, $post, $file, $overrides, $action ) use ( &$filter_args ) { + $filter_args = array( + 'time' => $time, + 'post' => $post, + 'file' => $file, + 'overrides' => $overrides, + 'action' => $action, + ); + + if ( $post instanceof WP_Post && 'page' === $post->post_type ) { + return $post->post_date; + } + + return $time; + }; + + $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); + $request->set_file_params( + array( + 'file' => array( + 'file' => file_get_contents( self::$test_file ), + 'name' => 'canola.jpg', + 'size' => filesize( self::$test_file ), + 'tmp_name' => self::$test_file, + ), + ) + ); + $request->set_header( 'Content-MD5', md5_file( self::$test_file ) ); + $request->set_param( 'post', $parent_id ); + + add_filter( 'wp_handle_upload_time', $filter, 10, 5 ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( 'wp_handle_upload_time', $filter, 10 ); + + $data = $response->get_data(); + + $this->assertSame( 201, $response->get_status() ); + + $uploads_dir = wp_upload_dir( '2010/01' ); + $expected = $uploads_dir['url'] . '/canola.jpg'; + + $this->assertSame( $expected, $data['source_url'] ); + $this->assertNull( $filter_args['time'] ); + $this->assertSame( 'canola.jpg', $filter_args['file']['name'] ); + $this->assertFalse( $filter_args['overrides']['test_form'] ); + $this->assertSame( 'wp_handle_mock_upload', $filter_args['action'] ); + $this->assertInstanceOf( WP_Post::class, $filter_args['post'] ); + $this->assertSame( $parent_id, $filter_args['post']->ID ); + } + + /** + * @ticket 46554 + * @requires function imagejpeg + */ + public function test_create_item_upload_time_can_be_filtered_for_page_parent_with_raw_data() { + wp_set_current_user( self::$editor_id ); + + $parent_id = self::factory()->post->create( + array( + 'post_date' => '2010-01-01 12:00:00', + 'post_type' => 'page', + ) + ); + + $filter_args = null; + $filter = static function ( $time, $post, $file, $overrides, $action ) use ( &$filter_args ) { + $filter_args = array( + 'time' => $time, + 'post' => $post, + 'file' => $file, + 'overrides' => $overrides, + 'action' => $action, + ); + + if ( $post instanceof WP_Post && 'page' === $post->post_type ) { + return $post->post_date; + } + + return $time; + }; + + $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); + $request->set_header( 'Content-Type', 'image/jpeg' ); + $request->set_header( 'Content-Disposition', 'attachment; filename=canola-raw.jpg' ); + $request->set_param( 'post', $parent_id ); + $request->set_body( file_get_contents( self::$test_file ) ); + + add_filter( 'wp_handle_upload_time', $filter, 10, 5 ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( 'wp_handle_upload_time', $filter, 10 ); + + $data = $response->get_data(); + + $this->assertSame( 201, $response->get_status() ); + + $uploads_dir = wp_upload_dir( '2010/01' ); + $expected = $uploads_dir['url'] . '/canola-raw.jpg'; + + $this->assertSame( $expected, $data['source_url'] ); + $this->assertNull( $filter_args['time'] ); + $this->assertSame( 'canola-raw.jpg', $filter_args['file']['name'] ); + $this->assertFalse( $filter_args['overrides']['test_form'] ); + $this->assertSame( 'wp_handle_sideload', $filter_args['action'] ); + $this->assertInstanceOf( WP_Post::class, $filter_args['post'] ); + $this->assertSame( $parent_id, $filter_args['post']->ID ); + } + /** * @requires function imagejpeg */