Completed
Push — add/masterbar-module ( 0660ea...2b5322 )
by
unknown
12:15 queued 02:52
created

Jetpack_Media::generate_new_filename()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 35
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 23
nc 4
nop 2
dl 0
loc 35
rs 8.5806
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
		// 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;
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...
51
52
			$new_filename = "{$filename}.{$file_ext}";
53
			$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...
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 View Code Duplication
	protected static function is_file_supported_for_sideloading( $file ) {
113
		if ( class_exists( 'finfo' ) ) { // php 5.3+
114
			$finfo = new finfo( FILEINFO_MIME );
115
			$mime = explode( '; ', $finfo->file( $file ) );
116
			$type = $mime[0];
117
118
		} elseif ( function_exists( 'mime_content_type' ) ) { // PHP 5.2
119
			$type = mime_content_type( $file );
120
121
		} else {
122
			return false;
123
		}
124
125
		/**
126
		 * Filter the list of supported mime types for media sideloading.
127
		 *
128
		 * @since 4.0
129
		 *
130
		 * @module json-api
131
		 *
132
		 * @param array $supported_mime_types Array of the supported mime types for media sideloading.
133
		 */
134
		$supported_mime_types = apply_filters( 'jetpack_supported_media_sideload_types', array(
135
			'image/png',
136
			'image/jpeg',
137
			'image/gif',
138
			'image/bmp',
139
			'video/quicktime',
140
			'video/mp4',
141
			'video/mpeg',
142
			'video/ogg',
143
			'video/3gpp',
144
			'video/3gpp2',
145
			'video/h261',
146
			'video/h262',
147
			'video/h264',
148
			'video/x-msvideo',
149
			'video/x-ms-wmv',
150
			'video/x-ms-asf',
151
		) );
152
153
		// If the type returned was not an array as expected, then we know we don't have a match.
154
		if ( ! is_array( $supported_mime_types ) ) {
155
			return false;
156
		}
157
158
		return in_array( $type, $supported_mime_types );
159
	}
160
161
	/**
162
	 * Try to remove the temporal file from the given file array.
163
	 * 	
164
	 * @param  array $file_array Array with data about the temporal file
165
	 * @return bool `true` if the file has been removed. `false` either the file doesn't exist or it couldn't be removed.
166
	 */
167
	private static function remove_tmp_file( $file_array ) {
168
		if ( ! file_exists ( $file_array['tmp_name'] ) ) {
169
			return false;
170
		}
171
		return @unlink( $file_array['tmp_name'] );
172
	}
173
174
	/**
175
	 * Save the given temporal file considering file type, 
176
	 * correct location according to the original file path, etc.
177
	 * The file type control is done through of `jetpack_supported_media_sideload_types` filter,
178
	 * which allows define to the users their own file types list.
179
	 * 
180
	 * @param  array  $file_array file to save
181
	 * @param  number $media_id
182
	 * @return array|WP_Error an array with information about the new file saved or a WP_Error is something went wrong.
183
	 */
184
	public static function save_temporary_file( $file_array, $media_id ) {
185
		$tmp_filename = $file_array['tmp_name'];
186
187
		if ( ! file_exists( $tmp_filename ) ) {
188
			return new WP_Error( 'invalid_input', 'No media provided in input.' );
189
		}
190
191
		// add additional mime_types through of the `jetpack_supported_media_sideload_types` filter
192
		$mime_type_static_filter = array(
193
			'Jetpack_Media',
194
			'get_allowed_mime_types'
195
		);
196
197
		add_filter( 'jetpack_supported_media_sideload_types', $mime_type_static_filter );
198
		if (
199
			! self::is_file_supported_for_sideloading( $tmp_filename ) &&
200
			! file_is_displayable_image( $tmp_filename )
201
		) {
202
			@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...
203
			return new WP_Error( 'invalid_input', 'Invalid file type.', 403 );
204
		}
205
		remove_filter( 'jetpack_supported_media_sideload_types', $mime_type_static_filter );
206
207
		// generate a new file name
208
		$tmp_new_filename = self::generate_new_filename( $media_id, $file_array[ 'name' ] );
209
210
		// start to create the parameters to move the temporal file
211
		$overrides = array( 'test_form' => false );
212
213
		// get time according to the original filaname
214
		$time = self::get_time_string_from_guid( $media_id );
215
216
		$file_array['name'] = $tmp_new_filename;
217
		$file = wp_handle_sideload( $file_array, $overrides, $time );
218
219
		self::remove_tmp_file( $file_array );
220
221
		if ( isset( $file['error'] ) ) {
222
			return new WP_Error( 'upload_error', $file['error'] );
223
		}
224
225
		return $file;
226
	}
227
228
	/**
229
	 * Return an object with an snapshot of a revision item.
230
	 * 
231
	 * @param  object $media_item - media post object
232
	 * @return object a revision item 
233
	 */
