Prefer sanitize filename with extension
| @@ -44,7 +44,7 @@ import { tokenizers } from './tokenizers.js'; | |||
| 44 | import { BIAS_CACHE } from './logit-bias.js'; | 44 | import { BIAS_CACHE } from './logit-bias.js'; |
| 45 | import { renderTemplateAsync } from './templates.js'; | 45 | import { renderTemplateAsync } from './templates.js'; |
| 46 | 46 | ||
| 47 | import { countOccurrences, debounce, delay, download, getFileText, getStringHash, isOdd, isTrueBoolean, onlyUnique, resetScrollHeight, shuffle, sortMoments, stringToRange, timestampToMoment } from './utils.js'; | 47 | import { countOccurrences, debounce, delay, download, getFileText, getSanitizedFilename, getStringHash, isOdd, isTrueBoolean, onlyUnique, resetScrollHeight, shuffle, sortMoments, stringToRange, timestampToMoment } from './utils.js'; |
| 48 | import { FILTER_TYPES } from './filters.js'; | 48 | import { FILTER_TYPES } from './filters.js'; |
| 49 | import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js'; | 49 | import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js'; |
| 50 | import { SlashCommand } from './slash-commands/SlashCommand.js'; | 50 | import { SlashCommand } from './slash-commands/SlashCommand.js'; |
| @@ -2347,7 +2347,7 @@ async function saveTheme(name = undefined, theme = undefined) { | |||
| 2347 | return; | 2347 | return; |
| 2348 | } | 2348 | } |
| 2349 | 2349 | ||
| 2350 | name = String(newName); | 2350 | name = await getSanitizedFilename(String(newName)); |
| 2351 | } | 2351 | } |
| 2352 | 2352 | ||
| 2353 | if (typeof theme !== 'object') { | 2353 | if (typeof theme !== 'object') { |
| @@ -2458,7 +2458,7 @@ async function saveMovingUI() { | |||
| 2458 | return; | 2458 | return; |
| 2459 | } | 2459 | } |
| 2460 | 2460 | ||
| 2461 | const name = String(popupResult); | 2461 | const name = await getSanitizedFilename(String(popupResult)); |
| 2462 | 2462 | ||
| 2463 | const movingUIPreset = { | 2463 | const movingUIPreset = { |
| 2464 | name, | 2464 | name, |
| @@ -6,6 +6,7 @@ import sanitize from 'sanitize-filename'; | |||
| 6 | import { sync as writeFileAtomicSync } from 'write-file-atomic'; | 6 | import { sync as writeFileAtomicSync } from 'write-file-atomic'; |
| 7 | 7 | ||
| 8 | import { humanizedISO8601DateTime } from '../util.js'; | 8 | import { humanizedISO8601DateTime } from '../util.js'; |
| 9 | import { getFileNameValidationFunction } from '../middleware/validateFileName.js'; | ||
| 9 | 10 | ||
| 10 | export const router = express.Router(); | 11 | export const router = express.Router(); |
| 11 | 12 | ||
| @@ -76,7 +77,7 @@ router.post('/create', (request, response) => { | |||
| 76 | generation_mode_join_prefix: request.body.generation_mode_join_prefix ?? '', | 77 | generation_mode_join_prefix: request.body.generation_mode_join_prefix ?? '', |
| 77 | generation_mode_join_suffix: request.body.generation_mode_join_suffix ?? '', | 78 | generation_mode_join_suffix: request.body.generation_mode_join_suffix ?? '', |
| 78 | }; | 79 | }; |
| 79 | const pathToFile = path.join(request.user.directories.groups, `${id}.json`); | 80 | const pathToFile = path.join(request.user.directories.groups, sanitize(`${id}.json`)); |
| 80 | const fileData = JSON.stringify(groupMetadata, null, 4); | 81 | const fileData = JSON.stringify(groupMetadata, null, 4); |
| 81 | 82 | ||
| 82 | if (!fs.existsSync(request.user.directories.groups)) { | 83 | if (!fs.existsSync(request.user.directories.groups)) { |
| @@ -87,19 +88,19 @@ router.post('/create', (request, response) => { | |||
| 87 | return response.send(groupMetadata); | 88 | return response.send(groupMetadata); |
| 88 | }); | 89 | }); |
| 89 | 90 | ||
| 90 | router.post('/edit', (request, response) => { | 91 | router.post('/edit', getFileNameValidationFunction('id'), (request, response) => { |
| 91 | if (!request.body || !request.body.id) { | 92 | if (!request.body || !request.body.id) { |
| 92 | return response.sendStatus(400); | 93 | return response.sendStatus(400); |
| 93 | } | 94 | } |
| 94 | const id = request.body.id; | 95 | const id = request.body.id; |
| 95 | const pathToFile = path.join(request.user.directories.groups, `${id}.json`); | 96 | const pathToFile = path.join(request.user.directories.groups, sanitize(`${id}.json`)); |
| 96 | const fileData = JSON.stringify(request.body, null, 4); | 97 | const fileData = JSON.stringify(request.body, null, 4); |
| 97 | 98 | ||
| 98 | writeFileAtomicSync(pathToFile, fileData); | 99 | writeFileAtomicSync(pathToFile, fileData); |
| 99 | return response.send({ ok: true }); | 100 | return response.send({ ok: true }); |
| 100 | }); | 101 | }); |
| 101 | 102 | ||
| 102 | router.post('/delete', async (request, response) => { | 103 | router.post('/delete', getFileNameValidationFunction('id'), async (request, response) => { |
| 103 | if (!request.body || !request.body.id) { | 104 | if (!request.body || !request.body.id) { |
| 104 | return response.sendStatus(400); | 105 | return response.sendStatus(400); |
| 105 | } | 106 | } |
| @@ -114,7 +115,7 @@ router.post('/delete', async (request, response) => { | |||
| 114 | if (group && Array.isArray(group.chats)) { | 115 | if (group && Array.isArray(group.chats)) { |
| 115 | for (const chat of group.chats) { | 116 | for (const chat of group.chats) { |
| 116 | console.info('Deleting group chat', chat); | 117 | console.info('Deleting group chat', chat); |
| 117 | const pathToFile = path.join(request.user.directories.groupChats, `${id}.jsonl`); | 118 | const pathToFile = path.join(request.user.directories.groupChats, sanitize(`${chat}.jsonl`)); |
| 118 | 119 | ||
| 119 | if (fs.existsSync(pathToFile)) { | 120 | if (fs.existsSync(pathToFile)) { |
| 120 | fs.unlinkSync(pathToFile); | 121 | fs.unlinkSync(pathToFile); |
| @@ -10,7 +10,7 @@ router.post('/save', (request, response) => { | |||
| 10 | return response.sendStatus(400); | 10 | return response.sendStatus(400); |
| 11 | } | 11 | } |
| 12 | 12 | ||
| 13 | const filename = path.join(request.user.directories.movingUI, sanitize(request.body.name) + '.json'); | 13 | const filename = path.join(request.user.directories.movingUI, sanitize(`${request.body.name}.json`)); |
| 14 | writeFileAtomicSync(filename, JSON.stringify(request.body, null, 4), 'utf8'); | 14 | writeFileAtomicSync(filename, JSON.stringify(request.body, null, 4), 'utf8'); |
| 15 | 15 | ||
| 16 | return response.sendStatus(200); | 16 | return response.sendStatus(200); |
| @@ -11,7 +11,7 @@ import { getDefaultPresetFile, getDefaultPresets } from './content-manager.js'; | |||
| 11 | * Gets the folder and extension for the preset settings based on the API source ID. | 11 | * Gets the folder and extension for the preset settings based on the API source ID. |
| 12 | * @param {string} apiId API source ID | 12 | * @param {string} apiId API source ID |
| 13 | * @param {import('../users.js').UserDirectoryList} directories User directories | 13 | * @param {import('../users.js').UserDirectoryList} directories User directories |
| 14 | * @returns {object} Object containing the folder and extension for the preset settings | 14 | * @returns {{folder: string?, extension: string?}} Object containing the folder and extension for the preset settings |
| 15 | */ | 15 | */ |
| 16 | function getPresetSettingsByAPI(apiId, directories) { | 16 | function getPresetSettingsByAPI(apiId, directories) { |
| 17 | switch (apiId) { | 17 | switch (apiId) { |
| @@ -108,8 +108,7 @@ router.post('/save-openai', function (request, response) { | |||
| 108 | const name = sanitize(request.query.name); | 108 | const name = sanitize(request.query.name); |
| 109 | if (!name) return response.sendStatus(400); | 109 | if (!name) return response.sendStatus(400); |
| 110 | 110 | ||
| 111 | const filename = `${name}.json`; | 111 | const fullpath = path.join(request.user.directories.openAI_Settings, `${name}.json`); |
| 112 | const fullpath = path.join(request.user.directories.openAI_Settings, filename); | ||
| 113 | writeFileAtomicSync(fullpath, JSON.stringify(request.body, null, 4), 'utf-8'); | 112 | writeFileAtomicSync(fullpath, JSON.stringify(request.body, null, 4), 'utf-8'); |
| 114 | return response.send({ name }); | 113 | return response.send({ name }); |
| 115 | }); | 114 | }); |
| @@ -120,7 +119,8 @@ router.post('/delete-openai', function (request, response) { | |||
| 120 | return response.sendStatus(400); | 119 | return response.sendStatus(400); |
| 121 | } | 120 | } |
| 122 | 121 | ||
| 123 | const name = request.body.name; | 122 | const name = sanitize(request.body.name); |
| 123 | if (!name) return response.sendStatus(400); | ||
| 124 | const pathToFile = path.join(request.user.directories.openAI_Settings, `${name}.json`); | 124 | const pathToFile = path.join(request.user.directories.openAI_Settings, `${name}.json`); |
| 125 | 125 | ||
| 126 | if (fs.existsSync(pathToFile)) { | 126 | if (fs.existsSync(pathToFile)) { |
| @@ -12,7 +12,7 @@ router.post('/save', (request, response) => { | |||
| 12 | return response.sendStatus(400); | 12 | return response.sendStatus(400); |
| 13 | } | 13 | } |
| 14 | 14 | ||
| 15 | const filename = path.join(request.user.directories.quickreplies, sanitize(request.body.name) + '.json'); | 15 | const filename = path.join(request.user.directories.quickreplies, sanitize(`${request.body.name}.json`)); |
| 16 | writeFileAtomicSync(filename, JSON.stringify(request.body, null, 4), 'utf8'); | 16 | writeFileAtomicSync(filename, JSON.stringify(request.body, null, 4), 'utf8'); |
| 17 | 17 | ||
| 18 | return response.sendStatus(200); | 18 | return response.sendStatus(200); |
| @@ -23,7 +23,7 @@ router.post('/delete', (request, response) => { | |||
| 23 | return response.sendStatus(400); | 23 | return response.sendStatus(400); |
| 24 | } | 24 | } |
| 25 | 25 | ||
| 26 | const filename = path.join(request.user.directories.quickreplies, sanitize(request.body.name) + '.json'); | 26 | const filename = path.join(request.user.directories.quickreplies, sanitize(`${request.body.name}.json`)); |
| 27 | if (fs.existsSync(filename)) { | 27 | if (fs.existsSync(filename)) { |
| 28 | fs.unlinkSync(filename); | 28 | fs.unlinkSync(filename); |
| 29 | } | 29 | } |
| @@ -12,19 +12,19 @@ router.post('/save', (request, response) => { | |||
| 12 | return response.sendStatus(400); | 12 | return response.sendStatus(400); |
| 13 | } | 13 | } |
| 14 | 14 | ||
| 15 | const filename = path.join(request.user.directories.themes, sanitize(request.body.name) + '.json'); | 15 | const filename = path.join(request.user.directories.themes, sanitize(`${request.body.name}.json`)); |
| 16 | writeFileAtomicSync(filename, JSON.stringify(request.body, null, 4), 'utf8'); | 16 | writeFileAtomicSync(filename, JSON.stringify(request.body, null, 4), 'utf8'); |
| 17 | 17 | ||
| 18 | return response.sendStatus(200); | 18 | return response.sendStatus(200); |
| 19 | }); | 19 | }); |
| 20 | 20 | ||
| 21 | router.post('/delete', function (request, response) { | 21 | router.post('/delete', (request, response) => { |
| 22 | if (!request.body || !request.body.name) { | 22 | if (!request.body || !request.body.name) { |
| 23 | return response.sendStatus(400); | 23 | return response.sendStatus(400); |
| 24 | } | 24 | } |
| 25 | 25 | ||
| 26 | try { | 26 | try { |
| 27 | const filename = path.join(request.user.directories.themes, sanitize(request.body.name) + '.json'); | 27 | const filename = path.join(request.user.directories.themes, sanitize(`${request.body.name}.json`)); |
| 28 | if (!fs.existsSync(filename)) { | 28 | if (!fs.existsSync(filename)) { |
| 29 | console.error('Theme file not found:', filename); | 29 | console.error('Theme file not found:', filename); |
| 30 | return response.sendStatus(404); | 30 | return response.sendStatus(404); |
| @@ -83,7 +83,7 @@ export function invalidateThumbnail(directories, type, file) { | |||
| 83 | const folder = getThumbnailFolder(directories, type); | 83 | const folder = getThumbnailFolder(directories, type); |
| 84 | if (folder === undefined) throw new Error('Invalid thumbnail type'); | 84 | if (folder === undefined) throw new Error('Invalid thumbnail type'); |
| 85 | 85 | ||
| 86 | const pathToThumbnail = path.join(folder, file); | 86 | const pathToThumbnail = path.join(folder, sanitize(file)); |
| 87 | 87 | ||
| 88 | if (fs.existsSync(pathToThumbnail)) { | 88 | if (fs.existsSync(pathToThumbnail)) { |
| 89 | fs.unlinkSync(pathToThumbnail); | 89 | fs.unlinkSync(pathToThumbnail); |
| @@ -19,7 +19,7 @@ export function readWorldInfoFile(directories, worldInfoName, allowDummy) { | |||
| 19 | return dummyObject; | 19 | return dummyObject; |
| 20 | } | 20 | } |
| 21 | 21 | ||
| 22 | const filename = `${worldInfoName}.json`; | 22 | const filename = sanitize(`${worldInfoName}.json`); |
| 23 | const pathToWorldInfo = path.join(directories.worlds, filename); | 23 | const pathToWorldInfo = path.join(directories.worlds, filename); |
| 24 | 24 | ||
| 25 | if (!fs.existsSync(pathToWorldInfo)) { | 25 | if (!fs.existsSync(pathToWorldInfo)) { |
| @@ -114,7 +114,7 @@ router.post('/edit', (request, response) => { | |||
| 114 | return response.status(400).send('Is not a valid world info file'); | 114 | return response.status(400).send('Is not a valid world info file'); |
| 115 | } | 115 | } |
| 116 | 116 | ||
| 117 | const filename = `${sanitize(request.body.name)}.json`; | 117 | const filename = sanitize(`${request.body.name}.json`); |
| 118 | const pathToFile = path.join(request.user.directories.worlds, filename); | 118 | const pathToFile = path.join(request.user.directories.worlds, filename); |
| 119 | 119 | ||
| 120 | writeFileAtomicSync(pathToFile, JSON.stringify(request.body.data, null, 4)); | 120 | writeFileAtomicSync(pathToFile, JSON.stringify(request.body.data, null, 4)); |
| @@ -13,6 +13,7 @@ import mime from 'mime-types'; | |||
| 13 | import archiver from 'archiver'; | 13 | import archiver from 'archiver'; |
| 14 | import _ from 'lodash'; | 14 | import _ from 'lodash'; |
| 15 | import { sync as writeFileAtomicSync } from 'write-file-atomic'; | 15 | import { sync as writeFileAtomicSync } from 'write-file-atomic'; |
| 16 | import sanitize from 'sanitize-filename'; | ||
| 16 | 17 | ||
| 17 | import { USER_DIRECTORY_TEMPLATE, DEFAULT_USER, PUBLIC_DIRECTORIES, SETTINGS_FILE, UPLOADS_DIRECTORY } from './constants.js'; | 18 | import { USER_DIRECTORY_TEMPLATE, DEFAULT_USER, PUBLIC_DIRECTORIES, SETTINGS_FILE, UPLOADS_DIRECTORY } from './constants.js'; |
| 18 | import { getConfigValue, color, delay, generateTimestamp } from './util.js'; | 19 | import { getConfigValue, color, delay, generateTimestamp } from './util.js'; |
| @@ -670,7 +671,7 @@ export async function getUserAvatar(handle) { | |||
| 670 | if (!avatarFile) { | 671 | if (!avatarFile) { |
| 671 | return PUBLIC_USER_AVATAR; | 672 | return PUBLIC_USER_AVATAR; |
| 672 | } | 673 | } |
| 673 | const avatarPath = path.join(directory.avatars, avatarFile); | 674 | const avatarPath = path.join(directory.avatars, sanitize(avatarFile)); |
| 674 | if (!fs.existsSync(avatarPath)) { | 675 | if (!fs.existsSync(avatarPath)) { |
| 675 | return PUBLIC_USER_AVATAR; | 676 | return PUBLIC_USER_AVATAR; |
| 676 | } | 677 | } |