PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 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 All 508 releases
← All changes | _inc/lib/class.media.php +509 -0 16.2-beta → 16.3 View file →
@@ -1,0 +1,509 @@
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, false ) );
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' );