PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 15.0
Jetpack – WP Security, Backup, Speed, & Growth v15.0
16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 14.3.1 All 501 releases
jetpack / _inc / lib / class.media.php

class.media.php in Jetpack – WP Security, Backup, Speed, & Growth 15.0, at _inc/lib/class.media.php

510 lines 15.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2
3 if ( ! defined( 'ABSPATH' ) ) {
4 exit( 0 );
5 }
6
7 require_once JETPACK__PLUGIN_DIR . 'sal/class.json-api-date.php';
8
9 /**
10 * Class to handle different actions related to media.
11 */
12 class Jetpack_Media {
13 /**
14 * Original media meta data. Metadata key as stored by WP.
15 *
16 * @var string
17 */
18 const WP_ORIGINAL_MEDIA = '_wp_original_post_media';
19 /**
20 * Revision history. Metadata key as stored by WP.
21 *
22 * @var string
23 */
24 const WP_REVISION_HISTORY = '_wp_revision_history';
25 /**
26 * Maximum amount of revisions.
27 *
28 * @var int
29 */
30 const REVISION_HISTORY_MAXIMUM_AMOUNT = 0;
31 /**
32 * Image Alt. Metadata key as stored by WP.
33 *
34 * @var string
35 */
36 const WP_ATTACHMENT_IMAGE_ALT = '_wp_attachment_image_alt';
37
38 /**
39 * Generate a filename in function of the original filename of the media.
40 * The returned name has the `{basename}-{hash}-{random-number}.{ext}` shape.
41 * The hash is built according to the filename trying to avoid name collisions
42 * with other media files.
43 *
44 * @param int $media_id - media post ID.
45 * @param string $new_filename - the new filename.
46 * @return string A random filename.
47 */
48 public static function generate_new_filename( $media_id, $new_filename ) {
49 // Get the right filename extension.
50 $new_filename_paths = pathinfo( $new_filename );
51 $new_file_ext = $new_filename_paths['extension'];
52
53 // Get the file parts from the current attachment.
54 $current_file = get_attached_file( $media_id );
55 $current_file_parts = pathinfo( $current_file );
56 $current_file_ext = $current_file_parts['extension'];
57 $current_file_dirname = $current_file_parts['dirname'];
58
59 // Take out filename from the original file or from the current attachment.
60 $original_media = (array) self::get_original_media( $media_id );
61
62 if ( ! empty( $original_media ) ) {
63 $original_file_parts = pathinfo( $original_media['file'] );
64 $filename_base = $original_file_parts['filename'];
65 } else {
66 $filename_base = $current_file_parts['filename'];
67 }
68
69 // Add unique seed based on the filename.
70 $filename_base .= '-' . crc32( $filename_base ) . '-';
71
72 $number_suffix = time() . wp_rand( 100, 999 );
73
74 do {
75 $filename = $filename_base;
76 $filename .= "e{$number_suffix}";
77 $file_ext = $new_file_ext ? $new_file_ext : $current_file_ext;
78
79 $new_filename = "{$filename}.{$file_ext}";
80 $new_path = "{$current_file_dirname}/$new_filename";
81 ++$number_suffix;
82 } while ( file_exists( $new_path ) );
83
84 return $new_filename;
85 }
86
87 /**
88 * File urls use the post (image item) date to generate a folder path.
89 * Post dates can change, so we use the original date used in the `guid`
90 * url so edits can remain in the same folder. In the following function
91 * we capture a string in the format of `YYYY/MM` from the guid.
92 *
93 * For example with a guid of
94 * "http://test.files.wordpress.com/2016/10/test.png" the resulting string
95 * would be: "2016/10"
96 *
97 * @param int $media_id Attachment ID.
98 * @return string
99 */
100 private static function get_time_string_from_guid( $media_id ) {
101 $time = gmdate( 'Y/m', strtotime( current_time( 'mysql' ) ) );
102
103 $media = get_post( $media_id );
104 if ( $media ) {
105 $pattern = '/\/(\d{4}\/\d{2})\//';
106 preg_match( $pattern, $media->guid, $matches );
107 if ( count( $matches ) > 1 ) {
108 $time = $matches[1];
109 }
110 }
111 return $time;
112 }
113
114 /**
115 * Return an array of allowed mime_type items used to upload a media file.
116 *
117 * @param array $default_mime_types Array of mime types.
118 *
119 * @return array mime_type array
120 */
121 public static function get_allowed_mime_types( $default_mime_types ) {
122 return array_unique(
123 array_merge(
124 $default_mime_types,
125 array(
126 'application/msword', // .doc
127 'application/vnd.ms-powerpoint', // .ppt, .pps
128 'application/vnd.ms-excel', // .xls
129 'application/vnd.openxmlformats-officedocument.presentationml.presentation', // .pptx
130 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', // .ppsx
131 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx
132 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .docx
133 'application/vnd.oasis.opendocument.text', // .odt
134 'application/pdf', // .pdf
135 )
136 )
137 );
138 }
139
140 /**
141 * Checks that the mime type of the file
142 * is among those in a filterable list of mime types.
143 *
144 * @param string $file Path to file to get its mime type.
145 * @return bool
146 */
147 protected static function is_file_supported_for_sideloading( $file ) {
148 return jetpack_is_file_supported_for_sideloading( $file );
149 }
150
151 /**
152 * Save the given uploaded temporary file considering file type,
153 * correct location according to the original file path, etc.
154 * The file type control is done through of `jetpack_supported_media_sideload_types` filter,
155 * which allows define to the users their own file types list.
156 *
157 * Note this does not support sideloads, only uploads.
158 *
159 * @param array $file_array Data derived from `$_FILES` for an uploaded file.
160 * @param int $media_id Attachment ID.
161 * @return array|WP_Error an array with information about the new file saved or a WP_Error is something went wrong.
162 */
163 public static function save_temporary_file( $file_array, $media_id ) {
164 $tmp_filename = $file_array['tmp_name'];
165
166 if ( ! is_uploaded_file( $tmp_filename ) ) {
167 return new WP_Error( 'invalid_input', 'No media provided in input.' );
168 }
169
170 // add additional mime_types through of the `jetpack_supported_media_sideload_types` filter.
171 $mime_type_static_filter = array(
172 'Jetpack_Media',
173 'get_allowed_mime_types',
174 );
175
176 add_filter( 'jetpack_supported_media_sideload_types', $mime_type_static_filter );
177 if (
178 ! self::is_file_supported_for_sideloading( $tmp_filename ) &&
179 ! file_is_displayable_image( $tmp_filename )
180 ) {
181 return new WP_Error( 'invalid_input', 'Invalid file type.', 403 );
182 }
183 remove_filter( 'jetpack_supported_media_sideload_types', $mime_type_static_filter );
184
185 // generate a new file name.
186 $tmp_new_filename = self::generate_new_filename( $media_id, $file_array['name'] );
187
188 // start to create the parameters to move the temporal file.
189 $overrides = array( 'test_form' => false );
190
191 // get time according to the original filaname.
192 $time = self::get_time_string_from_guid( $media_id );
193
194 $file_array['name'] = $tmp_new_filename;
195 $file = wp_handle_upload( $file_array, $overrides, $time );
196
197 if ( isset( $file['error'] ) ) {
198 return new WP_Error( 'upload_error', $file['error'] );
199 }
200
201 return $file;
202 }
203
204 /**
205 * Return an object with an snapshot of a revision item.
206 *
207 * @param object $media_item - media post object.
208 * @return object a revision item
209 */
210 public static function get_snapshot( $media_item ) {
211 $current_file = get_attached_file( $media_item->ID );
212 $file_paths = pathinfo( $current_file );
213
214 $snapshot = array(
215 'date' => (string) WPCOM_JSON_API_Date::format_date( $media_item->post_modified_gmt, $media_item->post_modified ),
216 'URL' => (string) wp_get_attachment_url( $media_item->ID ),
217 'file' => (string) $file_paths['basename'],
218 'extension' => (string) $file_paths['extension'],
219 'mime_type' => (string) $media_item->post_mime_type,
220 'size' => (int) filesize( $current_file ),
221 );
222
223 return (object) $snapshot;
224 }
225
226 /**
227 * Add a new item into revision_history array.
228 *
229 * @param object $media_item - media post object.
230 * @param array|WP_Error $file - File data, or WP_Error on error.
231 * @param bool $has_original_media - condition is the original media has been already added.
232 * @return bool `true` if the item has been added. Otherwise `false`.
233 */
234 public static function register_revision( $media_item, $file, $has_original_media ) {
235 if ( is_wp_error( $file ) || ! $has_original_media ) {
236 return false;
237 }
238
239 add_post_meta( $media_item->ID, self::WP_REVISION_HISTORY, self::get_snapshot( $media_item ) );
240 }
241 /**
242 * Return the `revision_history` of the given media.
243 *
244 * @param int $media_id - media post ID.
245 * @return array `revision_history` array
246 */
247 public static function get_revision_history( $media_id ) {
248 return array_reverse( get_post_meta( $media_id, self::WP_REVISION_HISTORY ) );
249 }
250
251 /**
252 * Return the original media data.
253 *
254 * @param int $media_id Attachment ID.
255 */
256 public static function get_original_media( $media_id ) {
257 $original = get_post_meta( $media_id, self::WP_ORIGINAL_MEDIA, true );
258 $original = $original ? $original : array();
259 return $original;
260 }
261
262 /**
263 * Delete a file.
264 *
265 * @param string $pathname Path name.
266 */
267 public static function delete_file( $pathname ) {
268 if ( ! file_exists( $pathname ) || ! is_file( $pathname ) ) {
269 $dir = dirname( $pathname );
270 if ( is_dir( $dir ) ) {
271 // let's touch a fake file to try to `really` remove the media file.
272 touch( $pathname ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_touch
273 }
274 }
275
276 return wp_delete_file( $pathname );
277 }
278
279 /**
280 * Try to delete a file according to the dirname of
281 * the media attached file and the filename.
282 *
283 * @param int $media_id - media post ID.
284 * @param string $filename - basename of the file ( name-of-file.ext ).
285 *
286 * @return void
287 */
288 private static function delete_media_history_file( $media_id, $filename ) {
289 $attached_path = get_attached_file( $media_id );
290 $attached_parts = pathinfo( $attached_path );
291 $dirname = $attached_parts['dirname'];
292
293 $pathname = $dirname . '/' . $filename;
294
295 // remove thumbnails.
296 $metadata = wp_generate_attachment_metadata( $media_id, $pathname );
297
298 if ( isset( $metadata ) && isset( $metadata['sizes'] ) ) {
299 foreach ( $metadata['sizes'] as $properties ) {
300 self::delete_file( $dirname . '/' . $properties['file'] );
301 }
302 }
303
304 // remove primary file.
305 self::delete_file( $pathname );
306 }
307
308 /**
309 * Remove specific items from the `revision history` array
310 * depending on the given criteria: array(
311 * 'from' => (int) <from>,
312 * 'to' => (int) <to>,
313 * )
314 *
315 * Also, it removes the file defined in each item.
316 *
317 * @param int $media_id - media post ID.
318 * @param array $criteria - criteria to remove the items.
319 * @param array $revision_history - revision history array.
320 *
321 * @return array `revision_history` array updated.
322 */
323 public static function remove_items_from_revision_history( $media_id, $criteria, $revision_history ) {
324 if ( ! isset( $revision_history ) ) {
325 $revision_history = self::get_revision_history( $media_id );
326 }
327
328 $from = $criteria['from'];
329 $to = $criteria['to'] ? $criteria['to'] : ( $from + 1 );
330
331 for ( $i = $from; $i < $to; $i++ ) {
332 $removed_item = array_slice( $revision_history, $from, 1 );
333 if ( ! $removed_item ) {
334 break;
335 }
336
337 array_splice( $revision_history, $from, 1 );
338 self::delete_media_history_file( $media_id, $removed_item[0]->file );
339 }
340
341 // override all history items.
342 delete_post_meta( $media_id, self::WP_REVISION_HISTORY );
343 $revision_history = array_reverse( $revision_history );
344 foreach ( $revision_history as &$item ) {
345 add_post_meta( $media_id, self::WP_REVISION_HISTORY, $item );
346 }
347
348 return $revision_history;
349 }
350
351 /**
352 * Limit the number of items of the `revision_history` array.
353 * When the stack is overflowing the oldest item is remove from there (FIFO).
354 *
355 * @param int $media_id - media post ID.
356 * @param null|int $limit - maximum amount of items. 20 as default.
357 *
358 * @return array items removed from `revision_history`
359 */
360 public static function limit_revision_history( $media_id, $limit = null ) {
361 if ( $limit === null ) {
362 $limit = self::REVISION_HISTORY_MAXIMUM_AMOUNT;
363 }
364
365 $revision_history = self::get_revision_history( $media_id );
366
367 $total = count( $revision_history );
368
369 if ( $total < $limit ) {
370 return array();
371 }
372
373 self::remove_items_from_revision_history(
374 $media_id,
375 array(
376 'from' => $limit,
377 'to' => $total,
378 ),
379 $revision_history
380 );
381
382 return self::get_revision_history( $media_id );
383 }
384
385 /**
386 * Remove the original file and clean the post metadata.
387 *
388 * @param int $media_id - media post ID.
389 */
390 public static function clean_original_media( $media_id ) {
391 $original_file = self::get_original_media( $media_id );
392
393 if ( ! $original_file ) {
394 return null;
395 }
396
397 self::delete_media_history_file( $media_id, $original_file->file );
398 return delete_post_meta( $media_id, self::WP_ORIGINAL_MEDIA );
399 }
400
401 /**
402 * Clean `revision_history` of the given $media_id. it means:
403 * - remove all media files tied to the `revision_history` items.
404 * - clean `revision_history` meta data.
405 * - remove and clean the `original_media`
406 *
407 * @param int $media_id - media post ID.
408 *
409 * @return array results of removing these files
410 */
411 public static function clean_revision_history( $media_id ) {
412 self::clean_original_media( $media_id );
413
414 $revision_history = self::get_revision_history( $media_id );
415 $total = count( $revision_history );
416 $updated_history = array();
417
418 if ( $total < 1 ) {
419 return $updated_history;
420 }
421
422 $updated_history = self::remove_items_from_revision_history(
423 $media_id,
424 array(
425 'from' => 0,
426 'to' => $total,
427 ),
428 $revision_history
429 );
430
431 return $updated_history;
432 }
433
434 /**
435 * Edit media item process:
436 *
437 * - update attachment file
438 * - preserve original media file
439 * - trace revision history
440 *
441 * Note this does not support sideloads, only uploads.
442 *
443 * @param int $media_id - media post ID.
444 * @param array $file_array - Data derived from `$_FILES` for an uploaded file.
445 * @return WP_Post|WP_Error Updated media item or a WP_Error is something went wrong.
446 */
447 public static function edit_media_file( $media_id, $file_array ) {
448 $media_item = get_post( $media_id );
449 $has_original_media = self::get_original_media( $media_id );
450
451 if ( ! $has_original_media ) {
452
453 // The first time that the media is updated
454 // the original media is stored into the revision_history.
455 $snapshot = self::get_snapshot( $media_item );
456 add_post_meta( $media_id, self::WP_ORIGINAL_MEDIA, $snapshot, true );
457 }
458
459 // Save temporary file in the correct location.
460 $uploaded_file = self::save_temporary_file( $file_array, $media_id );
461
462 if ( is_wp_error( $uploaded_file ) ) {
463 return $uploaded_file;
464 }
465
466 // Revision_history control.
467 self::register_revision( $media_item, $uploaded_file, $has_original_media );
468
469 $uploaded_path = $uploaded_file['file'];
470 $udpated_mime_type = $uploaded_file['type'];
471 $was_updated = update_attached_file( $media_id, $uploaded_path );
472
473 if ( ! $was_updated ) {
474 return new WP_Error( 'update_error', 'Media update error' );
475 }
476
477 // Check maximum amount of revision_history before updating the attachment metadata.
478 self::limit_revision_history( $media_id );
479
480 $new_metadata = wp_generate_attachment_metadata( $media_id, $uploaded_path );
481 wp_update_attachment_metadata( $media_id, $new_metadata );
482
483 $edited_action = wp_update_post(
484 (object) array(
485 'ID' => $media_id,
486 'post_mime_type' => $udpated_mime_type,
487 ),
488 true
489 );
490
491 if ( is_wp_error( $edited_action ) ) {
492 return $edited_action;
493 }
494
495 return $media_item;
496 }
497 }
498
499 // phpcs:disable Universal.Files.SeparateFunctionsFromOO.Mixed -- TODO: Move these functions to some other file.
500
501 /**
502 * Clean revision history when the media item is deleted.
503 *
504 * @param int $media_id Attachment ID.
505 */
506 function jetpack_clean_revision_history( $media_id ) {
507 Jetpack_Media::clean_revision_history( $media_id );
508 }
509 add_action( 'delete_attachment', 'jetpack_clean_revision_history' );
510