diff --git a/public/scripts/extensions/assets/index.js b/public/scripts/extensions/assets/index.js index 2aac489c9..8bab38633 100644 --- a/public/scripts/extensions/assets/index.js +++ b/public/scripts/extensions/assets/index.js @@ -63,22 +63,55 @@ function downloadAssetsList(url) { console.debug(DEBUG_PREFIX, "Checking asset", asset["id"], asset["url"]); + const assetInstall = async function () { + element.off("click"); + label.removeClass("fa-download"); + this.classList.add('asset-download-button-loading'); + await installAsset(asset["url"], assetType, asset["id"]); + label.addClass("fa-check"); + this.classList.remove('asset-download-button-loading'); + element.on("click", assetDelete); + element.on("mouseenter", function(){ + label.removeClass("fa-check"); + label.addClass("fa-trash"); + label.addClass("redOverlayGlow"); + }).on("mouseleave", function(){ + label.addClass("fa-check"); + label.removeClass("fa-trash"); + label.removeClass("redOverlayGlow"); + }); + }; + + const assetDelete = async function() { + element.off("click"); + await deleteAsset(assetType, asset["id"]); + label.removeClass("fa-check"); + label.removeClass("redOverlayGlow"); + label.removeClass("fa-trash"); + label.addClass("fa-download"); + element.off("mouseenter").off("mouseleave"); + element.on("click", assetInstall); + } + if (isAssetInstalled(assetType, asset["id"])) { console.debug(DEBUG_PREFIX, "installed, checked"); label.toggleClass("fa-download"); label.toggleClass("fa-check"); + element.on("click", assetDelete); + element.on("mouseenter", function(){ + label.removeClass("fa-check"); + label.addClass("fa-trash"); + label.addClass("redOverlayGlow"); + }).on("mouseleave", function(){ + label.addClass("fa-check"); + label.removeClass("fa-trash"); + label.removeClass("redOverlayGlow"); + }); } else { console.debug(DEBUG_PREFIX, "not installed, unchecked") element.prop("checked", false); - element.on("click", async function () { - 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'); - }) + element.on("click", assetInstall); } console.debug(DEBUG_PREFIX, "Created element for BGM", asset["id"]) @@ -133,6 +166,27 @@ async function installAsset(url, assetType, filename) { } } +async function deleteAsset(assetType, filename) { + console.debug(DEBUG_PREFIX, "Deleting ", assetType, filename); + const category = assetType; + try { + const body = { category, filename }; + const result = await fetch('/asset_delete', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify(body), + cache: 'no-cache', + }); + if (result.ok) { + console.debug(DEBUG_PREFIX, "Deletion success.") + } + } + catch (err) { + console.log(err); + return []; + } +} + //#############################// // API Calls // //#############################// diff --git a/public/scripts/extensions/assets/window.html b/public/scripts/extensions/assets/window.html index e676039b7..a85cfe5e8 100644 --- a/public/scripts/extensions/assets/window.html +++ b/public/scripts/extensions/assets/window.html @@ -10,9 +10,6 @@ -
-

Please refresh ST after downloading new asset to use them.

-
diff --git a/server.js b/server.js index b21d406ca..a0f818baa 100644 --- a/server.js +++ b/server.js @@ -5056,11 +5056,37 @@ app.post('/get_assets', jsonParser, async (request, response) => { } }); + +function checkAssetFileName(inputFilename) { + // Sanitize filename + if (inputFilename.indexOf('\0') !== -1) { + console.debug("Bad request: poisong null bytes in filename."); + return ''; + } + + if (!/^[a-zA-Z0-9_\-\.]+$/.test(inputFilename)) { + console.debug("Bad request: illegal character in filename, only alphanumeric, '_', '-' are accepted."); + return ''; + } + + if (contentManager.unsafeExtensions.some(ext => inputFilename.toLowerCase().endsWith(ext))) { + console.debug("Bad request: forbidden file extension."); + return ''; + } + + if (inputFilename.startsWith('.')) { + console.debug("Bad request: filename cannot start with '.'"); + return ''; + } + + return path.normalize(inputFilename).replace(/^(\.\.(\/|\\|$))+/, '');; +} + /** - * HTTP POST handler function to retrieve a character background music list. + * HTTP POST handler function to download the requested asset. * - * @param {Object} request - HTTP Request object, expects a folder path in the query. - * @param {Object} response - HTTP Response object will contain the path to save file. + * @param {Object} request - HTTP Request object, expects a url, a category and a filename. + * @param {Object} response - HTTP Response only gives status. * * @returns {void} */ @@ -5084,27 +5110,10 @@ app.post('/asset_download', jsonParser, async (request, response) => { } // Sanitize filename - if (inputFilename.indexOf('\0') !== -1) { - console.debug("Bad request: poisong null bytes in filename."); - return response.sendStatus(400); - } + const safe_input = checkAssetFileName(inputFilename); + if (safe_input == '') + return response.sendFile(400); - if (!/^[a-zA-Z0-9_\-\.]+$/.test(inputFilename)) { - console.debug("Bad request: illegal character in filename, only alphanumeric, '_', '-' are accepted."); - return response.sendStatus(400); - } - - if (contentManager.unsafeExtensions.some(ext => inputFilename.toLowerCase().endsWith(ext))) { - console.debug("Bad request: forbidden file extension."); - return response.sendStatus(400); - } - - if (inputFilename.startsWith('.')) { - console.debug("Bad request: filename cannot start with '.'"); - return response.sendStatus(400); - } - - const safe_input = path.normalize(inputFilename).replace(/^(\.\.(\/|\\|$))+/, ''); const temp_path = path.join(directories.assets, "temp", safe_input) const file_path = path.join(directories.assets, category, safe_input) console.debug("Request received to download", url, "to", file_path); @@ -5140,6 +5149,61 @@ app.post('/asset_download', jsonParser, async (request, response) => { } }); +/** + * HTTP POST handler function to delete the requested asset. + * + * @param {Object} request - HTTP Request object, expects a category and a filename + * @param {Object} response - HTTP Response only gives stats. + * + * @returns {void} + */ +app.post('/asset_delete', jsonParser, async (request, response) => { + const { Readable } = require('stream'); + const { finished } = require('stream/promises'); + const inputCategory = request.body.category; + const inputFilename = sanitize(request.body.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 + const safe_input = checkAssetFileName(inputFilename); + if (safe_input == '') + return response.sendFile(400); + + const file_path = path.join(directories.assets, category, safe_input) + console.debug("Request received to delete", category, file_path); + + try { + // Delete if previous download failed + if (fs.existsSync(file_path)) { + fs.unlink(file_path, (err) => { + if (err) throw err; + }); + console.debug("Asset deleted."); + } + else { + console.debug("Asset not found."); + response.sendStatus(400); + } + // Move into asset place + response.sendStatus(200); + } + catch (error) { + console.log(error); + response.sendStatus(500); + } +}); + /////////////////////////////// /**