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

505 lines 15.9 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 $finfo = new finfo( FILEINFO_MIME );
115 $mime = explode( '; ', $finfo->file( $file ) );
116 $type = $mime[0];
117
118 } elseif ( function_exists( 'mime_content_type' ) ) { // PHP 5.2
119 $type = mime_content_type( $file );
120
121 } else {
122 return false;
123 }
124
125 /**
126 * Filter the list of supported mime types for media sideloading.
127 *
128 * @since 4.0
129 *
130 * @module json-api
131 *
132 * @param array $supported_mime_types Array of the supported mime types for media sideloading.
133 */
134 $supported_mime_types = apply_filters( 'jetpack_supported_media_sideload_types', array(
135 'image/png',
136 'image/jpeg',
137 'image/gif',
138 'image/bmp',
139 'video/quicktime',
140 'video/mp4',
141 'video/mpeg',
142 'video/ogg',
143 'video/3gpp',
144 'video/3gpp2',
145 'video/h261',
146 'video/h262',
147 'video/h264',
148 'video/x-msvideo',
149 'video/x-ms-wmv',
150 'video/x-ms-asf',
151 ) );
152
153 // If the type returned was not an array as expected, then we know we don't have a match.
154 if ( ! is_array( $supported_mime_types ) ) {
155 return false;
156 }
157
158 return in_array( $type, $supported_mime_types );
159 }
160
161 /**
162 * Try to remove the temporal file from the given file array.
163 *
164 * @param array $file_array Array with data about the temporal file
165 * @return bool `true` if the file has been removed. `false` either the file doesn't exist or it couldn't be removed.
166 */
167 private static function remove_tmp_file( $file_array ) {
168 if ( ! file_exists ( $file_array['tmp_name'] ) ) {
169 return false;
170 }
171 return @unlink( $file_array['tmp_name'] );
172 }
173
174 /**
175 * Save the given temporal file considering file type,
176 * correct location according to the original file path, etc.
177 * The file type control is done through of `jetpack_supported_media_sideload_types` filter,
178 * which allows define to the users their own file types list.
179 *
180 * @param array $file_array file to save
181 * @param number $media_id
182 * @return array|WP_Error an array with information about the new file saved or a WP_Error is something went wrong.
183 */
184 public static function save_temporary_file( $file_array, $media_id ) {
185 $tmp_filename = $file_array['tmp_name'];
186
187 if ( ! file_exists( $tmp_filename ) ) {
188 return new WP_Error( 'invalid_input', 'No media provided in input.' );
189 }
190
191 // add additional mime_types through of the `jetpack_supported_media_sideload_types` filter
192 $mime_type_static_filter = array(
193 'Jetpack_Media',
194 'get_allowed_mime_types'
195 );
196
197 add_filter( 'jetpack_supported_media_sideload_types', $mime_type_static_filter );
198 if (
199 ! self::is_file_supported_for_sideloading( $tmp_filename ) &&
200 ! file_is_displayable_image( $tmp_filename )
201 ) {
202 @unlink( $tmp_filename );
203 return new WP_Error( 'invalid_input', 'Invalid file type.', 403 );
204 }
205 remove_filter( 'jetpack_supported_media_sideload_types', $mime_type_static_filter );
206
207 // generate a new file name
208 $tmp_new_filename = self::generate_new_filename( $media_id, $file_array[ 'name' ] );
209
210 // start to create the parameters to move the temporal file
211 $overrides = array( 'test_form' => false );
212
213 // get time according to the original filaname
214 $time = self::get_time_string_from_guid( $media_id );
215
216 $file_array['name'] = $tmp_new_filename;
217 $file = wp_handle_sideload( $file_array, $overrides, $time );
218
219 self::remove_tmp_file( $file_array );
220
221 if ( isset( $file['error'] ) ) {
222 return new WP_Error( 'upload_error', $file['error'] );
223 }
224
225 return $file;
226 }
227
228 /**
229 * Return an object with an snapshot of a revision item.
230 *
231 * @param object $media_item - media post object
232 * @return object a revision item
233 */
234 public static function get_snapshot( $media_item ) {
235 $current_file = get_attached_file( $media_item->ID );
236 $file_paths = pathinfo( $current_file );
237
238 $snapshot = array(
239 'date' => (string) WPCOM_JSON_API_Date::format_date( $media_item->post_modified_gmt, $media_item->post_modified ),
240 'URL' => (string) wp_get_attachment_url( $media_item->ID ),
241 'file' => (string) $file_paths['basename'],
242 'extension' => (string) $file_paths['extension'],
243 'mime_type' => (string) $media_item->post_mime_type,
244 'size' => (int) filesize( $current_file )
245 );
246
247 return (object) $snapshot;
248 }
249
250 /**
251 * Add a new item into revision_history array.
252 *
253 * @param object $media_item - media post object
254 * @param file $file - file recently added
255 * @param bool $has_original_media - condition is the original media has been already added
256 * @return bool `true` if the item has been added. Otherwise `false`.
257 */
258 public static function register_revision( $media_item, $file, $has_original_media ) {
259 if ( is_wp_error( $file ) || ! $has_original_media ) {
260 return false;
261 }
262
263 add_post_meta( $media_item->ID, self::$WP_REVISION_HISTORY, self::get_snapshot( $media_item ) );
264 }
265 /**
266 * Return the `revision_history` of the given media.
267 *
268 * @param number $media_id - media post ID
269 * @return array `revision_history` array
270 */
271 public static function get_revision_history( $media_id ) {
272 return array_reverse( get_post_meta( $media_id, self::$WP_REVISION_HISTORY ) );
273 }
274
275 /**
276 * Return the original media data
277 */
278 public static function get_original_media( $media_id ) {
279 $original = get_post_meta( $media_id, self::$WP_ORIGINAL_MEDIA, true );
280 $original = $original ? $original : array();
281 return $original;
282 }
283
284 public static function delete_file( $pathname ) {
285 if ( ! file_exists( $pathname ) || ! is_file( $pathname ) ) {
286 // let's touch a fake file to try to `really` remove the media file
287 touch( $pathname );
288 }
289
290 return wp_delete_file( $pathname );
291 }
292
293 /**
294 * Try to delete a file according to the dirname of
295 * the media attached file and the filename.
296 *
297 * @param number $media_id - media post ID
298 * @param string $filename - basename of the file ( name-of-file.ext )
299 * @return bool `true` is the file has been removed, `false` if not.
300 */
301 private static function delete_media_history_file( $media_id, $filename ) {
302 $attached_path = get_attached_file( $media_id );
303 $attached_parts = pathinfo( $attached_path );
304 $dirname = $attached_parts['dirname'];
305
306 $pathname = $dirname . '/' . $filename;
307
308 // remove thumbnails
309 $metadata = wp_generate_attachment_metadata( $media_id, $pathname );
310
311 if ( isset( $metadata ) && isset( $metadata['sizes'] ) ) {
312 foreach ( $metadata['sizes'] as $size => $properties ) {
313 self::delete_file( $dirname . '/' . $properties['file'] );
314 }
315 }
316
317 // remove primary file
318 self::delete_file( $pathname );
319 }
320
321 /**
322 * Remove specific items from the `revision history` array
323 * depending on the given criteria: array(
324 * 'from' => (int) <from>,
325 * 'to' => (int) <to>,
326 * )
327 *
328 * Also, it removes the file defined in each item.
329 *
330 * @param number $media_id - media post ID
331 * @param object $criteria - criteria to remove the items
332 * @param array [$revision_history] - revision history array
333 * @return array `revision_history` array updated.
334 */
335 public static function remove_items_from_revision_history( $media_id, $criteria = array(), $revision_history ) {
336 if ( ! isset ( $revision_history ) ) {
337 $revision_history = self::get_revision_history( $media_id );
338 }
339
340 $from = $criteria['from'];
341 $to = $criteria['to'] ? $criteria['to'] : ( $from + 1 );
342
343 for ( $i = $from; $i < $to; $i++ ) {
344 $removed_item = array_slice( $revision_history, $from, 1 );
345 if ( ! $removed_item ) {
346 break;
347 }
348
349 array_splice( $revision_history, $from, 1 );
350 self::delete_media_history_file( $media_id, $removed_item[0]->file );
351 }
352
353 // override all history items
354 delete_post_meta( $media_id, self::$WP_REVISION_HISTORY );
355 $revision_history = array_reverse( $revision_history );
356 foreach ( $revision_history as &$item ) {
357 add_post_meta( $media_id, self::$WP_REVISION_HISTORY, $item );
358 }
359
360 return $revision_history;
361 }
362
363 /**
364 * Limit the number of items of the `revision_history` array.
365 * When the stack is overflowing the oldest item is remove from there (FIFO).
366 *
367 * @param number $media_id - media post ID
368 * @param number [$limit] - maximun amount of items. 20 as default.
369 * @return array items removed from `revision_history`
370 */
371 public static function limit_revision_history( $media_id, $limit = null) {
372 if ( is_null( $limit ) ) {
373 $limit = self::$REVISION_HISTORY_MAXIMUM_AMOUNT;
374 }
375
376 $revision_history = self::get_revision_history( $media_id );
377
378 $total = count( $revision_history );
379
380 if ( $total < $limit ) {
381 return array();
382 }
383
384 self::remove_items_from_revision_history(
385 $media_id,
386 array( 'from' => $limit, 'to' => $total ),
387 $revision_history
388 );
389
390 return self::get_revision_history( $media_id );
391 }
392
393 /**
394 * Remove the original file and clean the post metadata.
395 *
396 * @param number $media_id - media post ID
397 */
398 public static function clean_original_media( $media_id ) {
399 $original_file = self::get_original_media( $media_id );
400
401 if ( ! $original_file ) {
402 return null;
403 }
404
405 self::delete_media_history_file( $media_id, $original_file->file );
406 return delete_post_meta( $media_id, self::$WP_ORIGINAL_MEDIA );
407 }
408
409 /**
410 * Clean `revision_history` of the given $media_id. it means:
411 * - remove all media files tied to the `revision_history` items.
412 * - clean `revision_history` meta data.
413 * - remove and clean the `original_media`
414 *
415 * @param number $media_id - media post ID
416 * @return array results of removing these files
417 */
418 public static function clean_revision_history( $media_id ) {
419 self::clean_original_media( $media_id );
420
421 $revision_history = self::get_revision_history( $media_id );
422 $total = count( $revision_history );
423 $updated_history = array();
424
425 if ( $total < 1 ) {
426 return $updated_history;
427 }
428
429 $updated_history = self::remove_items_from_revision_history(
430 $media_id,
431 array( 'from' => 0, 'to' => $total ),
432 $revision_history
433 );
434
435 return $updated_history;
436 }
437
438 /**
439 * Edit media item process:
440 *
441 * - update attachment file
442 * - preserve original media file
443 * - trace revision history
444 *
445 * @param number $media_id - media post ID
446 * @param array $file_array - temporal file
447 * @return {Post|WP_Error} Updated media item or a WP_Error is something went wrong.
448 */
449 public static function edit_media_file( $media_id, $file_array ) {
450 $media_item = get_post( $media_id );
451 $has_original_media = self::get_original_media( $media_id );
452
453 if ( ! $has_original_media ) {
454 // The first time that the media is updated
455 // the original media is stored into the revision_history
456 $snapshot = self::get_snapshot( $media_item );
457 add_post_meta( $media_id, self::$WP_ORIGINAL_MEDIA, $snapshot, true );
458 }
459
460 // save temporary file in the correct location
461 $uploaded_file = self::save_temporary_file( $file_array, $media_id );
462
463 if ( is_wp_error( $uploaded_file ) ) {
464 self::remove_tmp_file( $file_array );
465 return $uploaded_file;
466 }
467
468 // revision_history control
469 self::register_revision( $media_item, $uploaded_file, $has_original_media );
470
471 $uploaded_path = $uploaded_file['file'];
472 $udpated_mime_type = $uploaded_file['type'];
473 $was_updated = update_attached_file( $media_id, $uploaded_path );
474
475 if ( ! $was_updated ) {
476 return WP_Error( 'update_error', 'Media update error' );
477 }
478
479 $new_metadata = wp_generate_attachment_metadata( $media_id, $uploaded_path );
480 wp_update_attachment_metadata( $media_id, $new_metadata );
481
482 // check maximum amount of revision_history
483 self::limit_revision_history( $media_id );
484
485 $edited_action = wp_update_post( (object) array(
486 'ID' => $media_id,
487 'post_mime_type' => $udpated_mime_type
488 ), true );
489
490 if ( is_wp_error( $edited_action ) ) {
491 return $edited_action;
492 }
493
494 return $media_item;
495 }
496 }
497
498 // hook: clean revision history when the media item is deleted
499 function clean_revision_history( $media_id ) {
500 Jetpack_Media::clean_revision_history( $media_id );
501 };
502
503 add_action( 'delete_attachment', 'clean_revision_history' );
504
505