Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 81 additions & 26 deletions emhttp/plugins/dynamix/Browse.page
Original file line number Diff line number Diff line change
Expand Up @@ -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 += "<br><br>_(Upload timed out. Please check your network connection and try again.)_";
else if (errorType === 'network') message += "<br><br>_(Network error occurred. Please check your connection and try again.)_";
else if (errorType && errorType.indexOf('http') === 0) message += "<br><br>_(HTTP error: )_" + errorType.substring(5);
setTimeout(function(){swal({title:"_(Upload Error)_",text:message,html:true,confirmButtonText:"_(Ok)_"});},200);
}
}

function downloadFile(source) {
Expand All @@ -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)_: <span class='dfm_percent'>"+percent+"%</span><span class='dfm_speed'>Speed: "+speed+"</span><span class='orange-text'> ["+(index+1)+'/'+files.length+']&nbsp;&nbsp;'+file.name+"</span>");
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)_: <span class='dfm_percent'>"+percent+"%</span><span class='dfm_speed'>Speed: "+speed+"</span><span class='orange-text'> ["+(index+1)+'/'+files.length+']&nbsp;&nbsp;'+file.name+"</span>");
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());
Expand Down
40 changes: 32 additions & 8 deletions emhttp/plugins/dynamix/include/Control.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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':
Expand Down
18 changes: 15 additions & 3 deletions emhttp/plugins/dynamix/include/local_prepend.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'] : ''));
Expand Down
Loading