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

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