Completed
Push — try/composer ( 986589...72d7ac )
by
unknown
08:03
created

class.media.php ➔ clean_revision_history()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Class to handle different actions related to media.
5
 */
6
class Jetpack_Media {
7
	public static $WP_ORIGINAL_MEDIA = '_wp_original_post_media';
8
	public static $WP_REVISION_HISTORY = '_wp_revision_history';
9
	public static $REVISION_HISTORY_MAXIMUM_AMOUNT = 0;
10
	public static $WP_ATTACHMENT_IMAGE_ALT = '_wp_attachment_image_alt';
11
12
	/**
13
	 * Generate a filename in function of the original filename of the media.
14
	 * The returned name has the `{basename}-{hash}-{random-number}.{ext}` shape.
15
	 * The hash is built according to the filename trying to avoid name collisions
16
	 * with other media files.
17
	 *
18
	 * @param  number $media_id - media post ID
19
	 * @param  string $new_filename - the new filename
20
	 * @return string A random filename.
21
	 */
22
	public static function generate_new_filename( $media_id, $new_filename ) {
23
		// get the right filename extension
24
		$new_filename_paths = pathinfo( $new_filename );
25
		$new_file_ext = $new_filename_paths['extension'];
26
27
		// take out filename from the original file or from the current attachment
28
		$original_media = (array) self::get_original_media( $media_id );
29
30
		if ( ! empty( $original_media ) ) {
31
			$original_file_parts = pathinfo( $original_media['file'] );
32
			$filename_base = $original_file_parts['filename'];
33
		} else {
34
			$current_file = get_attached_file( $media_id );
35
			$current_file_parts = pathinfo( $current_file );
36
			$current_file_ext = $current_file_parts['filename'];
37
			$filename_base = $current_file_parts['filename'];
38
		}
39
40
		// add unique seed based on the filename
41
		$filename_base .=  '-' . crc32( $filename_base ) . '-';
42
43
		$number_suffix = time() . rand( 100, 999 );
44
45
		do {
46
			$filename = $filename_base;
47
			$filename .= $number_suffix;
48
			$file_ext = $new_file_ext ? $new_file_ext : $current_file_ext;
0 ignored issues
show
Bug introduced by
The variable $current_file_ext does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
49
50
			$new_filename = "{$filename}.{$file_ext}";
51
			$new_path = "{$current_file_parts['dirname']}/$new_filename";
0 ignored issues
show
Bug introduced by
The variable $current_file_parts does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
52
			$number_suffix++;
53
		} while( file_exists( $new_path ) );
54
55
		return $new_filename;
56
	}
57
58
	/**
59
	 * File urls use the post (image item) date to generate a folder path.
60
	 * Post dates can change, so we use the original date used in the `guid`
61
	 * url so edits can remain in the same folder. In the following function
62
	 * we capture a string in the format of `YYYY/MM` from the guid.
63
	 *
64
	 * For example with a guid of
65
	 * "http://test.files.wordpress.com/2016/10/test.png" the resulting string
66
	 * would be: "2016/10"
67
	 *
68
	 * @param  number $media_id
69
	 * @return string
70
	 */
71 View Code Duplication
	private function get_time_string_from_guid( $media_id ) {
72
		$time = date( "Y/m", strtotime( current_time( 'mysql' ) ) );
73
74
		if ( $media = get_post( $media_id ) ) {
75
			$pattern = '/\/(\d{4}\/\d{2})\//';
76
			preg_match( $pattern, $media->guid, $matches );
77
			if ( count( $matches ) > 1 ) {
78
				$time = $matches[1];
79
			}
80
		}
81
		return $time;
82
	}
83
84
	/**
85
	 * Return an array of allowed mime_type items used to upload a media file.
86
	 *
87
	 * @return array mime_type array
88
	 */
89 View Code Duplication
	static function get_allowed_mime_types( $default_mime_types ) {
90
		return array_unique( array_merge( $default_mime_types, array(
91
			'application/msword',                                                         // .doc
92
			'application/vnd.ms-powerpoint',                                              // .ppt, .pps
93
			'application/vnd.ms-excel',                                                   // .xls
94
			'application/vnd.openxmlformats-officedocument.presentationml.presentation',  // .pptx
95
			'application/vnd.openxmlformats-officedocument.presentationml.slideshow',     // .ppsx
96
			'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',          // .xlsx
97
			'application/vnd.openxmlformats-officedocument.wordprocessingml.document',    // .docx
98
			'application/vnd.oasis.opendocument.text',                                    // .odt
99
			'application/pdf',                                                            // .pdf
100
		) ) );
101
	}
102
103
	/**
104
	 * Checks that the mime type of the file
105
	 * is among those in a filterable list of mime types.
106
	 *
107
	 * @param  string $file Path to file to get its mime type.
108
	 * @return bool
109
	 */
110 View Code Duplication
	protected static function is_file_supported_for_sideloading( $file ) {
111
		if ( class_exists( 'finfo' ) ) { // php 5.3+
112
			// phpcs:ignore PHPCompatibility.PHP.NewClasses.finfoFound
113
			$finfo = new finfo( FILEINFO_MIME );
114
			$mime = explode( '; ', $finfo->file( $file ) );
115
			$type = $mime[0];
116
117
		} elseif ( function_exists( 'mime_content_type' ) ) { // PHP 5.2
118
			$type = mime_content_type( $file );
119
120
		} else {
121
			return false;
122
		}
123
124
		/**
125
		 * Filter the list of supported mime types for media sideloading.
126
		 *
127
		 * @since 4.0
128
		 *
129
		 * @module json-api
130
		 *
131
		 * @param array $supported_mime_types Array of the supported mime types for media sideloading.
132
		 */
133
		$supported_mime_types = apply_filters( 'jetpack_supported_media_sideload_types', array(
134
			'image/png',
135
			'image/jpeg',
136
			'image/gif',
137
			'image/bmp',
138
			'video/quicktime',
139
			'video/mp4',
140
			'video/mpeg',
141
			'video/ogg',
142
			'video/3gpp',
143
			'video/3gpp2',
144
			'video/h261',
145
			'video/h262',
146
			'video/h264',
147
			'video/x-msvideo',
148
			'video/x-ms-wmv',
149
			'video/x-ms-asf',
150
		) );
151
152
		// If the type returned was not an array as expected, then we know we don't have a match.
153
		if ( ! is_array( $supported_mime_types ) ) {
154
			return false;
155
		}
156
157
		return in_array( $type, $supported_mime_types );
158
	}
159
160
	/**
161
	 * Try to remove the temporal file from the given file array.
162
	 *
163
	 * @param  array $file_array Array with data about the temporal file
164
	 * @return bool `true` if the file has been removed. `false` either the file doesn't exist or it couldn't be removed.
165
	 */
166
	private static function remove_tmp_file( $file_array ) {
167
		if ( ! file_exists ( $file_array['tmp_name'] ) ) {
168
			return false;
169
		}
170
		return @unlink( $file_array['tmp_name'] );
171
	}
172
173
	/**
174
	 * Save the given temporal file considering file type,
175
	 * correct location according to the original file path, etc.
176
	 * The file type control is done through of `jetpack_supported_media_sideload_types` filter,
177
	 * which allows define to the users their own file types list.
178
	 *
179
	 * @param  array  $file_array file to save
180
	 * @param  number $media_id
181
	 * @return array|WP_Error an array with information about the new file saved or a WP_Error is something went wrong.
182
	 */
183
	public static function save_temporary_file( $file_array, $media_id ) {
184
		$tmp_filename = $file_array['tmp_name'];
185
186
		if ( ! file_exists( $tmp_filename ) ) {
187
			return new WP_Error( 'invalid_input', 'No media provided in input.' );
188
		}
189
190
		// add additional mime_types through of the `jetpack_supported_media_sideload_types` filter
191
		$mime_type_static_filter = array(
192
			'Jetpack_Media',
193
			'get_allowed_mime_types'
194
		);
195
196
		add_filter( 'jetpack_supported_media_sideload_types', $mime_type_static_filter );
197 View Code Duplication
		if (
198
			! self::is_file_supported_for_sideloading( $tmp_filename ) &&
199
			! file_is_displayable_image( $tmp_filename )
200
		) {
201
			@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...
202
			return new WP_Error( 'invalid_input', 'Invalid file type.', 403 );
203
		}
204
		remove_filter( 'jetpack_supported_media_sideload_types', $mime_type_static_filter );
205
206
		// generate a new file name
207
		$tmp_new_filename = self::generate_new_filename( $media_id, $file_array[ 'name' ] );
208
209
		// start to create the parameters to move the temporal file
210
		$overrides = array( 'test_form' => false );
211
212
		// get time according to the original filaname
213
		$time = self::get_time_string_from_guid( $media_id );
214
215
		$file_array['name'] = $tmp_new_filename;
216
		$file = wp_handle_sideload( $file_array, $overrides, $time );
217
218
		self::remove_tmp_file( $file_array );
219
220
		if ( isset( $file['error'] ) ) {
221
			return new WP_Error( 'upload_error', $file['error'] );
222
		}
223
224
		return $file;
225
	}
226
227
	/**
228
	 * Return an object with an snapshot of a revision item.
229
	 *
230
	 * @param  object $media_item - media post object
231
	 * @return object a revision item
232
	 */
233 View Code Duplication
	public static function get_snapshot( $media_item ) {
234
		$current_file = get_attached_file( $media_item->ID );
235
		$file_paths = pathinfo( $current_file );
236
237
		$snapshot = array(
238
			'date'             => (string) WPCOM_JSON_API_Date::format_date( $media_item->post_modified_gmt, $media_item->post_modified ),
239
			'URL'              => (string) wp_get_attachment_url( $media_item->ID ),
240
			'file'             => (string) $file_paths['basename'],
241
			'extension'        => (string) $file_paths['extension'],
242
			'mime_type'        => (string) $media_item->post_mime_type,
243
			'size'             => (int) filesize( $current_file )
244
		);
245
246
		return (object) $snapshot;
247
	}
248
249
	/**
250
	 * Add a new item into revision_history array.
251
	 *
252
	 * @param  object $media_item - media post object
253
	 * @param  file $file - file recently added
254
	 * @param  bool $has_original_media - condition is the original media has been already added
255
	 * @return bool `true` if the item has been added. Otherwise `false`.
256
	 */
257
	public static function register_revision( $media_item, $file, $has_original_media ) {
258
		if ( is_wp_error( $file ) || ! $has_original_media ) {
259
			return false;
260
		}
261
262
		add_post_meta( $media_item->ID, self::$WP_REVISION_HISTORY, self::get_snapshot( $media_item ) );
263
	}
264
	/**
265
	 * Return the `revision_history` of the given media.
266
	 *
267
	 * @param  number $media_id - media post ID
268
	 * @return array `revision_history` array
269
	 */
270
	public static function get_revision_history( $media_id ) {
271
		return array_reverse( get_post_meta( $media_id, self::$WP_REVISION_HISTORY ) );
272
	}
273
274
	/**
275
	 * Return the original media data
276
	 */
277
	public static function get_original_media( $media_id ) {
278
		$original = get_post_meta( $media_id, self::$WP_ORIGINAL_MEDIA, true );
279
		$original = $original ? $original : array();
280
		return $original;
281
	}
282
283
	public static function delete_file( $pathname ) {
284
		if ( ! file_exists( $pathname ) || ! is_file( $pathname ) ) {
285
			// let's touch a fake file to try to `really` remove the media file
286
			touch( $pathname );
287
		}
288
289
		return wp_delete_file( $pathname );
290
	}
291
292
	/**
293
	 * Try to delete a file according to the dirname of
294
	 * the media attached file and the filename.
295
	 *
296
	 * @param  number $media_id - media post ID
297
	 * @param  string $filename - basename of the file ( name-of-file.ext )
298
	 * @return bool `true` is the file has been removed, `false` if not.
299
	 */
300
	private static function delete_media_history_file( $media_id, $filename ) {
301
		$attached_path = get_attached_file( $media_id );
302
		$attached_parts = pathinfo( $attached_path );
303
		$dirname = $attached_parts['dirname'];
304
305
		$pathname = $dirname . '/' . $filename;
306
307
		// remove thumbnails
308
		$metadata = wp_generate_attachment_metadata( $media_id, $pathname );
309
310
		if ( isset( $metadata ) && isset( $metadata['sizes'] ) ) {
311
			foreach ( $metadata['sizes'] as $size => $properties ) {
312
				self::delete_file( $dirname . '/' . $properties['file'] );
313
			}
314
		}
315
316
		// remove primary file
317
		self::delete_file( $pathname );
318
	}
319
320
	/**
321
	 * Remove specific items from the `revision history` array
322
	 * depending on the given criteria: array(
323
	 *   'from' => (int) <from>,
324
	 *   'to' =>   (int) <to>,
325
	 * )
326
	 *
327
	 * Also, it removes the file defined in each item.
328
	 *
329
	 * @param  number $media_id - media post ID
330
	 * @param  object $criteria - criteria to remove the items
0 ignored issues
show
Documentation introduced by
Should the type for parameter $criteria not be object|array?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
331
	 * @param  array [$revision_history] - revision history array
332
	 * @return array `revision_history` array updated.
333
	 */
334
	public static function remove_items_from_revision_history( $media_id, $criteria = array(), $revision_history ) {
335
		if ( ! isset ( $revision_history ) ) {
336
			$revision_history = self::get_revision_history( $media_id );
337
		}
338
339
		$from = $criteria['from'];
340
		$to = $criteria['to'] ? $criteria['to'] : ( $from + 1 );
341
342
		for ( $i = $from; $i < $to; $i++ ) {
343
			$removed_item = array_slice( $revision_history, $from, 1 );
344
			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...
345
				break;
346
			}
347
348
			array_splice( $revision_history, $from, 1 );
349
			self::delete_media_history_file( $media_id, $removed_item[0]->file );
350
		}
351
352
		// override all history items
353
		delete_post_meta( $media_id, self::$WP_REVISION_HISTORY );
354
		$revision_history = array_reverse( $revision_history );
355
		foreach ( $revision_history as &$item ) {
356
			add_post_meta( $media_id, self::$WP_REVISION_HISTORY, $item );
357
		}
358
359
		return $revision_history;
360
	}
