Skip to content

Commit b55dca4

Browse files
authored
Merge pull request #412 from Codeinwp/fix/image-upload-chunks
fix: chunk file name for uploaded files
2 parents 93cb728 + 9ddd1bd commit b55dca4

File tree

2 files changed

+201
-60
lines changed

2 files changed

+201
-60
lines changed

inc/files.php

Lines changed: 107 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,66 @@ function ppom_create_thumb_for_meta( $file_name, $product_id, $cropped = false,
131131
return apply_filters( 'ppom_meta_file_thumb', $ppom_html, $file_name, $product_id );
132132
}
133133

134+
/**
135+
* Create a new file name that contains a unique string.
136+
*
137+
* @param string $file_name The file name.
138+
* @param string $file_ext The file extension.
139+
*
140+
* @return string The new file name.
141+
*/
142+
function ppom_create_unique_file_name( $file_name, $file_ext ) {
143+
return $file_name . "." . base64_encode( substr( wp_hash_password( $file_name ), 0, 8 ) ) . "." . $file_ext;
144+
}
145+
146+
final class UploadFileErrors {
147+
const OPEN_INPUT = 'open_input';
148+
const OPEN_OUTPUT = 'open_output';
149+
const MISSING_TEMP_FILE = 'missing_temp_file';
150+
const OPEN_DIR = 'open_dir';
151+
152+
static function get_message_response( $error_slug ) {
153+
$msg = array(
154+
self::OPEN_INPUT => '{"jsonrpc" : "2.0", "error" : {"code": 101, "message": "Failed to open input stream."}, "id" : "id"}',
155+
self::OPEN_OUTPUT => '{"jsonrpc" : "2.0", "error" : {"code": 102, "message": "Failed to open output stream."}, "id" : "id"}',
156+
self::MISSING_TEMP_FILE => '{"jsonrpc" : "2.0", "error" : {"code": 103, "message": "Failed to move uploaded file."}, "id" : "id"}',
157+
self::OPEN_DIR => '{"jsonrpc" : "2.0", "error" : {"code": 100, "message": "Failed to open temp directory."}, "id" : "id"}',
158+
);
159+
160+
return isset( $msg[$error_slug] ) ? $msg[$error_slug] : false;
161+
}
162+
}
163+
164+
/**
165+
* Move the content of the file to read to the given ppom file chunk.
166+
*
167+
* @param string $file_path_to_read The file to read.
168+
* @param string $ppom_chunk_file_path The chunk file to write.
169+
* @param string $mode The writing mode for the chunk file.
170+
*
171+
* @return false|string The error.
172+
*/
173+
function ppom_create_chunk_file( $file_path_to_read, $ppom_chunk_file_path, $mode ) {
174+
$chunk_file = fopen( $ppom_chunk_file_path, $mode );
175+
if ( $chunk_file ) {
176+
// Read binary input stream and append it to temp file
177+
$temp_file = fopen( $file_path_to_read, 'rb' );
178+
179+
if ( $temp_file ) {
180+
while ( $buff = fread( $temp_file, 4096 ) ) {
181+
fwrite( $chunk_file, $buff );
182+
}
183+
} else {
184+
return UploadFileErrors::OPEN_INPUT;
185+
}
186+
fclose( $temp_file );
187+
fclose( $chunk_file );
188+
} else {
189+
return UploadFileErrors::OPEN_OUTPUT;
190+
}
191+
192+
return false;
193+
}
134194

