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

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