From a251849f8f72094836e352a569f7b2557f4cdcf9 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Wed, 22 May 2024 21:11:39 +0200 Subject: [PATCH] WI import checking for existing worlds too - WI import uses the same check as create new world - API endpoint to get server-side sanitized filenames - Small changes to toast messages --- public/scripts/utils.js | 30 +++++++++++++++++++ public/scripts/world-info.js | 58 ++++++++++++++++++++++++++---------- src/endpoints/files.js | 17 +++++++++++ 3 files changed, 90 insertions(+), 15 deletions(-) diff --git a/public/scripts/utils.js b/public/scripts/utils.js index 2484e0e8d..10abc1645 100644 --- a/public/scripts/utils.js +++ b/public/scripts/utils.js @@ -1021,6 +1021,36 @@ export function extractDataFromPng(data, identifier = 'chara') { } } +/** + * Sends a request to the server to sanitize a given filename + * + * @param {string} fileName - The name of the file to sanitize + * @returns {Promise} A Promise that resolves to the sanitized filename if successful, or rejects with an error message if unsuccessful + */ +export async function getSanitizedFilename(fileName) { + try { + const result = await fetch('/api/files/sanitize-filename', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify({ + fileName: fileName, + }), + }); + + if (!result.ok) { + const error = await result.text(); + throw new Error(error); + } + + const responseData = await result.json(); + return responseData.fileName; + } catch (error) { + toastr.error(String(error), 'Could not sanitize fileName'); + console.error('Could not sanitize fileName', error); + throw error; + } +} + /** * Sends a base64 encoded image to the backend to be saved as a file. * diff --git a/public/scripts/world-info.js b/public/scripts/world-info.js index a553fffd8..6f282d6dd 100644 --- a/public/scripts/world-info.js +++ b/public/scripts/world-info.js @@ -1,5 +1,5 @@ import { saveSettings, callPopup, substituteParams, getRequestHeaders, chat_metadata, this_chid, characters, saveCharacterDebounced, menu_type, eventSource, event_types, getExtensionPromptByName, saveMetadata, getCurrentChatId, extension_prompt_roles } from '../script.js'; -import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option, waitUntilCondition, isTrueBoolean, setValueByPath, flashHighlight, select2ModifyOptions, getSelect2OptionId, dynamicSelect2DataViaAjax, highlightRegex, select2ChoiceClickSubscribe, isFalseBoolean, equalsIgnoreCaseAndAccents } from './utils.js'; +import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option, waitUntilCondition, isTrueBoolean, setValueByPath, flashHighlight, select2ModifyOptions, getSelect2OptionId, dynamicSelect2DataViaAjax, highlightRegex, select2ChoiceClickSubscribe, isFalseBoolean, equalsIgnoreCaseAndAccents, getSanitizedFilename } from './utils.js'; import { extension_settings, getContext } from './extensions.js'; import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from './authors-note.js'; import { isMobile } from './RossAscends-mods.js'; @@ -2631,19 +2631,9 @@ async function createNewWorldInfo(worldInfoName, { interactive = false } = {}) { return false; } - const existingWorld = world_names.find(x => equalsIgnoreCaseAndAccents(x, worldInfoName)); - if (existingWorld) { - const overwrite = interactive ? await callPopup(`

Creating New World Info

A world with the same name already exists:
${existingWorld}

Do you want to overwrite it?`, 'confirm') : false; - - if (!overwrite) { - toastr.warning(`World creation cancelled. A world with the same name already exists:
${existingWorld}`, 'Creating New World Info', { escapeHtml: false }); - return false; - } - - toastr.info(`Overwriting Existing World Info:
${existingWorld}`, 'Creating New World Info', { escapeHtml: false }); - - // Manually delete, as we want to overwrite. The name might be slightly different so file name would not be the same. - await deleteWorldInfo(existingWorld); + const allowed = await checkCanOverwriteWorldInfo(worldInfoName, { interactive: interactive, actionName: 'Create' }); + if (!allowed) { + return false; } await saveWorldInfo(worldInfoName, worldInfoTemplate, true); @@ -2659,6 +2649,37 @@ async function createNewWorldInfo(worldInfoName, { interactive = false } = {}) { return true; } + +/** + * Confirms if the user wants to overwrite an existing world info with the same name. + * If no world info with the name exists, this simply returns true + * + * @param {string} name - The name of the world info to create + * @param {Object} options - Optional parameters + * @param {boolean} [options.interactive=false] - Whether to show a confirmation dialog when overwriting an existing world + * @param {string} [options.actionName='overwrite'] - The action name to display in the confirmation dialog + * @returns {Promise} True if the user confirmed the overwrite, false otherwise + */ +async function checkCanOverwriteWorldInfo(name, { interactive = false, actionName = 'Overwrite' } = {}) { + const existingWorld = world_names.find(x => equalsIgnoreCaseAndAccents(x, name)); + if (!existingWorld) { + return true; + } + + const overwrite = interactive ? await callPopup(`

World Info ${actionName}

A world with the same name already exists:
${existingWorld}

Do you want to overwrite it?`, 'confirm') : false; + if (!overwrite) { + toastr.warning(`World ${actionName.toLowerCase()} cancelled. A world with the same name already exists:
${existingWorld}`, `World Info ${actionName}`, { escapeHtml: false }); + return false; + } + + toastr.info(`Overwriting Existing World Info:
${existingWorld}`, `World Info ${actionName}`, { escapeHtml: false }); + + // Manually delete, as we want to overwrite. The name might be slightly different so file name would not be the same. + await deleteWorldInfo(existingWorld); + + return true; +} + async function getCharacterLore() { const character = characters[this_chid]; const name = character?.name; @@ -3589,6 +3610,13 @@ export async function importWorldInfo(file) { return; } + const worldName = file.name.substr(0, file.name.lastIndexOf(".")); + const sanitizedWorldName = await getSanitizedFilename(worldName); + const allowed = await checkCanOverwriteWorldInfo(sanitizedWorldName, { interactive: true, actionName: 'Import' }); + if (!allowed) { + return false; + } + jQuery.ajax({ type: 'POST', url: '/api/worldinfo/import', @@ -3606,7 +3634,7 @@ export async function importWorldInfo(file) { $('#world_editor_select').val(newIndex).trigger('change'); } - toastr.info(`World Info "${data.name}" imported successfully!`); + toastr.success(`World Info "${data.name}" imported successfully!`); } }, error: (_jqXHR, _exception) => { }, diff --git a/src/endpoints/files.js b/src/endpoints/files.js index 371381c21..1c571a406 100644 --- a/src/endpoints/files.js +++ b/src/endpoints/files.js @@ -2,11 +2,28 @@ const path = require('path'); const fs = require('fs'); const writeFileSyncAtomic = require('write-file-atomic').sync; const express = require('express'); +const sanitize = require('sanitize-filename'); const router = express.Router(); const { validateAssetFileName } = require('./assets'); const { jsonParser } = require('../express-common'); const { clientRelativePath } = require('../util'); +router.post('/sanitize-filename', jsonParser, async (request, response) => { + try { + const fileName = String(request.body.fileName); + if (!fileName) { + return response.status(400).send('No fileName specified'); + } + + const sanitizedFilename = sanitize(fileName); + console.debug(`Sanitized fileName: ${fileName} -> ${sanitizedFilename}`); + return response.send({ fileName: sanitizedFilename }); + } catch (error) { + console.log(error); + return response.sendStatus(500); + } +}); + router.post('/upload', jsonParser, async (request, response) => { try { if (!request.body.name) {