135195
function ppom_upload_file() {
136196

@@ -140,7 +200,7 @@ function ppom_upload_file() {
140200
header( 'Cache-Control: post-check=0, pre-check=0', false );
141201
header( 'Pragma: no-cache' );
142202

143-
$ppom_nonce = $_REQUEST['ppom_nonce'];
203+
$ppom_nonce = sanitize_key( $_REQUEST['ppom_nonce'] );
144204
$file_upload_nonce_action = 'ppom_uploading_file_action';
145205
if ( ! wp_verify_nonce( $ppom_nonce, $file_upload_nonce_action ) && apply_filters( 'ppom_verify_upload_file', true ) ) {
146206
$response ['status'] = 'error';
@@ -200,14 +260,17 @@ function ppom_upload_file() {
200260
// Get parameters
201261
$chunk = isset( $_REQUEST ['chunk'] ) ? intval( $_REQUEST ['chunk'] ) : 0;
202262
$chunks = isset( $_REQUEST ['chunks'] ) ? intval( $_REQUEST ['chunks'] ) : 0;
263+
203264
// $file_name = isset ( $_REQUEST ["name"] ) ? sanitize_file_name($_REQUEST ["name"]) : '';
204265

205266
$file_path_thumb = $file_dir_path . 'thumbs';
206267
$file_name = wp_unique_filename( $file_path_thumb, $file_name );
207268
$file_name = strtolower( $file_name );
208269
$file_ext = pathinfo( $file_name, PATHINFO_EXTENSION );
209-
$unique_hash = substr( hash( 'sha256', wp_generate_password( 8, false, false ) ), 0, 8 );
210-
$file_name = str_replace( ".$file_ext", ".$unique_hash.$file_ext", $file_name );
270+
$original_name = $file_name;
271+
$original_name = str_replace(".$file_ext", "", $original_name);
272+
$file_hash = substr( hash('haval192,5', $file_name), 0, 8 ) . '-' . $ppom_nonce;
273+
$file_name = str_replace( ".$file_ext", ".$file_hash.$file_ext", $file_name );
211274
$file_path = $file_dir_path . $file_name;
212275

213276
// Make sure the fileName is unique but only if chunking is disabled
@@ -226,88 +289,72 @@ function ppom_upload_file() {
226289
}
227290

228291
// Remove old temp files
229-
if ( $cleanupTargetDir && is_dir( $file_dir_path ) && ( $dir = opendir( $file_dir_path ) ) ) {
292+
if ( is_dir( $file_dir_path ) && ( $dir = opendir( $file_dir_path ) ) ) {
230293
while ( ( $file = readdir( $dir ) ) !== false ) {
231-
$tmpfilePath = $file_dir_path . $file;
294+
$tmp_file_path = $file_dir_path . $file;
232295

233296
// Remove temp file if it is older than the max age and is not the current file
234-
if ( preg_match( '/\.part$/', $file ) && ( filemtime( $tmpfilePath ) < time() - $maxFileAge ) && ( $tmpfilePath != "{$file_path}.part" ) ) {
235-
@unlink( $tmpfilePath );
297+
if (
298+
preg_match( '/\.part$/', $file ) &&
299+
( filemtime( $tmp_file_path ) < time() - $maxFileAge ) &&
300+
( $tmp_file_path != "$file_path.part" )
301+
) {
302+
@unlink( $tmp_file_path );
236303
}
237304
}
238305

239306
closedir( $dir );
240307
} else {
241-
die( '{"jsonrpc" : "2.0", "error" : {"code": 100, "message": "Failed to open temp directory."}, "id" : "id"}' );
308+
die( UploadFileErrors::get_message_response( UploadFileErrors::MISSING_TEMP_FILE ) );
242309
}
243310

244-
311+
$http_content_type = '';
245312
// Look for the content type header
246313
if ( isset( $_SERVER ['HTTP_CONTENT_TYPE'] ) ) {
247-
$contentType = $_SERVER ['HTTP_CONTENT_TYPE'];
314+
$http_content_type = $_SERVER ['HTTP_CONTENT_TYPE'];
248315
}
249316

250317
if ( isset( $_SERVER ['CONTENT_TYPE'] ) ) {
251-
$contentType = $_SERVER ['CONTENT_TYPE'];
318+
$http_content_type = $_SERVER ['CONTENT_TYPE'];
252319
}
253320

254-
// Handle non multipart uploads older WebKit versions didn't support multipart in HTML5
255-
if ( strpos( $contentType, 'multipart' ) !== false ) {
256-
if ( isset( $_FILES ['file'] ['tmp_name'] ) && is_uploaded_file( $_FILES ['file'] ['tmp_name'] ) ) {
257-
// Open temp file
258-
$out = fopen( "{$file_path}.part", $chunk == 0 ? 'wb' : 'ab' );
259-
if ( $out ) {
260-
// Read binary input stream and append it to temp file
261-
$in = fopen( sanitize_text_field( $_FILES ['file'] ['tmp_name'] ), 'rb' );
262-
263-
if ( $in ) {
264-
while ( $buff = fread( $in, 4096 ) ) {
265-
fwrite( $out, $buff );
266-
}
267-
} else {
268-
die( '{"jsonrpc" : "2.0", "error" : {"code": 101, "message": "Failed to open input stream."}, "id" : "id"}' );
269-
}
270-
fclose( $in );
271-
fclose( $out );
272-
@unlink( sanitize_text_field( $_FILES ['file'] ['tmp_name'] ) );
273-
} else {
274-
die( '{"jsonrpc" : "2.0", "error" : {"code": 102, "message": "Failed to open output stream."}, "id" : "id"}' );
275-
}
276-
} else {
277-
die( '{"jsonrpc" : "2.0", "error" : {"code": 103, "message": "Failed to move uploaded file."}, "id" : "id"}' );
278-
}
279-
} else {
280-
// Open temp file
281-
$out = fopen( "{$file_path}.part", $chunk == 0 ? 'wb' : 'ab' );
282-
if ( $out ) {
283-
// Read binary input stream and append it to temp file
284-
$in = fopen( 'php://input', 'rb' );
285-
286-
if ( $in ) {
287-
while ( $buff = fread( $in, 4096 ) ) {
288-
fwrite( $out, $buff );
289-
}
290-
} else {
291-
die( '{"jsonrpc" : "2.0", "error" : {"code": 101, "message": "Failed to open input stream."}, "id" : "id"}' );
292-
}
321+
$temp_file_name = isset( $_FILES['file']['tmp_name'] ) ? realpath( $_FILES['file']['tmp_name'] ) : '';
322+
$is_multipart = ! empty( $http_content_type ) && false !== strpos( $http_content_type, 'multipart' );
293323

294-
fclose( $in );
295-
fclose( $out );
296-
} else {
297-
die( '{"jsonrpc" : "2.0", "error" : {"code": 102, "message": "Failed to open output stream."}, "id" : "id"}' );
298-
}
324+
if (
325+
$is_multipart &&
326+
( empty( $temp_file_name ) || ! is_uploaded_file( $temp_file_name ) )
327+
) {
328+
die( UploadFileErrors::get_message_response( UploadFileErrors::MISSING_TEMP_FILE ) );
299329
}
300330

301-
// Check if file has been uploaded
302-
if ( ! $chunks || $chunk == $chunks - 1 ) {
303-
// Strip the temp .part suffix off
304-
rename( "{$file_path}.part", $file_path );
331+
$chunk_file_path = "$file_path.part";
332+
$uploaded_file_path_to_read = $is_multipart ? $temp_file_name : 'php://input';
333+
334+
$error = ppom_create_chunk_file( $uploaded_file_path_to_read, $chunk_file_path, $chunk == 0 ? 'wb' : 'ab' );
335+
336+
if ( $is_multipart ) {
337+
@unlink( $temp_file_name );
338+
}
339+
340+
if ( $error ) {
341+
die( UploadFileErrors::get_message_response( $error ) );
342+
}
343+
344+
// Check if the file has been uploaded completely.
345+
if ( ! $chunks || $chunk === $chunks - 1 ) {
346+
347+
// Give a unique name to prevent name collisions.
348+
$file_name = ppom_create_unique_file_name( $original_name, $file_ext );
349+
$unique_file_path = $file_dir_path . $file_name;
350+
351+
rename( $chunk_file_path, $unique_file_path );
352+
$file_path = $unique_file_path;
305353

306354
$product_id = intval( $_REQUEST['product_id'] );
307355
$data_name = sanitize_key( $_REQUEST['data_name'] );
308356
$file_meta = ppom_get_field_meta_by_dataname( $product_id, $data_name );
309357

310-
311358
// making thumb if images
312359
if ( ppom_is_file_image( $file_path ) ) {
313360

tests/test-file-upload.php

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
/**
3+
* Class Test_File_Upload
4+
*
5+
* @package ppom-pro
6+
*/
7+
8+
class Test_File_Upload extends WP_UnitTestCase {
9+
10+
/**
11+
* Test ppom_create_unique_file_name function.
12+
*/
13+
public function test_ppom_create_unique_file_name() {
14+
$file_name = 'example';
15+
$file_ext = 'jpg';
16+
17+
$unique_file_name = ppom_create_unique_file_name($file_name, $file_ext);
18+
19+
// Check if the file name contains the original name
20+
$this->assertStringContainsString($file_name, $unique_file_name);
21+
22+
// Check if the file name contains the extension
23+
$this->assertStringContainsString($file_ext, $unique_file_name);
24+
25+
// Check if the file name contains the unique hash
26+
$this->assertMatchesRegularExpression('/\.[a-zA-Z0-9+\/=]+\./', $unique_file_name);
27+
}
28+
29+
/**
30+
* Test ppom_create_unique_file_name generates different names on subsequent calls.
31+
*/
32+
public function test_ppom_create_unique_file_name_is_unique() {
33+
$file_name = 'example';
34+
$file_ext = 'jpg';
35+
36+
$unique_file_name_1 = ppom_create_unique_file_name($file_name, $file_ext);
37+
$unique_file_name_2 = ppom_create_unique_file_name($file_name, $file_ext);
38+
39+
// Check if the two generated file names are different
40+
$this->assertNotEquals($unique_file_name_1, $unique_file_name_2);
41+
}
42+
43+
/**
44+
* Test ppom_create_chunk_file function.
45+
*/
46+
public function test_ppom_create_chunk_file_success() {
47+
$file_path_to_read = tempnam(sys_get_temp_dir(), 'read');
48+
$ppom_chunk_file_path = tempnam(sys_get_temp_dir(), 'chunk');
49+
$mode = 'wb';
50+
51+
52+
file_put_contents($file_path_to_read, 'test data');
53+
54+
$error = ppom_create_chunk_file($file_path_to_read, $ppom_chunk_file_path, $mode);
55+
56+
$this->assertFalse($error);
57+
58+
// Check if the chunk file contains the correct data
59+
$this->assertFileExists($ppom_chunk_file_path);
60+
$this->assertEquals('test data', file_get_contents($ppom_chunk_file_path));
61+
62+
// Clean up
63+
unlink($file_path_to_read);
64+
unlink($ppom_chunk_file_path);
65+
}
66+
67+
public function test_ppom_create_chunk_file_input_error() {
68+
$file_path_to_read = 'non_existent_file';
69+
$ppom_chunk_file_path = tempnam(sys_get_temp_dir(), 'chunk');
70+
$mode = 'wb';
71+
72+
$error = @ppom_create_chunk_file($file_path_to_read, $ppom_chunk_file_path, $mode);
73+
74+
$this->assertEquals(UploadFileErrors::OPEN_INPUT, $error);
75+
76+
// Clean up
77+
unlink($ppom_chunk_file_path);
78+
}
79+
80+
public function test_ppom_create_chunk_file_output_error() {
81+
$file_path_to_read = tempnam(sys_get_temp_dir(), 'read');
82+
$ppom_chunk_file_path = '/invalid/path/chunk';
83+
$mode = 'wb';
84+
85+
86+
file_put_contents($file_path_to_read, 'test data');
87+
$error = @ppom_create_chunk_file($file_path_to_read, $ppom_chunk_file_path, $mode);
88+
89+
$this->assertEquals(UploadFileErrors::OPEN_OUTPUT, $error);
90+
91+
// Clean up
92+
unlink($file_path_to_read);
93+
}
94+
}

0 commit comments

Comments
 (0)