361
362
	/**
363
	 * Limit the number of items of the `revision_history` array.
364
	 * When the stack is overflowing the oldest item is remove from there (FIFO).
365
	 *
366
	 * @param  number $media_id - media post ID
367
	 * @param  number [$limit] - maximun amount of items. 20 as default.
368
	 * @return array items removed from `revision_history`
369
	 */
370
	public static function limit_revision_history( $media_id, $limit = null) {
371
		if ( is_null( $limit ) ) {
372
			$limit = self::$REVISION_HISTORY_MAXIMUM_AMOUNT;
373
		}
374
375
		$revision_history = self::get_revision_history( $media_id );
376
377
		$total = count( $revision_history );
378
379
		if ( $total < $limit ) {
380
			return array();
381
		}
382
383
		self::remove_items_from_revision_history(
384
			$media_id,
385
			array( 'from' => $limit, 'to' => $total ),
386
			$revision_history
387
		);
388
389
		return self::get_revision_history( $media_id );
390
	}
391
392
	/**
393
	 * Remove the original file and clean the post metadata.
394
	 *
395
	 * @param  number $media_id - media post ID
396
	 */
397
	public static function clean_original_media( $media_id ) {
398
		$original_file = self::get_original_media( $media_id );
399
400
		if ( ! $original_file ) {
401
			return null;
402
		}
403
404
		self::delete_media_history_file( $media_id, $original_file->file );
405
		return delete_post_meta( $media_id, self::$WP_ORIGINAL_MEDIA );
406
	}
