Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 142
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
Attachment
0.00% covered (danger)
0.00%
0 / 140
0.00% covered (danger)
0.00%
0 / 11
506
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 register_routes
0.00% covered (danger)
0.00%
0 / 45
0.00% covered (danger)
0.00%
0 / 1
2
 create_item
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
12
 update_item
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 add_additional_fields_schema
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 post_process_item
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 set_upload_dir
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 prepare_item_for_database
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 get_file_info
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 get_attachment_by_file_info
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
30
 prepare_attachment_for_response
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * Attachments REST route
4 *
5 * @package automattic/jetpack-import
6 */
7
8namespace Automattic\Jetpack\Import\Endpoints;
9
10use WP_Error;
11use WP_REST_Request;
12use WP_REST_Response;
13
14if ( ! defined( 'ABSPATH' ) ) {
15    exit( 0 );
16}
17
18/**
19 * Class Attachment
20 */
21class Attachment extends \WP_REST_Attachments_Controller {
22
23    /**
24     * Base class
25     */
26    use Import;
27
28    /**
29     * The Import ID add a new item to the schema.
30     */
31    use Import_ID;
32
33    /**
34     * Whether the controller supports batching. Default false.
35     *
36     * @var false
37     */
38    protected $allow_batch = false;
39
40    /**
41     * Constructor.
42     */
43    public function __construct() {
44        parent::__construct( 'attachment' );
45
46        // @see add_term_meta
47        $this->import_id_meta_type = 'post';
48    }
49
50    /**
51     * Registers the routes for the objects of the controller.
52     *
53     * @see WP_REST_Terms_Controller::register_rest_route()
54     */
55    public function register_routes() {
56        register_rest_route(
57            self::$rest_namespace,
58            '/' . $this->rest_base,
59            $this->get_route_options()
60        );
61
62        register_rest_route(
63            self::$rest_namespace,
64            '/' . $this->rest_base . '/(?P<id>[\d]+)',
65            array(
66                'args'        => array(
67                    'id' => array(
68                        'description' => __( 'Unique identifier for the attachment.', 'jetpack-import' ),
69                        'type'        => 'integer',
70                    ),
71                ),
72                array(
73                    'methods'             => \WP_REST_Server::EDITABLE,
74                    'callback'            => array( $this, 'update_item' ),
75                    'permission_callback' => array( $this, 'update_item_permissions_check' ),
76                    'args'                => $this->get_endpoint_args_for_item_schema( \WP_REST_Server::EDITABLE ),
77                ),
78                'allow_batch' => array( 'v1' => true ),
79                'schema'      => array( $this, 'get_public_item_schema' ),
80            )
81        );
82
83        register_rest_route(
84            self::$rest_namespace,
85            '/' . $this->rest_base . '/(?P<id>[\d]+)/post-process',
86            array(
87                'methods'             => \WP_REST_Server::CREATABLE,
88                'callback'            => array( $this, 'post_process_item' ),
89                'permission_callback' => array( $this, 'import_permissions_callback' ),
90                'args'                => array(
91                    'id'     => array(
92                        'description' => __( 'Unique identifier for the attachment.', 'jetpack-import' ),
93                        'type'        => 'integer',
94                    ),
95                    'action' => array(
96                        'type'     => 'string',
97                        'enum'     => array( 'create-image-subsizes' ),
98                        'required' => true,
99                    ),
100                ),
101            )
102        );
103    }
104
105    /**
106     * Create a single attachment.
107     *
108     * @param WP_REST_Request $request Full details about the request.
109     * @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure.
110     */
111    public function create_item( $request ) {
112        // Set the WP_IMPORTING constant to prevent sync notifications
113        $this->set_importing();
114        $file_info  = $this->get_file_info( $request );
115        $attachment = $this->get_attachment_by_file_info( $file_info );
116        if ( $attachment ) {
117            $response = $this->prepare_attachment_for_response( $attachment, $request );
118
119            if ( \is_wp_error( $response ) ) {
120                return $response;
121            }
122
123            return new WP_Error(
124                'attachment_exists',
125                __( 'The attachment already exists.', 'jetpack-import' ),
126                array(
127                    'status'        => 409,
128                    'attachment'    => $response,
129                    'attachment_id' => $attachment->ID,
130                )
131            );
132        }
133
134        $this->set_upload_dir( $request );
135        // Disable scaled image generation.
136        add_filter( 'big_image_size_threshold', '__return_false' );
137        return parent::create_item( $request );
138    }
139
140    /**
141     * Updates a single attachment.
142     *
143     * @param WP_REST_Request $request Full details about the request.
144     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
145     */
146    public function update_item( $request ) {
147        $response = parent::update_item( $request );
148
149        return $this->add_import_id_metadata( $request, $response );
150    }
151
152    /**
153     * Adds the schema from additional fields to a schema array.
154     *
155     * The type of object is inferred from the passed schema.
156     *
157     * @param array $schema Schema array.
158     * @return array Modified Schema array.
159     */
160    public function add_additional_fields_schema( $schema ) {
161        // Validate the upload_date, used for placing the uploaded file in the correct upload directory.
162        $schema['properties']['upload_date'] = array(
163            'description' => __( 'The date for the upload directory of the attachment.', 'jetpack-import' ),
164            'type'        => array( 'string', 'null' ),
165            'pattern'     => '^\d{4}\/\d{2}$',
166            'required'    => false,
167        );
168
169        // The unique identifier is only required for PUT requests.
170        return $this->add_unique_identifier_to_schema( $schema, isset( $_SERVER['REQUEST_METHOD'] ) && $_SERVER['REQUEST_METHOD'] === 'PUT' );
171    }
172
173    /**
174     * Performs post-processing on an attachment.
175     *
176     * @param WP_REST_Request $request Full details about the request.
177     * @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure.
178     */
179    public function post_process_item( $request ) {
180        require_once ABSPATH . 'wp-admin/includes/image.php';
181
182        \wp_update_image_subsizes( $request['id'] );
183        $request['context'] = 'edit';
184
185        return $this->prepare_item_for_response( \get_post( $request['id'] ), $request );
186    }
187
188    /**
189     * Add a filter that rewrites the upload path.
190     *
191     * @param WP_REST_Request $request Full details about the request.
192     *
193     * @return void
194     * @throws \Exception If the date is invalid.
195     */
196    protected function set_upload_dir( $request ) {
197
198        if ( ! $request->get_param( 'upload_date' ) ) {
199            return;
200        }
201
202        add_filter(
203            'upload_dir',
204            static function ( $data ) use ( $request ) {
205                $date              = $request->get_param( 'upload_date' );
206                $fields_to_rewrite = array( 'path', 'url', 'subdir' );
207                foreach ( $fields_to_rewrite as $field ) {
208                    $data[ $field ] = preg_replace( '/\d{4}\/\d{2}$/', $date, $data[ $field ] );
209                }
210
211                return $data;
212            }
213        );
214    }
215
216    /**
217     * Prepares a single attachment for create or update. This function overrides the parent function
218     *
219     * @param WP_REST_Request $request Request object.
220     * @return \stdClass|WP_Error Post object.
221     */
222    protected function prepare_item_for_database( $request ) {
223        $prepared_attachment = parent::prepare_item_for_database( $request );
224        // date_gmt is equal to the date by default, so we need to override it.
225        if ( $request->get_param( 'date_gmt' ) ) {
226            $prepared_attachment->post_date_gmt = $request->get_param( 'date_gmt' );
227        }
228        return $prepared_attachment;
229    }
230
231    /**
232     * Retrieve the filename and MIME type from the request headers.
233     *
234     * @param WP_REST_Request $request Full details about the request.
235     *
236     * @return array An associative array containing the filename and MIME type.
237     */
238    protected function get_file_info( $request ) {
239        // Get the filename from the Content-Disposition header.
240        $filename_header = $request->get_header( 'content_disposition' );
241        $filename        = self::get_filename_from_disposition( (array) $filename_header );
242        $post_date_gmt   = $request->get_param( 'date_gmt' );
243
244        // Get the MIME type from the Content-Type header.
245        $mime_type = $request->get_header( 'content_type' );
246
247        return array(
248            'filename'      => $filename,
249            'mime_type'     => $mime_type,
250            'post_date_gmt' => $post_date_gmt,
251        );
252    }
253
254    /**
255     * Retrieve attachment metadata by file information.
256     *
257     * This function retrieves attachment metadata for a given file based on its filename, MIME type, and creation date.
258     *
259     * @param array $fileinfo An associative array containing information about the file. The array must contain the following keys:
260     *   - 'filename': The name of the file.
261     *   - 'mime_type': The MIME type of the file (e.g. 'image/jpeg').
262     *   - 'date': The creation date of the file (e.g. '2022-01-01 12:00:00').
263     *
264     * @return mixed An associative array containing metadata for the attachment, or false if no attachment was found.
265     */
266    protected function get_attachment_by_file_info( $fileinfo ) {
267        // Make sure all required variables are set and not empty
268        if ( empty( $fileinfo['filename'] ) || empty( $fileinfo['mime_type'] ) ) {
269            return false;
270        }
271        $original_filename = $fileinfo['filename'];
272        $mime_type         = $fileinfo['mime_type'];
273        $post_date_gmt     = $fileinfo['post_date_gmt'];
274        // From WordPress 5.3, we introduced the scaled image feature, so we'll also need to check for the scaled filename.
275        // https://make.wordpress.org/core/2019/10/09/introducing-handling-of-big-images-in-wordpress-5-3/
276        $extension_pos        = strrpos( $original_filename, '.' );
277        $scaled_filename      = substr( $original_filename, 0, $extension_pos ) . '-scaled' . substr( $original_filename, $extension_pos );
278        $filename_check_array = array( $original_filename, $scaled_filename );
279
280        $args = array(
281            'post_type'      => 'attachment',
282            'post_mime_type' => $mime_type,
283            'date_query'     => array(
284                array(
285                    'after'     => $post_date_gmt,
286                    'before'    => $post_date_gmt,
287                    'inclusive' => true,
288                    'column'    => 'post_date_gmt',
289                ),
290            ),
291            'posts_per_page' => 1,
292        );
293
294        $args['meta_query'] = array( 'relation' => 'OR' );
295        foreach ( $filename_check_array as $filename ) {
296            $args['meta_query'][] = array(
297                'key'     => '_wp_attached_file',
298                'value'   => preg_quote( $filename, '/' ),
299                'compare' => 'REGEXP',
300            );
301        }
302
303        $attachments = \get_posts( $args );
304
305        if ( ! empty( $attachments ) ) {
306            // Return the first attachment data found
307            return $attachments[0];
308        }
309        return false;
310    }
311
312    /**
313     * Prepares an attachment object for REST API response and returns the resulting data as an array.
314     *
315     * @param object $attachment The attachment object to be prepared for response.
316     * @param object $request The REST API request object.
317     *
318     * @return array|WP_Error The prepared data as an array, or a WP_Error object if there was an error preparing the data.
319     */
320    private function prepare_attachment_for_response( $attachment, $request ) {
321        // Prepare attachment data for response
322        $response = $this->prepare_item_for_response( $attachment, $request );
323
324        // Check if there was an error preparing the data
325        if ( \is_wp_error( $response ) ) {
326            return $response;
327        }
328
329        return (array) $response->get_data();
330    }
331}