diff --git a/emhttp/plugins/dynamix/Browse.page b/emhttp/plugins/dynamix/Browse.page
index cb1f94be8..3853640a7 100644
--- a/emhttp/plugins/dynamix/Browse.page
+++ b/emhttp/plugins/dynamix/Browse.page
@@ -866,15 +866,22 @@ 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;
+ 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('');
$('#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.)_";
+ 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);
+ }
}
function downloadFile(source) {
@@ -889,41 +896,89 @@ 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();
+ currentXhr = xhr; // Store for abort capability
+ 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', '=$var['csrf_token']?>');
+ 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, 'http:' + xhr.status);
+ return;
+ }
+ var reply = xhr.responseText;
+ if (reply == 'stop') {stopUpload(file.name); 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;
+ for (var i=0,f; f=files[i]; i++) {
+ if (i < index) completed += f.size;
+ total += f.size;
+ }
+ const d = new Date();
+ var bytesTransferred = completed + next;
+ 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) {
+ // 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.onabort = function() {
+ // User cancelled upload - trigger deletion via cancel=1 parameter
+ $.post('/webGui/include/Control.php', {
+ mode: 'upload',
+ file: filePath,
+ start: 0,
+ cancel: 1
+ }).always(function() {
+ // Cleanup UI regardless of POST success/failure
+ stopUpload(file.name, false);
});
};
- reader.readAsBinaryString(blob);
+
+ xhr.onerror = function() {
+ // Don't show error if it was a user cancel
+ if (cancel === 1) return;
+ stopUpload(file.name, true, 'network');
+ };
+
+ xhr.ontimeout = function() {
+ stopUpload(file.name, true, 'timeout');
+ };
+
+ xhr.send(blob);
}
-var reader = {};
var cancel = 0;
+var currentXhr = null;
function startUpload(files) {
if (files.length == 0) return;
- reader = new FileReader();
+ 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 045da6886..fbbbfa28a 100644
--- a/emhttp/plugins/dynamix/include/Control.php
+++ b/emhttp/plugins/dynamix/include/Control.php
@@ -42,12 +42,23 @@ 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 = (int)($_POST['start'] ?? $_GET['start'] ?? 0);
+ $cancel = (int)($_POST['cancel'] ?? $_GET['cancel'] ?? 0);
$local = "/var/tmp/".basename($file).".tmp";
- if ($_POST['start']==0) {
+ // 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'] : '');
file_put_contents($local,$file);
@@ -58,13 +69,26 @@ function quoted($name) {return is_array($name) ? implode(' ',array_map('escape',
chmod($file,0666);
}
$file = file_get_contents($local);
- if ($_POST['cancel']==1) {
- delete_file($file);
- die('stop');
+ // Temp file does not exist
+ if ($file === false) {
+ die('error:tempfile');
+ }
+ // 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 (strlen($chunk) > 21000000) { // slightly more than 20MB to allow overhead
+ unlink($local);
+ die('error:chunksize:'.strlen($chunk));
+ }
}
- if (file_put_contents($file,base64_decode($_POST['data']),FILE_APPEND)===false) {
+ if (file_put_contents($file,$chunk,FILE_APPEND)===false) {
delete_file($file);
- die('error');
+ delete_file($local);
+ die('error:write');
}
die();
case 'calc':
diff --git a/emhttp/plugins/dynamix/include/local_prepend.php b/emhttp/plugins/dynamix/include/local_prepend.php
index 4159561b3..d2d7dfd4e 100644
--- a/emhttp/plugins/dynamix/include/local_prepend.php
+++ b/emhttp/plugins/dynamix/include/local_prepend.php
@@ -30,12 +30,24 @@ 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']);
+ 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'] : ''));