From fba4112d3982bf97148834942ddf4ee1021ffbc0 Mon Sep 17 00:00:00 2001 From: mgutt <10757176+mgutt@users.noreply.github.com> Date: Sun, 28 Dec 2025 15:44:00 +0100 Subject: [PATCH 1/7] Optimize file upload performance - Remove Base64 encoding overhead (33% size reduction) - Increase chunk size from 2MB to 20MB - Implement raw binary upload via XMLHttpRequest - Add X-CSRF-Token header support in local_prepend.php - Fix upload speed calculation formula - Add HTTP status code validation and timeout handling - Maintain backward compatibility with legacy Base64 method - Use hash_equals() for timing-attack safe CSRF validation Performance: Achieved 92 MB/s upload speed (vs ~22 MB/s baseline) Security: OWASP-compliant header-based CSRF, strict null checking Compatibility: Works with existing Base64 uploads, no nginx changes Fixes #2495 --- emhttp/plugins/dynamix/Browse.page | 65 ++++++++++++------- emhttp/plugins/dynamix/include/Control.php | 20 ++++-- .../plugins/dynamix/include/local_prepend.php | 17 ++++- 3 files changed, 72 insertions(+), 30 deletions(-) diff --git a/emhttp/plugins/dynamix/Browse.page b/emhttp/plugins/dynamix/Browse.page index cb1f94be8d..c419b7b21c 100644 --- a/emhttp/plugins/dynamix/Browse.page +++ b/emhttp/plugins/dynamix/Browse.page @@ -889,31 +889,52 @@ function downloadFile(source) { function uploadFile(files,index,start,time) { var file = files[index]; - var slice = 2097152; // 2M + var slice = 20971520; // 20MB chunks - no Base64 overhead, raw binary var next = start + slice; var blob = file.slice(start, next); - reader.onloadend = function(e){ - if (e.target.readyState !== FileReader.DONE) return; - $.post('/webGui/include/Control.php',{mode:'upload',file:encodeURIComponent(dir+'/'+dfm_htmlspecialchars(file.name)),start:start,data:window.btoa(e.target.result),cancel:cancel},function(reply){ - if (reply == 'stop') {stopUpload(file.name); return;} - if (reply == 'error') {stopUpload(file.name,true); return;} - if (next < file.size) { - var total = 0; - for (var i=0,f; f=files[i]; i++) { - if (i < index) start += f.size; - total += f.size; - } - const d = new Date(); - var speed = autoscale(((start + slice) * 8) / (d.getTime() - time)); - var percent = Math.floor((start + slice) / total * 100); - $('#dfm_uploadStatus').html("_(Uploading)_: "+percent+"%Speed: "+speed+" ["+(index+1)+'/'+files.length+']  '+file.name+""); - uploadFile(files,index,next,time); - } else if (index < files.length-1) { - uploadFile(files,index+1,0,time); - } else {stopUpload(file.name); return;} - }); + + var xhr = new XMLHttpRequest(); + var url = '/webGui/include/Control.php?mode=upload&file=' + encodeURIComponent(dir + '/' + dfm_htmlspecialchars(file.name)) + '&start=' + start + '&cancel=' + cancel; + xhr.open('POST', url, true); + xhr.setRequestHeader('Content-Type', 'application/octet-stream'); + xhr.setRequestHeader('X-CSRF-Token', ''); + xhr.timeout = 600000; // 10 minutes per chunk + + xhr.onload = function() { + if (xhr.status < 200 || xhr.status >= 300) { + stopUpload(file.name, true); + return; + } + var reply = xhr.responseText; + if (reply == 'stop') {stopUpload(file.name); return;} + if (reply == 'error') {stopUpload(file.name,true); return;} + if (next < file.size) { + var total = 0; + for (var i=0,f; f=files[i]; i++) { + if (i < index) start += f.size; + total += f.size; + } + const d = new Date(); + var bytesTransferred = start + slice; + var elapsedSeconds = (d.getTime() - time) / 1000; + var speed = autoscale(bytesTransferred / elapsedSeconds); + var percent = Math.floor(bytesTransferred / total * 100); + $('#dfm_uploadStatus').html("_(Uploading)_: "+percent+"%Speed: "+speed+" ["+(index+1)+'/'+files.length+']  '+file.name+""); + uploadFile(files,index,next,time); + } else if (index < files.length-1) { + uploadFile(files,index+1,0,time); + } else {stopUpload(file.name); return;} + }; + + xhr.onerror = function() { + stopUpload(file.name,true); + }; + + xhr.ontimeout = function() { + stopUpload(file.name,true); }; - reader.readAsBinaryString(blob); + + xhr.send(blob); } var reader = {}; diff --git a/emhttp/plugins/dynamix/include/Control.php b/emhttp/plugins/dynamix/include/Control.php index 045da68869..fd8308ea8e 100644 --- a/emhttp/plugins/dynamix/include/Control.php +++ b/emhttp/plugins/dynamix/include/Control.php @@ -42,12 +42,14 @@ function validname($name) { function escape($name) {return escapeshellarg(validname($name));} function quoted($name) {return is_array($name) ? implode(' ',array_map('escape',$name)) : escape($name);} -switch ($_POST['mode']) { +switch ($_POST['mode'] ?? $_GET['mode'] ?? '') { case 'upload': - $file = validname(htmlspecialchars_decode(rawurldecode($_POST['file']))); + $file = validname(htmlspecialchars_decode(rawurldecode($_POST['file'] ?? $_GET['file'] ?? ''))); if (!$file) die('stop'); + $start = $_POST['start'] ?? $_GET['start'] ?? 0; + $cancel = $_POST['cancel'] ?? $_GET['cancel'] ?? 0; $local = "/var/tmp/".basename($file).".tmp"; - if ($_POST['start']==0) { + if ($start==0) { $my = pathinfo($file); $n = 0; while (file_exists($file)) $file = $my['dirname'].'/'.preg_replace('/ \(\d+\)$/','',$my['filename']).' ('.++$n.')'.($my['extension'] ? '.'.$my['extension'] : ''); file_put_contents($local,$file); @@ -58,11 +60,19 @@ function quoted($name) {return is_array($name) ? implode(' ',array_map('escape', chmod($file,0666); } $file = file_get_contents($local); - if ($_POST['cancel']==1) { + if ($cancel==1) { delete_file($file); die('stop'); } - if (file_put_contents($file,base64_decode($_POST['data']),FILE_APPEND)===false) { + // Support both legacy base64 method and new raw binary method + if (isset($_POST['data'])) { + // Legacy base64 upload method (backward compatible) + $chunk = base64_decode($_POST['data']); + } else { + // New raw binary upload method (read from request body) + $chunk = file_get_contents('php://input'); + } + if (file_put_contents($file,$chunk,FILE_APPEND)===false) { delete_file($file); die('error'); } diff --git a/emhttp/plugins/dynamix/include/local_prepend.php b/emhttp/plugins/dynamix/include/local_prepend.php index 4159561b33..2cda4eb55d 100644 --- a/emhttp/plugins/dynamix/include/local_prepend.php +++ b/emhttp/plugins/dynamix/include/local_prepend.php @@ -30,11 +30,22 @@ function csrf_terminate($reason) { session_name("unraid_".md5(strstr($_SERVER['HTTP_HOST'].':', ':', true))); } session_set_cookie_params(0, '/', null, $secure, true); -if ($_SERVER['SCRIPT_NAME'] != '/login.php' && $_SERVER['SCRIPT_NAME'] != '/auth-request.php' && isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST') { +if ( + $_SERVER['SCRIPT_NAME'] != '/login.php' && + $_SERVER['SCRIPT_NAME'] != '/auth-request.php' && + isset($_SERVER['REQUEST_METHOD']) && + $_SERVER['REQUEST_METHOD'] === 'POST' +) { if (!isset($var)) $var = parse_ini_file('state/var.ini'); if (!isset($var['csrf_token'])) csrf_terminate("uninitialized"); - if (!isset($_POST['csrf_token'])) csrf_terminate("missing"); - if ($var['csrf_token'] != $_POST['csrf_token']) csrf_terminate("wrong"); + + // accept CSRF token via POST field (webGUI/plugins) or X-header (XHR/API/octet-stream/JSON uploads). + $csrf_token = $_POST['csrf_token'] ?? ($_SERVER['HTTP_X_CSRF_TOKEN'] ?? null); + if ($csrf_token === null) csrf_terminate("missing"); + + // Use hash_equals() for timing-attack safe comparison + if (!hash_equals($var['csrf_token'], $csrf_token)) csrf_terminate("wrong"); + unset($_POST['csrf_token']); } $proxy_cfg = (array)@parse_ini_file('/var/local/emhttp/proxy.ini',true); From 1c6bfac50f4fc8832feca1cd3a830286b3d10306 Mon Sep 17 00:00:00 2001 From: mgutt <10757176+mgutt@users.noreply.github.com> Date: Sun, 28 Dec 2025 15:55:38 +0100 Subject: [PATCH 2/7] Fix progress calculation and add error handling - Fix variable shadowing in progress calculation (use 'completed' instead of reusing 'start') - Add error handling when temp file doesn't exist (e.g., after server restart) --- emhttp/plugins/dynamix/Browse.page | 5 +++-- emhttp/plugins/dynamix/include/Control.php | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/emhttp/plugins/dynamix/Browse.page b/emhttp/plugins/dynamix/Browse.page index c419b7b21c..05a4aa89f7 100644 --- a/emhttp/plugins/dynamix/Browse.page +++ b/emhttp/plugins/dynamix/Browse.page @@ -910,12 +910,13 @@ function uploadFile(files,index,start,time) { if (reply == 'error') {stopUpload(file.name,true); return;} if (next < file.size) { var total = 0; + var completed = 0; for (var i=0,f; f=files[i]; i++) { - if (i < index) start += f.size; + if (i < index) completed += f.size; total += f.size; } const d = new Date(); - var bytesTransferred = start + slice; + var bytesTransferred = completed + next; var elapsedSeconds = (d.getTime() - time) / 1000; var speed = autoscale(bytesTransferred / elapsedSeconds); var percent = Math.floor(bytesTransferred / total * 100); diff --git a/emhttp/plugins/dynamix/include/Control.php b/emhttp/plugins/dynamix/include/Control.php index fd8308ea8e..4b8ab04382 100644 --- a/emhttp/plugins/dynamix/include/Control.php +++ b/emhttp/plugins/dynamix/include/Control.php @@ -60,6 +60,10 @@ function quoted($name) {return is_array($name) ? implode(' ',array_map('escape', chmod($file,0666); } $file = file_get_contents($local); + // Temp file does not exist + if ($file === false) { + die('error'); + } if ($cancel==1) { delete_file($file); die('stop'); From ebb4ad48ab41cef5ae36ba588eb17db38337f47f Mon Sep 17 00:00:00 2001 From: mgutt <10757176+mgutt@users.noreply.github.com> Date: Sun, 28 Dec 2025 16:39:44 +0100 Subject: [PATCH 3/7] Improve upload robustness and error handling - Add dynamic timeout calculation based on chunk size (~1 minute per MB, minimum 10 minutes) - Add specific error messages for timeout, network, and HTTP errors - Add chunk size validation (max 21MB) for defense-in-depth - Fix temp file cleanup for multi-file uploads - Fix type casting for start/cancel parameters to work with strict comparison - Add temp file cleanup on write errors - Add detailed error codes for debugging (tempfile, chunksize, write) - Remove unused FileReader variable (dead code cleanup) - Use strict comparison for start parameter check --- emhttp/plugins/dynamix/Browse.page | 27 ++++++++++++++-------- emhttp/plugins/dynamix/include/Control.php | 15 ++++++++---- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/emhttp/plugins/dynamix/Browse.page b/emhttp/plugins/dynamix/Browse.page index 05a4aa89f7..9148f4e834 100644 --- a/emhttp/plugins/dynamix/Browse.page +++ b/emhttp/plugins/dynamix/Browse.page @@ -866,7 +866,7 @@ function doActions(action, title) { setTimeout(function(){if (dfm.window.find('#dfm_target').length) dfm.window.find('#dfm_target').focus().click(); else $('.ui-dfm .ui-dialog-buttonset button:eq(0)').focus();}); } -function stopUpload(file,error) { +function stopUpload(file,error,errorType) { window.onbeforeunload = null; $.post('/webGui/include/Control.php',{mode:'stop',file:encodeURIComponent(dfm_htmlspecialchars(file))}); $('#dfm_uploadButton').val("_(Upload)_").prop('onclick',null).off('click').click(function(){$('#dfm_upload').click();}); @@ -874,7 +874,12 @@ function stopUpload(file,error) { $('#dfm_upload').val(''); dfm.running = false; loadList(); - if (error) setTimeout(function(){swal({title:"_(Upload Error)_",text:"_(File is removed)_",html:true,confirmButtonText:"_(Ok)_"});},200); + if (error) { + var message = "_(File is removed)_"; + if (errorType === 'timeout') message += "