407
408
	/**
409
	 * Clean `revision_history` of the given $media_id. it means:
410
	 *   - remove all media files tied to the `revision_history` items.
411
	 *   - clean `revision_history` meta data.
412
	 *   - remove and clean the `original_media`
413
	 *
414
	 * @param  number $media_id - media post ID
415
	 * @return array results of removing these files
416
	 */
417
	public static function clean_revision_history( $media_id ) {
418
		self::clean_original_media( $media_id );
419
420
		$revision_history = self::get_revision_history( $media_id );
421
		$total = count( $revision_history );
422
		$updated_history = array();
423
424
		if ( $total < 1 ) {
425
			return $updated_history;
426
		}
427
428
		$updated_history = self::remove_items_from_revision_history(
429
			$media_id,
430
			array( 'from' => 0, 'to' => $total ),
431
			$revision_history
432
		);
433
434
		return $updated_history;
435
	}
436
437
	/**
438
	 * Edit media item process:
439
	 *
440
	 * - update attachment file
441
	 * - preserve original media file
442
	 * - trace revision history
443
	 *
444
	 * @param  number $media_id - media post ID
445
	 * @param  array $file_array - temporal file
446
	 * @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...
447
	 */
448
	public static function edit_media_file( $media_id, $file_array ) {
449
		$media_item = get_post( $media_id );
450
		$has_original_media = self::get_original_media( $media_id );
451
452 View Code Duplication
		if ( ! $has_original_media ) {
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
			self::remove_tmp_file( $file_array );
464
			return $uploaded_file;
465
		}
466
467
		// revision_history control
468
		self::register_revision( $media_item, $uploaded_file, $has_original_media );
0 ignored issues
show
Bug introduced by
It seems like $uploaded_file defined by self::save_temporary_file($file_array, $media_id) on line 460 can also be of type array; however, Jetpack_Media::register_revision() does only seem to accept object<file>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
469
470
		$uploaded_path = $uploaded_file['file'];
471
		$udpated_mime_type = $uploaded_file['type'];
472
		$was_updated = update_attached_file( $media_id, $uploaded_path );
473
474
		if ( ! $was_updated ) {
475
			return WP_Error( 'update_error', 'Media update error' );
476
		}
477
478
		$new_metadata = wp_generate_attachment_metadata( $media_id, $uploaded_path );
479
		wp_update_attachment_metadata( $media_id, $new_metadata );
480
481
		// check maximum amount of revision_history
482
		self::limit_revision_history( $media_id );
483
484
		$edited_action = wp_update_post( (object) array(
485
			'ID'              => $media_id,
486
			'post_mime_type'  => $udpated_mime_type
487
		), true );
488
489
		if ( is_wp_error( $edited_action ) ) {
490
			return $edited_action;
491
		}
492
493
		return $media_item;
494
	}
495
}
496
497
// hook: clean revision history when the media item is deleted
498
function clean_revision_history( $media_id ) {
499
	Jetpack_Media::clean_revision_history( $media_id );
500
};
501
502
add_action( 'delete_attachment', 'clean_revision_history' );
503
504