234
	public static function get_snapshot( $media_item ) {
235
		$current_file = get_attached_file( $media_item->ID );
236
		$file_paths = pathinfo( $current_file );
237
238
		$snapshot = array(
239
			'date'             => (string) WPCOM_JSON_API_Date::format_date( $media_item->post_modified_gmt, $media_item->post_modified ),
240
			'URL'              => (string) wp_get_attachment_url( $media_item->ID ),
241
			'file'             => (string) $file_paths['basename'],
242
			'extension'        => (string) $file_paths['extension'],
243
			'mime_type'        => (string) $media_item->post_mime_type,
244
			'size'             => (int) filesize( $current_file ) 
245
		);
246
247
		return (object) $snapshot;
248
	}
249
250
	/**
251
	 * Add a new item into revision_history array.
252
	 * 
253
	 * @param  object $media_item - media post object
254
	 * @param  file $file - file recently added
255
	 * @param  bool $has_original_media - condition is the original media has been already added
256
	 * @return bool `true` if the item has been added. Otherwise `false`.
257
	 */
258
	public static function register_revision( $media_item, $file, $has_original_media ) {
259
		if ( is_wp_error( $file ) || ! $has_original_media ) {
260
			return false;
261
		}
262
263
		add_post_meta( $media_item->ID, self::$WP_REVISION_HISTORY, self::get_snapshot( $media_item ) );
264
	}
265
	/**
266
	 * Return the `revision_history` of the given media.
267
	 * 
268
	 * @param  number $media_id - media post ID
269
	 * @return array `revision_history` array
270
	 */
271
	public static function get_revision_history( $media_id ) {
272
		return array_reverse( get_post_meta( $media_id, self::$WP_REVISION_HISTORY ) );
273
	}
274
275
	/**
276
	 * Return the original media data
277
	 */
278
	public static function get_original_media( $media_id ) {
279
		$original = get_post_meta( $media_id, self::$WP_ORIGINAL_MEDIA, true );
280
		$original = $original ? $original : array();
281
		return $original;
282
	}
283
284
	public static function delete_file( $pathname ) {
285
		if ( ! file_exists( $pathname ) || ! is_file( $pathname ) ) {
286
			// let's touch a fake file to try to `really` remove the media file
287
			touch( $pathname );
288
		}
289
290
		return wp_delete_file( $pathname );
291
	}
292
293
	/**
294
	 * Try to delete a file according to the dirname of
295
	 * the media attached file and the filename.
296
	 * 
297
	 * @param  number $media_id - media post ID
298
	 * @param  string $filename - basename of the file ( name-of-file.ext )
299
	 * @return bool `true` is the file has been removed, `false` if not.
300
	 */
301
	private static function delete_media_history_file( $media_id, $filename ) {
302
		$attached_path = get_attached_file( $media_id );
303
		$attached_parts = pathinfo( $attached_path );
304
		$dirname = $attached_parts['dirname'];
305
306
		$pathname = $dirname . '/' . $filename;
307
308
		// remove thumbnails
309
		$metadata = wp_generate_attachment_metadata( $media_id, $pathname );
310
311
		if ( isset( $metadata ) && isset( $metadata['sizes'] ) ) {
312
			foreach ( $metadata['sizes'] as $size => $properties ) {
313
				self::delete_file( $dirname . '/' . $properties['file'] );
314
			}
315
		}
316
317
		// remove primary file
318
		self::delete_file( $pathname );
319
	}
320
321
	/**
322
	 * Remove specific items from the `revision history` array
323
	 * depending on the given criteria: array(
324
	 *   'from' => (int) <from>,
325
	 *   'to' =>   (int) <to>,
326
	 * )
327
	 * 
328
	 * Also, it removes the file defined in each item.
329
	 *
330
	 * @param  number $media_id - media post ID
331
	 * @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...
332
	 * @param  array [$revision_history] - revision history array
333
	 * @return array `revision_history` array updated.
334
	 */
335
	public static function remove_items_from_revision_history( $media_id, $criteria = array(), $revision_history ) {
0 ignored issues
show
Coding Style introduced by
Parameters which have default values should be placed at the end.

If you place a parameter with a default value before a parameter with a default value, the default value of the first parameter will never be used as it will always need to be passed anyway:

// $a must always be passed; it's default value is never used.
function someFunction($a = 5, $b) { }
Loading history...
336
		if ( ! isset ( $revision_history ) ) {
337
			$revision_history = self::get_revision_history( $media_id );
338
		}
339
340
		$from = $criteria['from'];
341
		$to = $criteria['to'] ? $criteria['to'] : ( $from + 1 );
342
343
		for ( $i = $from; $i < $to; $i++ ) {
344
			$removed_item = array_slice( $revision_history, $from, 1 );
345
			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...
346
				break;
347
			}
348
349
			array_splice( $revision_history, $from, 1 );
350
			self::delete_media_history_file( $media_id, $removed_item[0]->file );
351
		}
352
353
		// override all history items
354
		delete_post_meta( $media_id, self::$WP_REVISION_HISTORY );
