Added sanitization of request input for assets_download function. Changed assets download UI for button with little animation while downloading.

This commit is contained in:
Tony Ribeiro 2023-08-24 00:17:07 +02:00
parent 0afcf5a12b
commit a5f66bda63
3 changed files with 120 additions and 22 deletions

View File

@ -56,7 +56,9 @@ function downloadAssetsList(url) {
for (const i in availableAssets[assetType]) { for (const i in availableAssets[assetType]) {
const asset = availableAssets[assetType][i]; const asset = availableAssets[assetType][i];
const elemId = `assets_install_${assetType}_${i}`; const elemId = `assets_install_${assetType}_${i}`;
let element = $('<input />', { type: 'checkbox', id: elemId}) let element = $('<button />', { id:elemId, type:"button", class:"asset-download-button menu_button"})
const label = $("<i class=\"fa-solid fa-download fa-xl\"></i>");
element.append(label);
//if (DEBUG_TONY_SAMA_FORK_MODE) //if (DEBUG_TONY_SAMA_FORK_MODE)
// assetUrl = assetUrl.replace("https://github.com/SillyTavern/","https://github.com/Tony-sama/"); // DBG // assetUrl = assetUrl.replace("https://github.com/SillyTavern/","https://github.com/Tony-sama/"); // DBG
@ -64,17 +66,20 @@ function downloadAssetsList(url) {
console.debug(DEBUG_PREFIX,"Checking asset",asset["id"], asset["url"]); console.debug(DEBUG_PREFIX,"Checking asset",asset["id"], asset["url"]);
if (isAssetInstalled(assetType, asset["id"])) { if (isAssetInstalled(assetType, asset["id"])) {
console.debug(DEBUG_PREFIX,"installed, checked") console.debug(DEBUG_PREFIX,"installed, checked");
element.prop("disabled",true); label.toggleClass("fa-download");
element.prop("checked",true); label.toggleClass("fa-check");
} }
else { else {
console.debug(DEBUG_PREFIX,"not installed, unchecked") console.debug(DEBUG_PREFIX,"not installed, unchecked")
element.prop("checked",false); element.prop("checked",false);
element.on("click", function(){ element.on("click", async function(){
installAsset(asset["url"], assetType, asset["id"]);
element.prop("disabled",true);
element.off("click"); element.off("click");
label.toggleClass("fa-download");
this.classList.toggle('asset-download-button-loading');
await installAsset(asset["url"], assetType, asset["id"]);
label.toggleClass("fa-check");
this.classList.toggle('asset-download-button-loading');
}) })
} }
@ -82,7 +87,7 @@ function downloadAssetsList(url) {
$(`<i></i>`) $(`<i></i>`)
.append(element) .append(element)
.append(`<p>${asset["id"]}</p>`) .append(`<span>${asset["id"]}</span>`)
.appendTo(assetTypeMenu); .appendTo(assetTypeMenu);
} }
assetTypeMenu.appendTo("#assets_menu"); assetTypeMenu.appendTo("#assets_menu");
@ -109,14 +114,15 @@ function isAssetInstalled(assetType,filename) {
async function installAsset(url, assetType, filename) { async function installAsset(url, assetType, filename) {
console.debug(DEBUG_PREFIX,"Downloading ",url); console.debug(DEBUG_PREFIX,"Downloading ",url);
const save_path = "public/assets/"+assetType+"/"+filename; const category = assetType;
try { try {
const result = await fetch(`/asset_download?url=${url}&save_path=${save_path}`, { const result = await fetch(`/asset_download?url=${url}&category=${category}&filename=${filename}`, {
method: 'POST', method: 'POST',
headers: getRequestHeaders(), headers: getRequestHeaders(),
}); });
let assets = result.ok ? (await result.json()) : []; if(result.ok) {
return assets; console.debug(DEBUG_PREFIX,"Download success.")
}
} }
catch (err) { catch (err) {
console.log(err); console.log(err);

View File

@ -6,4 +6,64 @@
.assets-list-div i { .assets-list-div i {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center;
justify-content: left;
padding: 5px;
} }
.assets-list-div i span{
margin-left: 10px;
}
.asset-download-button {
position: relative;
width: 50px;
padding: 8px 16px;
border: none;
outline: none;
border-radius: 2px;
cursor: pointer;
}
.asset-download-button:active {
background: #007a63;
}
.asset-download-button-text {
font: bold 20px "Quicksand", san-serif;
color: #ffffff;
transition: all 0.2s;
}
.asset-download-button-loading .asset-download-button-text {
visibility: hidden;
opacity: 0;
}
.asset-download-button-loading::after {
content: "";
position: absolute;
width: 16px;
height: 16px;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
border: 4px solid transparent;
border-top-color: #ffffff;
border-radius: 50%;
animation: asset-download-button-loading-spinner 1s ease infinite;
}
@keyframes asset-download-button-loading-spinner {
from {
transform: rotate(0turn);
}
to {
transform: rotate(1turn);
}
}

View File

@ -5056,20 +5056,52 @@ app.post('/asset_download', jsonParser, async (request, response) => {
const { finished } = require('stream/promises'); const { finished } = require('stream/promises');
const path = require("path"); const path = require("path");
const url = request.query.url; const url = request.query.url;
const file_path = request.query.save_path; const inputCategory = request.query.category;
const inputFilename = request.query.filename;
const validCategories = ["bgm","ambient"]
// Check category
let category = null
for(i of validCategories)
if (i == inputCategory)
category = i
if (category === null) {
console.debug("Bad request: unsuported asset category.");
return response.sendStatus(400);
}
// Sanitize filename
if (inputFilename.indexOf('\0') !== -1) {
console.debug("Bad request: poisong null bytes in filename.");
return response.sendStatus(400);
}
if (!/^[a-zA-Z0-9_\-\.]+$/.test(inputFilename)) {
console.debug("Bad request: illegal character in filename, only alphanumeric, '_', '-' are accepted.");
return response.sendStatus(400);
}
const safe_input = path.normalize(inputFilename).replace(/^(\.\.(\/|\\|$))+/, '');
const file_path = path.join(directories.assets, category, safe_input)
console.debug("Request received to download", url,"to",file_path); console.debug("Request received to download", url,"to",file_path);
try {
const downloadFile = (async (url, file_path) => { const downloadFile = (async (url, file_path) => {
const res = await fetch(url); const res = await fetch(url);
const destination = path.resolve(file_path); const destination = path.resolve(file_path);
const fileStream = fs.createWriteStream(destination, { flags: 'wx' }); const fileStream = fs.createWriteStream(destination, { flags: 'wx' });
await finished(Readable.fromWeb(res.body).pipe(fileStream)); await finished(Readable.fromWeb(res.body).pipe(fileStream));
console.debug("Download finished, file saved to",file_path); console.debug("Download finished, file saved to",file_path);
}); });
downloadFile(url, file_path) await downloadFile(url, file_path);
response.sendStatus(200);
}
catch(error) {
console.log(error);
response.sendStatus(500);
}
}); });