PluginProbe
FV Player 8 / trunk
FV Player 8 vtrunk
trunk 8.0.18 8.0.19 8.0.20 8.0.21 8.0.25 8.0.27 8.1 8.1.3
fv-player / controller / s3-upload.php

s3-upload.php in FV Player 8 trunk, at controller/s3-upload.php

597 lines 20.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 class FV_Player_S3_Upload {
4
5 function sanitize_path($path) {
6 $path = str_replace( 'Home/', '', stripslashes($path) );
7 $path = preg_replace( '~/$~', '', $path ); // We need to remove trailing slash to keep the breadcrumbs working
8
9 return $path;
10 }
11
12 function remove_special_chars($string) {
13 // coconut doesnt like this characters, we need to remove them
14 $string = str_replace( ' ', '-', $string );
15 $string = str_replace( ',', '-', $string );
16 $string = str_replace( '?', '', $string );
17 $string = str_replace( '&', '', $string );
18 $string = str_replace( '#', '', $string );
19 $string = str_replace( '%', '', $string );
20 $string = str_replace( '^', '', $string );
21 $string = str_replace( '$', '', $string );
22 $string = str_replace( '\'', '', $string );
23 $string = str_replace( '"', '', $string );
24
25 return $string;
26 }
27
28 /**
29 * Easy wrapper around S3 API
30 * @param mixed $command the function to call
31 * @param mixed $args variable args to pass
32 * @return mixed
33 */
34 function s3( $command = null, $args = null) {
35 global $FV_Player_DigitalOcean_Spaces_Browser;
36
37 static $s3 = null;
38 if ( $s3 === null ) {
39 $FV_Player_DigitalOcean_Spaces_Browser->include_aws_sdk();
40 $s3 = $FV_Player_DigitalOcean_Spaces_Browser->get_s3_client();
41 }
42
43 if ( $command === null ) return $s3;
44
45 $args=func_get_args();
46 array_shift($args);
47 try {
48 return call_user_func_array( [$s3, $command ], $args );
49 } catch (AwsException $e) {
50 echo esc_html( $e->getMessage() ), PHP_EOL;
51 }
52
53 return null;
54 }
55
56 function file_exists($contents, $filename) {
57 if ( is_array( $contents ) ) {
58 foreach( $contents as $object ) {
59 if( isset($object['Key']) && $object['Key'] == $filename ) {
60 return true;
61 }
62 }
63 }
64
65 return false;
66 }
67
68 function create_multiupload() {
69 if( !isset($_POST['nonce']) || !wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'fv_flowplayer_create_multiupload' ) ) {
70 wp_send_json( array( 'error' => 'Access denied, please reload the page and try again.' ) );
71 }
72
73 global $FV_Player_DigitalOcean_Spaces;
74
75 $filename = $this->sanitize_path($_POST['fileInfo']['name']);
76 $filename = $this->remove_special_chars($filename);
77
78 $filename = remove_accents( $filename );
79 $filename = str_replace('Ę', 'E', $filename);
80
81 $target = dirname($filename);
82
83 if( $target === '.' ) {
84 $target = '';
85 } else {
86 $target = trailingslashit($target);
87 }
88
89 $filename_parts = explode('.', $filename);
90
91 try {
92 $s3Client = $this->s3();
93
94 if ( ! $s3Client ) {
95 $message = "AWS S3 SDK Failed to load.";
96
97 if ( version_compare(phpversion(),'7.4') == -1 ) {
98 $message .= " You need to use PHP version 7.4 or above.";
99 }
100
101 if ( function_exists( 'FV_Player_Coconut' ) ) {
102 FV_Player_Coconut()->plugin_api->log( "create_multiupload: " . $message );
103 }
104
105 wp_send_json( array( 'error' => $message ) );
106 }
107
108 $bucket = $FV_Player_DigitalOcean_Spaces->get_space();
109
110 // get objects from source space
111 $objects = $s3Client->listObjects(array(
112 'Bucket' => $bucket,
113 'Prefix' => $target,
114 'ResponseCacheControl' => 'No-cache',
115 'ResponseExpires' => gmdate(DATE_RFC2822, time() + 3600),
116 ));
117
118 $contents = $objects->get('Contents');
119
120 $rename_suffix_counter = 2;
121
122 $filename_final = $filename;
123
124 // verify if file already exists and append -{number} to prevent overwriting in source space
125 while( $this->file_exists($contents, $filename_final) ) {
126 $filename_parts = explode('.', $filename);
127 $filename_parts[count($filename_parts) -2] .= '-' . $rename_suffix_counter; // add suffix to second last part of filename before extension
128 $filename_final = implode('.', $filename_parts);
129 $rename_suffix_counter++;
130 }
131
132 // TODO: Is this needed anywhere? If so do it properly!
133 $_POST['fileInfo']['name'] = $filename_final;
134
135 } catch( Aws\S3\Exception\S3Exception $e ) {
136 $message = "Error checking files, please check your DigitalOcean Spaces keys in FV Player -> Coconut -> Settings.";
137
138 if ( function_exists( 'FV_Player_Coconut' ) ) {
139 FV_Player_Coconut()->plugin_api->log( "create_multiupload: " . $message . " Details: " . $e->getMessage() );
140 }
141
142 wp_send_json( array( 'error' => $message ) );
143 }
144
145 /**
146 * Make sure we have correct CORS on the DOS bucket.
147 * But if this fails then just go on as we want to allow key without full privileges
148 * to succeed at the upload.
149 */
150 try {
151 $this->s3("putBucketCors",
152 array(
153 "Bucket" => $FV_Player_DigitalOcean_Spaces->get_space(),
154 "CORSConfiguration" => array(
155 "CORSRules" => array(
156 array(
157 'AllowedHeaders' => array(
158 'Access-Control-Allow-Methods',
159 'Access-Control-Allow-Origin',
160 'Origin',
161 'Range',
162 ),
163 'AllowedMethods'=> array('GET','HEAD','PUT'),
164 "AllowedOrigins"=> array("*"),
165 ),
166 ),
167 ),
168 )
169 );
170 } catch( Aws\S3\Exception\S3Exception $e ) {
171 if ( function_exists( 'FV_Player_Coconut' ) ) {
172 FV_Player_Coconut()->plugin_api->log( "create_multiupload: Error setting CORS: " . $e->getMessage() );
173 }
174 }
175
176 try {
177 $res = $this->s3( "createMultipartUpload", array(
178 'Bucket' => $FV_Player_DigitalOcean_Spaces->get_space(),
179 'Key' => $filename_final,
180 'ContentType' => sanitize_text_field( $_REQUEST['fileInfo']['type'] ),
181 'Metadata' => array(
182 'name' => sanitize_text_field( $_REQUEST['fileInfo']['name'] ),
183 'type' => sanitize_text_field( $_REQUEST['fileInfo']['type'] ),
184 'size' => intval( $_REQUEST['fileInfo']['size'] ),
185 )
186 ));
187
188 if ( function_exists( 'FV_Player_Coconut' ) ) {
189 FV_Player_Coconut()->plugin_api->log( "create_multiupload: uploadId: " . $res->get('UploadId') . " for key: " . $res->get('Key') );
190 }
191
192 wp_send_json( array(
193 'uploadId' => $res->get('UploadId'),
194 'key' => $res->get('Key'),
195 ));
196 } catch( Aws\S3\Exception\S3Exception $e ) {
197 $message = "Error creating upload, please check your DigitalOcean Spaces keys in FV Player -> Coconut -> Settings.";
198
199 if ( function_exists( 'FV_Player_Coconut' ) ) {
200 FV_Player_Coconut()->plugin_api->log( "create_multiupload: " . $message . " Details: " . $e->getMessage() );
201 }
202
203 wp_send_json( array( 'error' => $message ) );
204 }
205
206 wp_die();
207 }
208
209 function validate_file_upload() {
210 if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'fv_flowplayer_validate_file' ) ) {
211 wp_send_json( array( 'error' => 'Access denied, please reload the page and try again.' ) );
212 }
213
214 // Check if file chunk was uploaded
215 if (! isset( $_FILES['file_chunk']) || $_FILES['file_chunk']['error'] !== UPLOAD_ERR_OK ) {
216 $error_msg = 'File upload failed or no file received.';
217 if ( isset( $_FILES['file_chunk'] ) ) {
218 $error_msg .= ' Upload error code: ' . $_FILES['file_chunk']['error'];
219 }
220 wp_send_json(array('error' => $error_msg));
221 }
222
223 // Get file info
224 $file_info = json_decode( stripslashes( $_POST['file_info'] ), true );
225 if ( ! $file_info ) {
226 wp_send_json(array('error' => 'Invalid file information.'));
227 }
228
229 // Basic file validation
230 $uploaded_file = $_FILES['file_chunk'];
231 $file_size = $uploaded_file['size'];
232 $file_name = $uploaded_file['name'];
233
234 // Check file size (5MB chunk should be reasonable)
235 if ( $file_size > 5 * 1024 * 1024 ) {
236 wp_send_json(array('error' => 'File chunk too large: ' . $file_size . ' bytes (max: ' . ( 5 * 1024 * 1024 ) . ' bytes)'));
237 }
238
239 // Check if file is empty
240 if ( $file_size === 0 ) {
241 wp_send_json( array( 'error' => 'File appears to be empty.' ) );
242 }
243
244 // Check for malicious file extensions
245 $dangerous_extensions = array('php', 'php3', 'php4', 'php5', 'phtml', 'pl', 'py', 'cgi', 'asp', 'aspx', 'jsp', 'so', 'dll', 'exe', 'bat', 'cmd', 'sh', 'com');
246 $file_extension = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));
247 if ( in_array( $file_extension, $dangerous_extensions ) ) {
248 wp_send_json( array( 'error' => 'File type not allowed for security reasons: ' . $file_extension ) );
249 }
250
251 // Check for ELF headers (Linux executables)
252 if ( substr( $file_content, 0, 4 ) === "\x7fELF" ) {
253 wp_send_json(array('error' => 'File appears to be a Linux executable and is not allowed.'));
254 }
255
256 // Check for PE headers (Windows executables)
257 if ( substr( $file_content, 0, 2 ) === "MZ" ) {
258 wp_send_json(array('error' => 'File appears to be a Windows executable and is not allowed.'));
259 }
260
261 // Use getID3 to analyze the actual file content
262 if ( ! class_exists( 'getID3' ) ) {
263 require( ABSPATH . WPINC . '/ID3/getid3.php' );
264 }
265 $getID3 = new getID3;
266
267 /**
268 * Analyze the uploaded file.
269 *
270 * Note: This is not 100% reliable as not all the uploaded files will have moov meta data
271 * at the start of the file (in first 5MB). That's why we also run the check in browser,
272 * see s3-upload-base.js file where is runs document.createElement('video').
273 */
274 $ThisFileInfo = $getID3->analyze($uploaded_file['tmp_name']);
275
276 error_log('validate_file_upload: getID3 analysis: ' . print_r($ThisFileInfo, true));
277
278 // Check if getID3 detected a valid file type
279 $detected_mime_type = '';
280 if ( isset( $ThisFileInfo['mime_type'] ) ) {
281 $detected_mime_type = $ThisFileInfo['mime_type'];
282
283 } elseif ( isset( $ThisFileInfo['fileformat'] ) ) {
284 // Map file formats to MIME types
285 $format_mime_map = array(
286 'mp4' => 'video/mp4',
287 'webm' => 'video/webm',
288 'ogg' => 'video/ogg',
289 'avi' => 'video/avi',
290 'mov' => 'video/mov',
291 'wmv' => 'video/wmv',
292 'flv' => 'video/flv',
293 'mkv' => 'video/mkv',
294 'mp3' => 'audio/mp3',
295 'wav' => 'audio/wav',
296 'm4a' => 'audio/m4a',
297 );
298
299 $detected_mime_type = false;
300 if ( isset( $format_mime_map[ $ThisFileInfo['fileformat'] ] ) ) {
301 $detected_mime_type = $format_mime_map[ $ThisFileInfo['fileformat'] ];
302 }
303 }
304
305 // If getID3 couldn't detect the type, fall back to browser MIME type
306 if ( empty( $detected_mime_type ) ) {
307 wp_send_json( array( 'error' => 'File type not supported.' ) );
308 exit;
309 }
310
311 /**
312 * Ensure video resolution is at least the minimal resolution
313 */
314 $video_width = 0;
315 $video_height = 0;
316
317 global $fv_fp;
318 $minimal_video_resolution = $fv_fp->_get_option( array( 'coconut', 'minimal_source_video_resolution' ) );
319
320 if ( $minimal_video_resolution && ! empty( $ThisFileInfo['video']['resolution_x'] ) && ! empty( $ThisFileInfo['video']['resolution_y'] ) ) {
321
322 // Convert resolution names like 720p to actual dimensions
323 $resolution_map = array(
324 '480p' => array( 720, 480 ),
325 '720p' => array( 1280, 720 ),
326 '1080p' => array( 1920, 1080 ),
327 '1440p' => array( 2560, 1440 ),
328 '4K' => array( 3840, 2160 )
329 );
330
331 if ( array_key_exists( $minimal_video_resolution, $resolution_map ) ) {
332 $minimal_width = $resolution_map[ $minimal_video_resolution ][0];
333 $minimal_height = $resolution_map[ $minimal_video_resolution ][1];
334 } else {
335 $minimal_width = 0;
336 $minimal_height = 0;
337 }
338
339 if ( $minimal_width && $minimal_height ) {
340
341 // TODO: Limit vertical videos to 9:16 with minimum width of 720px
342 // TODO: New error message: "Maximum vertical video aspect ratio is 9:16"
343 // TODO: New error message: "Maximum widescreen video aspect ratio is Cinemascope 2.55:1"
344
345 // For 1:3 aspect ratio
346 $minimal_width_4_3 = $minimal_height;
347 $minimal_height_4_3 = $minimal_height;
348
349 // For 2.55:1 aspect ratio
350 $minimal_width_21_9 = $minimal_width;
351 $minimal_height_21_9 = $minimal_width * 1 / 2.55;
352
353 $video_width = absint( $ThisFileInfo['video']['resolution_x'] );
354 $video_height = absint( $ThisFileInfo['video']['resolution_y'] );
355
356 $video_width_check = $video_width;
357 $video_height_check = $video_height;
358 $force_pass = false;
359
360 // We need to flip the dimentions before checking aspect ratio for vertical videos
361 // Alternatively we could parse degrees from $ThisFileInfo['video']['rotate'], but is that commonly used for vertical videos?
362 if ( $video_width < $video_height ) {
363
364 // If vertical video is 1080p or higher with a reasonable width, we accept it
365 if ( $video_height >= 1080 && $video_width >= 540 ) {
366 $force_pass = true;
367
368 } else {
369 $video_width_check = intval( $ThisFileInfo['video']['resolution_y'] );
370 $video_height_check = intval( $ThisFileInfo['video']['resolution_x'] );
371 }
372 }
373
374 if (
375 $force_pass ||
376 $video_width_check >= $minimal_width && $video_height_check >= $minimal_height ||
377 $video_width_check >= $minimal_width_4_3 && $video_height_check >= $minimal_height_4_3 ||
378 $video_width_check >= $minimal_width_21_9 && $video_height_check >= $minimal_height_21_9
379 ) {
380 // Video dimensions are good for one of the aspect ratios
381
382 } else {
383 wp_send_json( array( 'error' => "I'm sorry, your video is only " . absint( $ThisFileInfo['video']['resolution_x'] ) . "x" . absint( $ThisFileInfo['video']['resolution_y'] ) . ". Please re-render it as " . $minimal_video_resolution . " or higher and upload again." ) );
384 exit;
385 }
386
387 }
388 }
389
390 // Define allowed MIME types
391 $allowed_types = array(
392 'video/mp4',
393 'video/webm',
394 'video/ogg',
395 'video/avi',
396 'video/mov',
397 'video/wmv',
398 'video/flv',
399 'video/mkv',
400 'video/quicktime',
401 'video/x-matroska',
402 'audio/mp3',
403 'audio/wav',
404 'audio/ogg',
405 'audio/m4a',
406 );
407
408 if ( ! in_array( $detected_mime_type, $allowed_types ) ) {
409 wp_send_json( array( 'error' => 'File type not allowed: ' . $detected_mime_type ) );
410 }
411
412 // Clean up the uploaded file
413 unlink( $uploaded_file['tmp_name'] );
414
415 // Generate a new nonce for the create_multiupload action
416 $create_multiupload_nonce = wp_create_nonce( 'fv_flowplayer_create_multiupload' );
417
418 wp_send_json(array(
419 'success' => true,
420 'message' => 'File validation passed.',
421 'create_multiupload_nonce' => $create_multiupload_nonce,
422 'multiupload_send_part_nonce' => wp_create_nonce( 'fv_flowplayer_multiupload_send_part' ),
423 'multiupload_abort_nonce' => wp_create_nonce( 'fv_flowplayer_multiupload_abort' ),
424 'multiupload_complete_nonce' => wp_create_nonce( 'fv_flowplayer_multiupload_complete' ),
425 'validated_file_info' => $file_info,
426 'detected_mime_type' => $detected_mime_type,
427 'file_analysis' => array(
428 'fileformat' => isset( $ThisFileInfo['fileformat'] ) ? $ThisFileInfo['fileformat'] : 'unknown',
429 'mime_type' => $detected_mime_type,
430 'filesize' => $file_size,
431 'resolution' => $video_width . 'x' . $video_height,
432 'height' => $video_height,
433 'width' => $video_width,
434 'duration' => $ThisFileInfo['playtime_seconds'],
435 'duration_hms' => flowplayer::format_hms( $ThisFileInfo['playtime_seconds'] ),
436 )
437 ));
438 }
439
440 function multiupload_send_part() {
441 if( !isset($_POST['nonce']) || !wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'fv_flowplayer_multiupload_send_part' ) ) {
442 wp_send_json( array( 'error' => 'Access denied, please reload the page and try again.' ) );
443 }
444
445 global $FV_Player_DigitalOcean_Spaces;
446
447 $args = array(
448 'Bucket' => $FV_Player_DigitalOcean_Spaces->get_space(),
449 'Key' => sanitize_text_field( $_REQUEST['sendBackData']['key'] ),
450 'UploadId' => sanitize_text_field( $_REQUEST['sendBackData']['uploadId'] ),
451 'PartNumber' => intval( $_REQUEST['partNumber'] ),
452 'ContentLength' => intval( $_REQUEST['contentLength'] )
453 );
454
455 if ( function_exists( 'FV_Player_Coconut' ) ) {
456 FV_Player_Coconut()->plugin_api->log( "multiupload_send_part: S3 UploadPart: " . print_r( $args, true ) );
457 }
458
459 $command = $this->s3( "getCommand", "UploadPart", $args );
460
461 // Give it at least 24 hours for large uploads
462 $request = $this->s3( "createPresignedRequest" , $command, "+48 hours" );
463
464 wp_send_json( array(
465 'url' => (string) $request->getUri(),
466 ));
467 wp_die();
468 }
469
470 function multiupload_complete() {
471 if( !isset($_POST['nonce']) || !wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'fv_flowplayer_multiupload_complete' ) ) {
472 wp_send_json( array( 'error' => 'Access denied, please reload the page and try again.' ) );
473 }
474
475 global $FV_Player_DigitalOcean_Spaces;
476
477 // Try to complete the upload 4 times
478 $attempt = 1;
479
480 while( 1 ) {
481
482 // Initial wait as these file parts may take a bit of time to really appear
483 sleep(5);
484
485 try {
486 $args = array(
487 'Bucket' => $FV_Player_DigitalOcean_Spaces->get_space(),
488 'Key' => sanitize_text_field( $_REQUEST['sendBackData']['key'] ),
489 'UploadId' => sanitize_text_field( $_REQUEST['sendBackData']['uploadId'] ),
490 );
491
492 if ( function_exists( 'FV_Player_Coconut' ) ) {
493 FV_Player_Coconut()->plugin_api->log( "multiupload_complete: S3 listParts: " . print_r( $args, true ) );
494 }
495
496 $partsModel = $this->s3("listParts", $args);
497
498 } catch ( Exception $e ) {
499 if ( function_exists( 'FV_Player_Coconut' ) ) {
500 FV_Player_Coconut()->plugin_api->log( "multiupload_complete: S3 listParts exception: " . $e->getMessage() );
501 }
502
503 wp_send_json( array(
504 'error' => true,
505 'message' => $e->getMessage(),
506 ) );
507 }
508
509 $parts = array();
510
511 if (isset($partsModel["Parts"]) ) {
512 $parts = $partsModel["Parts"];
513 } else if (isset($partsModel["data"]["Parts"]) ) {
514 $parts = $partsModel["data"]["Parts"];
515 }
516
517 try {
518 $args = array(
519 'Bucket' => $FV_Player_DigitalOcean_Spaces->get_space(),
520 'Key' => sanitize_text_field( $_REQUEST['sendBackData']['key'] ),
521 'UploadId' => sanitize_text_field( $_REQUEST['sendBackData']['uploadId'] ),
522 'MultipartUpload' => array(
523 "Parts" => $parts,
524 )
525 );
526
527 if ( function_exists( 'FV_Player_Coconut' ) ) {
528 FV_Player_Coconut()->plugin_api->log( "multiupload_complete: S3 completeMultipartUpload: " . print_r( $args, true ) );
529 }
530
531 $ret = $this->s3( "completeMultipartUpload", $args )->toArray();
532
533 // Do not try again if it succeeded!
534 break;
535
536 } catch ( Exception $e ) {
537 $attempt++;
538
539 if ( function_exists( 'FV_Player_Coconut' ) ) {
540 FV_Player_Coconut()->plugin_api->log( "multiupload_complete: S3 completeMultipartUpload exception: " . $e->getMessage() );
541 }
542
543 if ( $attempt > 4 ) {
544 wp_send_json( array(
545 'error' => true,
546 'message' => $e->getMessage(),
547 ) );
548 }
549
550 sleep(5);
551 }
552 }
553
554 wp_send_json( array(
555 'success' => true,
556 'url' => $ret['ObjectURL'],
557 'key' => $ret['Key'],
558 'nonce' => wp_create_nonce( 'fv_player_coconut' ),
559 'attempt' => $attempt
560 ));
561 wp_die();
562 }
563
564 function multiupload_abort() {
565 if( !isset($_POST['nonce']) || !wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'fv_flowplayer_multiupload_abort' ) ) {
566 wp_send_json( array( 'error' => 'Access denied, please reload the page and try again.' ) );
567 }
568
569 global $FV_Player_DigitalOcean_Spaces;
570
571 // if initial pre-upload request fails, we'll have no sendBackData to abort
572 if ( !empty( $_REQUEST['sendBackData'] ) ) {
573
574 $args = array(
575 'Bucket' => $FV_Player_DigitalOcean_Spaces->get_space(),
576 'Key' => sanitize_text_field( $_REQUEST['sendBackData']['key'] ),
577 'UploadId' => sanitize_text_field( $_REQUEST['sendBackData']['uploadId'] )
578 );
579
580 if ( function_exists( 'FV_Player_Coconut' ) ) {
581 FV_Player_Coconut()->plugin_api->log( "multiupload_abort: S3 abortMultipartUpload: " . print_r( $args, true ) );
582 }
583
584 $this->s3("abortMultipartUpload", $args );
585 }
586
587 wp_send_json( array(
588 'success' => true
589 ));
590 wp_die();
591 }
592
593 }
594
595 global $FV_Player_S3_Upload;
596 $FV_Player_S3_Upload = new FV_Player_S3_Upload();
597