PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 7.7
Jetpack – WP Security, Backup, Speed, & Growth v7.7
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 7.7, at _inc/lib/class.media.php

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