_(Upload timed out. Please check your network connection and try again.)_"; + else if (errorType === 'network') message += "

_(Network error occurred. Please check your connection and try again.)_"; + setTimeout(function(){swal({title:"_(Upload Error)_",text:message,html:true,confirmButtonText:"_(Ok)_"});},200); + } } function downloadFile(source) { @@ -898,16 +903,20 @@ function uploadFile(files,index,start,time) { xhr.open('POST', url, true); xhr.setRequestHeader('Content-Type', 'application/octet-stream'); xhr.setRequestHeader('X-CSRF-Token', ''); - xhr.timeout = 600000; // 10 minutes per chunk + xhr.timeout = Math.max(600000, slice / 1024 * 60); // ~1 minute per MB, minimum 10 minutes xhr.onload = function() { if (xhr.status < 200 || xhr.status >= 300) { - stopUpload(file.name, true); + stopUpload(file.name, true, 'http'); return; } var reply = xhr.responseText; if (reply == 'stop') {stopUpload(file.name); return;} - if (reply == 'error') {stopUpload(file.name,true); return;} + if (reply.indexOf('error') === 0) { + console.error('Upload error:', reply); + stopUpload(file.name,true); + return; + } if (next < file.size) { var total = 0; var completed = 0; @@ -923,27 +932,27 @@ function uploadFile(files,index,start,time) { $('#dfm_uploadStatus').html("_(Uploading)_: "+percent+"%Speed: "+speed+" ["+(index+1)+'/'+files.length+']  '+file.name+""); uploadFile(files,index,next,time); } else if (index < files.length-1) { + // Clean up temp file for completed upload before starting next file + $.post('/webGui/include/Control.php',{mode:'stop',file:encodeURIComponent(dfm_htmlspecialchars(file.name))}); uploadFile(files,index+1,0,time); } else {stopUpload(file.name); return;} }; xhr.onerror = function() { - stopUpload(file.name,true); + stopUpload(file.name, true, 'network'); }; xhr.ontimeout = function() { - stopUpload(file.name,true); + stopUpload(file.name, true, 'timeout'); }; xhr.send(blob); } -var reader = {}; var cancel = 0; function startUpload(files) { if (files.length == 0) return; - reader = new FileReader(); window.onbeforeunload = function(e){return '';}; $('#dfm_uploadButton').val("_(Cancel)_").prop('onclick',null).off('click').click(function(){cancel=1;}); dfm.running = true; diff --git a/emhttp/plugins/dynamix/include/Control.php b/emhttp/plugins/dynamix/include/Control.php index 4b8ab04382..176a7e5a06 100644 --- a/emhttp/plugins/dynamix/include/Control.php +++ b/emhttp/plugins/dynamix/include/Control.php @@ -46,10 +46,10 @@ function quoted($name) {return is_array($name) ? implode(' ',array_map('escape', case 'upload': $file = validname(htmlspecialchars_decode(rawurldecode($_POST['file'] ?? $_GET['file'] ?? ''))); if (!$file) die('stop'); - $start = $_POST['start'] ?? $_GET['start'] ?? 0; - $cancel = $_POST['cancel'] ?? $_GET['cancel'] ?? 0; + $start = (int)($_POST['start'] ?? $_GET['start'] ?? 0); + $cancel = (int)($_POST['cancel'] ?? $_GET['cancel'] ?? 0); $local = "/var/tmp/".basename($file).".tmp"; - if ($start==0) { + if ($start === 0) { $my = pathinfo($file); $n = 0; while (file_exists($file)) $file = $my['dirname'].'/'.preg_replace('/ \(\d+\)$/','',$my['filename']).' ('.++$n.')'.($my['extension'] ? '.'.$my['extension'] : ''); file_put_contents($local,$file); @@ -62,7 +62,7 @@ function quoted($name) {return is_array($name) ? implode(' ',array_map('escape', $file = file_get_contents($local); // Temp file does not exist if ($file === false) { - die('error'); + die('error:tempfile'); } if ($cancel==1) { delete_file($file); @@ -75,10 +75,15 @@ function quoted($name) {return is_array($name) ? implode(' ',array_map('escape', } else { // New raw binary upload method (read from request body) $chunk = file_get_contents('php://input'); + if (strlen($chunk) > 21000000) { // slightly more than 20MB to allow overhead + unlink($local); + die('error:chunksize:'.strlen($chunk)); + } } if (file_put_contents($file,$chunk,FILE_APPEND)===false) { delete_file($file); - die('error'); + delete_file($local); + die('error:write'); } die(); case 'calc': From 76937d00041466fc79df4be4f4d915b9113badbe Mon Sep 17 00:00:00 2001 From: mgutt <10757176+mgutt@users.noreply.github.com> Date: Sun, 28 Dec 2025 17:25:26 +0100 Subject: [PATCH 4/7] Improve upload cancel functionality - Store xhr reference and call abort() to immediately stop request - Add onabort handler for clean UI reset without error dialog - Check cancel flag in onerror to prevent error on user-initiated abort - Reset cancel flag on each upload start - Add HTTP status code to error messages for better debugging - Move cancel check before file creation to prevent partial files - Wait for cancel cleanup to complete before UI reset (callback) --- emhttp/plugins/dynamix/Browse.page | 25 ++++++++++++++++++++-- emhttp/plugins/dynamix/include/Control.php | 13 +++++++---- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/emhttp/plugins/dynamix/Browse.page b/emhttp/plugins/dynamix/Browse.page index 9148f4e834..60f9286056 100644 --- a/emhttp/plugins/dynamix/Browse.page +++ b/emhttp/plugins/dynamix/Browse.page @@ -878,6 +878,7 @@ function stopUpload(file,error,errorType) { var message = "_(File is removed)_"; if (errorType === 'timeout') message += "

_(Upload timed out. Please check your network connection and try again.)_"; else if (errorType === 'network') message += "

_(Network error occurred. Please check your connection and try again.)_"; + else if (errorType && errorType.indexOf('http') === 0) message += "

_(HTTP error: )_" + errorType.substring(5); setTimeout(function(){swal({title:"_(Upload Error)_",text:message,html:true,confirmButtonText:"_(Ok)_"});},200); } } @@ -899,6 +900,7 @@ function uploadFile(files,index,start,time) { var blob = file.slice(start, next); var xhr = new XMLHttpRequest(); + currentXhr = xhr; // Store for abort capability var url = '/webGui/include/Control.php?mode=upload&file=' + encodeURIComponent(dir + '/' + dfm_htmlspecialchars(file.name)) + '&start=' + start + '&cancel=' + cancel; xhr.open('POST', url, true); xhr.setRequestHeader('Content-Type', 'application/octet-stream'); @@ -907,7 +909,7 @@ function uploadFile(files,index,start,time) { xhr.onload = function() { if (xhr.status < 200 || xhr.status >= 300) { - stopUpload(file.name, true, 'http'); + stopUpload(file.name, true, 'http:' + xhr.status); return; } var reply = xhr.responseText; @@ -938,7 +940,21 @@ function uploadFile(files,index,start,time) { } else {stopUpload(file.name); return;} }; + xhr.onabort = function() { + // User cancelled upload - trigger deletion via cancel=1 parameter + $.post('/webGui/include/Control.php', { + mode: 'upload', + file: dir + '/' + dfm_htmlspecialchars(file.name), + start: 0, + cancel: 1 + }, function() { + stopUpload(file.name, false); + }); + }; + xhr.onerror = function() { + // Don't show error if it was a user cancel + if (cancel === 1) return; stopUpload(file.name, true, 'network'); }; @@ -950,11 +966,16 @@ function uploadFile(files,index,start,time) { } var cancel = 0; +var currentXhr = null; function startUpload(files) { if (files.length == 0) return; + cancel = 0; // Reset cancel flag window.onbeforeunload = function(e){return '';}; - $('#dfm_uploadButton').val("_(Cancel)_").prop('onclick',null).off('click').click(function(){cancel=1;}); + $('#dfm_uploadButton').val("_(Cancel)_").prop('onclick',null).off('click').click(function(){ + cancel=1; + if (currentXhr) currentXhr.abort(); + }); dfm.running = true; const d = new Date(); uploadFile(files,0,0,d.getTime()); diff --git a/emhttp/plugins/dynamix/include/Control.php b/emhttp/plugins/dynamix/include/Control.php index 176a7e5a06..fbbbfa28a2 100644 --- a/emhttp/plugins/dynamix/include/Control.php +++ b/emhttp/plugins/dynamix/include/Control.php @@ -49,6 +49,15 @@ function quoted($name) {return is_array($name) ? implode(' ',array_map('escape', $start = (int)($_POST['start'] ?? $_GET['start'] ?? 0); $cancel = (int)($_POST['cancel'] ?? $_GET['cancel'] ?? 0); $local = "/var/tmp/".basename($file).".tmp"; + // Check cancel BEFORE creating new file + if ($cancel==1) { + if (file_exists($local)) { + $file = file_get_contents($local); + if ($file !== false) delete_file($file); + } + delete_file($local); + die('stop'); + } if ($start === 0) { $my = pathinfo($file); $n = 0; while (file_exists($file)) $file = $my['dirname'].'/'.preg_replace('/ \(\d+\)$/','',$my['filename']).' ('.++$n.')'.($my['extension'] ? '.'.$my['extension'] : ''); @@ -64,10 +73,6 @@ function quoted($name) {return is_array($name) ? implode(' ',array_map('escape', if ($file === false) { die('error:tempfile'); } - if ($cancel==1) { - delete_file($file); - die('stop'); - } // Support both legacy base64 method and new raw binary method if (isset($_POST['data'])) { // Legacy base64 upload method (backward compatible) From 076978036eab3787ade37e3b25116d3d6c296dd2 Mon Sep 17 00:00:00 2001 From: mgutt <10757176+mgutt@users.noreply.github.com> Date: Sun, 28 Dec 2025 17:31:31 +0100 Subject: [PATCH 5/7] Unset X-CSRF-Token header after validation Consistent with existing unset of POST csrf_token field to minimize token exposure after validation completes. --- emhttp/plugins/dynamix/include/local_prepend.php | 1 + 1 file changed, 1 insertion(+) diff --git a/emhttp/plugins/dynamix/include/local_prepend.php b/emhttp/plugins/dynamix/include/local_prepend.php index 2cda4eb55d..d2d7dfd4e2 100644 --- a/emhttp/plugins/dynamix/include/local_prepend.php +++ b/emhttp/plugins/dynamix/include/local_prepend.php @@ -47,6 +47,7 @@ function csrf_terminate($reason) { if (!hash_equals($var['csrf_token'], $csrf_token)) csrf_terminate("wrong"); unset($_POST['csrf_token']); + unset($_SERVER['HTTP_X_CSRF_TOKEN']); } $proxy_cfg = (array)@parse_ini_file('/var/local/emhttp/proxy.ini',true); putenv('http_proxy='.((array_key_exists('http_proxy', $proxy_cfg)) ? $proxy_cfg['http_proxy'] : '')); From 3b0cfe7c4eb24663a8a3eacdabcf7d5ddc17bda1 Mon Sep 17 00:00:00 2001 From: mgutt <10757176+mgutt@users.noreply.github.com> Date: Sun, 28 Dec 2025 17:35:28 +0100 Subject: [PATCH 6/7] Add error handling to upload cancellation POST Use .always() instead of success callback to ensure stopUpload() is called even if the cleanup POST request fails, preventing UI from getting stuck in cancel state. --- emhttp/plugins/dynamix/Browse.page | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/emhttp/plugins/dynamix/Browse.page b/emhttp/plugins/dynamix/Browse.page index 60f9286056..606db80753 100644 --- a/emhttp/plugins/dynamix/Browse.page +++ b/emhttp/plugins/dynamix/Browse.page @@ -947,7 +947,8 @@ function uploadFile(files,index,start,time) { file: dir + '/' + dfm_htmlspecialchars(file.name), start: 0, cancel: 1 - }, function() { + }).always(function() { + // Cleanup UI regardless of POST success/failure stopUpload(file.name, false); }); }; From f23d234a510b40a63c30c1d770bdb71e7bd9d7a2 Mon Sep 17 00:00:00 2001 From: mgutt <10757176+mgutt@users.noreply.github.com> Date: Mon, 29 Dec 2025 09:35:33 +0100 Subject: [PATCH 7/7] Prevent double-slash in file paths and clear XHR reference - Strip trailing slashes from dir before constructing file path - Clear currentXhr reference in stopUpload for clean state - Use consistent filePath variable in both upload and cancel --- emhttp/plugins/dynamix/Browse.page | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/emhttp/plugins/dynamix/Browse.page b/emhttp/plugins/dynamix/Browse.page index 606db80753..3853640a70 100644 --- a/emhttp/plugins/dynamix/Browse.page +++ b/emhttp/plugins/dynamix/Browse.page @@ -868,6 +868,7 @@ function doActions(action, title) { function stopUpload(file,error,errorType) { window.onbeforeunload = null; + currentXhr = null; $.post('/webGui/include/Control.php',{mode:'stop',file:encodeURIComponent(dfm_htmlspecialchars(file))}); $('#dfm_uploadButton').val("_(Upload)_").prop('onclick',null).off('click').click(function(){$('#dfm_upload').click();}); $('#dfm_uploadStatus').html(''); @@ -901,7 +902,8 @@ function uploadFile(files,index,start,time) { var xhr = new XMLHttpRequest(); currentXhr = xhr; // Store for abort capability - var url = '/webGui/include/Control.php?mode=upload&file=' + encodeURIComponent(dir + '/' + dfm_htmlspecialchars(file.name)) + '&start=' + start + '&cancel=' + cancel; + var filePath = dir.replace(/\/+$/, '') + '/' + dfm_htmlspecialchars(file.name); + var url = '/webGui/include/Control.php?mode=upload&file=' + encodeURIComponent(filePath) + '&start=' + start + '&cancel=' + cancel; xhr.open('POST', url, true); xhr.setRequestHeader('Content-Type', 'application/octet-stream'); xhr.setRequestHeader('X-CSRF-Token', ''); @@ -944,7 +946,7 @@ function uploadFile(files,index,start,time) { // User cancelled upload - trigger deletion via cancel=1 parameter $.post('/webGui/include/Control.php', { mode: 'upload', - file: dir + '/' + dfm_htmlspecialchars(file.name), + file: filePath, start: 0, cancel: 1 }).always(function() {