355
		$revision_history = array_reverse( $revision_history );
356
		foreach ( $revision_history as &$item ) {
357
			add_post_meta( $media_id, self::$WP_REVISION_HISTORY, $item );
358
		}
359
360
		return $revision_history;
361
	}
362
363
	/**
364
	 * Limit the number of items of the `revision_history` array.
365
	 * When the stack is overflowing the oldest item is remove from there (FIFO).
366
	 * 
367
	 * @param  number $media_id - media post ID
368
	 * @param  number [$limit] - maximun amount of items. 20 as default.
369
	 * @return array items removed from `revision_history`
370
	 */
371
	public static function limit_revision_history( $media_id, $limit = null) {
372
		if ( is_null( $limit ) ) {
373
			$limit = self::$REVISION_HISTORY_MAXIMUM_AMOUNT;
374
		}
375
376
		$revision_history = self::get_revision_history( $media_id );
377
378
		$total = count( $revision_history );
379
380
		if ( $total < $limit ) {
381
			return array();
382
		}
383
384
		self::remove_items_from_revision_history(
385
			$media_id,
386
			array( 'from' => $limit, 'to' => $total ),
387
			$revision_history
388
		);
389
390
		return self::get_revision_history( $media_id );
391
	}
392
393
	/**
394
	 * Remove the original file and clean the post metadata.
395
	 * 
396
	 * @param  number $media_id - media post ID
397
	 */
398
	public static function clean_original_media( $media_id ) {
399
		$original_file = self::get_original_media( $media_id );
400
401
		if ( ! $original_file ) {
402
			return null;
403
		}
404
405
		self::delete_media_history_file( $media_id, $original_file->file );
406
		return delete_post_meta( $media_id, self::$WP_ORIGINAL_MEDIA );
407
	}
408
409
	/**
410
	 * Clean `revision_history` of the given $media_id. it means:
411
	 *   - remove all media files tied to the `revision_history` items.
412
	 *   - clean `revision_history` meta data.
413
	 *   - remove and clean the `original_media`
414
	 * 
415
	 * @param  number $media_id - media post ID
416
	 * @return array results of removing these files
417
	 */
418
	public static function clean_revision_history( $media_id ) {
419
		self::clean_original_media( $media_id );
420
421
		$revision_history = self::get_revision_history( $media_id );
422
		$total = count( $revision_history );
423
		$updated_history = array();
424
425
		if ( $total < 1 ) {
426
			return $updated_history;
427
		}
428
429
		$updated_history = self::remove_items_from_revision_history(
430
			$media_id,
431
			array( 'from' => 0, 'to' => $total ),
432
			$revision_history
433
		);
434
435
		return $updated_history;
436
	}
437
438
	/**
439
	 * Edit media item process:
440
	 *
441
	 * - update attachment file
442
	 * - preserve original media file
443
	 * - trace revision history
444
	 * 
445
	 * @param  number $media_id - media post ID
446
	 * @param  array $file_array - temporal file
447
	 * @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...
448
	 */
449
	public static function edit_media_file( $media_id, $file_array ) {
450
		$media_item = get_post( $media_id );
451
		$has_original_media = self::get_original_media( $media_id );
452
453
		if ( ! $has_original_media ) {
454
			// The first time that the media is updated
455
			// the original media is stored into the revision_history
456
			$snapshot = self::get_snapshot( $media_item );
457
			add_post_meta( $media_id, self::$WP_ORIGINAL_MEDIA, $snapshot, true );
458
		}
459
460
		// save temporary file in the correct location
461
		$uploaded_file = self::save_temporary_file( $file_array, $media_id );
462
463
		if ( is_wp_error( $uploaded_file ) ) {
464
			self::remove_tmp_file( $file_array );
465
			return $uploaded_file;
466
		}
467
468
		// revision_history control
469
		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 461 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...
470
471
		$uploaded_path = $uploaded_file['file'];
472
		$udpated_mime_type = $uploaded_file['type'];
473
		$was_updated = update_attached_file( $media_id, $uploaded_path );
474
475
		if ( ! $was_updated ) {
476
			return WP_Error( 'update_error', 'Media update error' );
477
		}
478
479
		$new_metadata = wp_generate_attachment_metadata( $media_id, $uploaded_path );
480
		wp_update_attachment_metadata( $media_id, $new_metadata );
481
482
		// check maximum amount of revision_history
483
		self::limit_revision_history( $media_id );
484
485
		$edited_action = wp_update_post( (object) array(
486
			'ID'              => $media_id,
487
			'post_mime_type'  => $udpated_mime_type
488
		), true );
489
490
		if ( is_wp_error( $edited_action ) ) {
491
			return $edited_action;
492
		}
493
494
		return $media_item;
495
	}
496
}
497
498
// hook: clean revision history when the media item is deleted
499
function clean_revision_history( $media_id ) {
500
	Jetpack_Media::clean_revision_history( $media_id );
501
};
502
503
add_action( 'delete_attachment', 'clean_revision_history' );
504
505