Completed
Push — fusion-sync/enej/r215923-wpcom... ( a31b29...b311c1 )
by
unknown
08:53 queued 01:16
created

Jetpack_Media::clean_original_media()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 1
dl 0
loc 10
rs 9.9332
c 0
b 0
f 0
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 View Code Duplication
	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 View Code Duplication
	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.' );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_input'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
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 View Code Duplication
		if (
157
			! self::is_file_supported_for_sideloading( $tmp_filename ) &&
158
			! file_is_displayable_image( $tmp_filename )
159
		) {
160
			@unlink( $tmp_filename );
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
161
			return new WP_Error( 'invalid_input', 'Invalid file type.', 403 );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_input'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
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'] );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'upload_error'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
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 View Code Duplication
	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 ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $removed_item of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
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 ),
0 ignored issues
show
Documentation introduced by
array('from' => $limit, 'to' => $total) is of type array<string,?,{"from":"?","to":"integer"}>, but the function expects a object.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
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 ),
0 ignored issues
show
Documentation introduced by
array('from' => 0, 'to' => $total) is of type array<string,integer,{"f...teger","to":"integer"}>, but the function expects a object.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
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.
0 ignored issues
show
Documentation introduced by
The doc-type {Post|WP_Error} could not be parsed: Unknown type name "{Post" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
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 View Code Duplication
		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 );
0 ignored issues
show
Documentation introduced by
$uploaded_file is of type array|object<WP_Error>, but the function expects a object<file>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
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