diff --git a/public/css/extensions-panel.css b/public/css/extensions-panel.css index ef662fdfe..528fd11ae 100644 --- a/public/css/extensions-panel.css +++ b/public/css/extensions-panel.css @@ -97,13 +97,23 @@ label[for="extensions_autoconnect"] { font-size: 1.05em; } -.extensions_info .extension_version { +.extensions_info :is(.extension_version, .extension_author) { opacity: 0.8; font-size: 0.8em; font-weight: normal; margin-left: 2px; } +.extensions_info :is(.extension_version, .extension_author):empty { + display: none; +} + +.extensions_info .extension_author { + display: inline-flex; + gap: 2px; + align-items: baseline; +} + .extensions_info .extension_block a { color: var(--SmartThemeBodyColor); } @@ -161,4 +171,4 @@ input.extension_missing[type="checkbox"] { z-index: 1; margin-bottom: 10px; padding: 5px; -} \ No newline at end of file +} diff --git a/public/script.js b/public/script.js index 772db9055..90d2039d7 100644 --- a/public/script.js +++ b/public/script.js @@ -7953,6 +7953,10 @@ export async function getSettings(initLoaderHandle = null) { const isVersionChanged = settings.currentVersion !== currentVersion; await loadExtensionSettings(settings, isVersionChanged, enableAutoUpdate); await eventSource.emit(event_types.EXTENSION_SETTINGS_LOADED); + } else { + Object.assign(extension_settings, (settings.extension_settings ?? {})); + $('#third_party_extension_button').addClass('disabled'); + $('#extensions_details').addClass('disabled'); } firstRun = !!settings.firstRun; diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js index afdbba204..3b8369b48 100644 --- a/public/scripts/extensions.js +++ b/public/scripts/extensions.js @@ -61,6 +61,19 @@ let manifests = {}; */ const defaultUrl = 'http://localhost:5100'; +/** + * Checks if the extension is officially supported by its URL pattern. + * @param {string} url URL to check + * @returns {boolean} True if the URL matches the pattern, false otherwise (or not a valid URL) + */ +export const isOfficialExtension = (url) => { + try { + return /^https:\/\/github\.com\/SillyTavern\/(.+)$/i.test(new URL(url).href); + } catch (e) { + return false; + } +}; + let requiresReload = false; let stateChanged = false; let saveMetadataTimeout = null; @@ -928,6 +941,7 @@ function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal, ${originHtml} ${DOMPurify.sanitize(displayName)} + ${DOMPurify.sanitize(displayVersion)} ${modulesInfo} @@ -1557,9 +1571,53 @@ async function switchExtensionBranch(extensionName, isGlobal, branch) { * Installs a third-party extension via the API. * @param {string} url Extension repository URL * @param {boolean} global Is the extension global? - * @returns {Promise} + * @param {string} [branch] Optional branch to install, if not provided the default branch will be used + * @returns {Promise} True if the extension was installed successfully, false otherwise */ export async function installExtension(url, global, branch = '') { + try { + const parsedUrl = new URL(url); + if (!['http:', 'https:'].includes(parsedUrl.protocol)) { + throw new Error('Invalid URL protocol'); + } + + // Normalize the URL (resolve relative paths, remove redundant segments, etc.) + url = parsedUrl.href; + } catch (error) { + console.error('Invalid URL:', error); + toastr.error(t`Only valid HTTP and HTTPS URLs are allowed.`, t`Invalid URL`); + return false; + } + + if (!isOfficialExtension(url)) { + const extensionInstallationWarningKey = 'extensionInstallationWarningShown'; + if (accountStorage.getItem(extensionInstallationWarningKey)) { + console.debug('Bypassed URL check for third-party extension (account preference).', url); + } else { + let dismissWarning = false; + const confirmation = await Popup.show.confirm( + t`Install a third-party extension?`, + await renderTemplateAsync('thirdPartyExtensionWarning'), + { + customInputs: [{ id: 'dontAskAgain', type: 'checkbox', label: t`Don't show this warning again`, defaultState: false }], + onClose: (popup) => { + if (!popup.result) { + return; + } + dismissWarning = Boolean(popup.inputResults?.get('dontAskAgain') ?? false); + }, + okButton: t`Yes, install it`, + cancelButton: t`No, cancel`, + }); + if (!confirmation) { + return false; + } + if (dismissWarning) { + accountStorage.setItem(extensionInstallationWarningKey, '1'); + } + } + } + console.debug('Extension installation started', url); toastr.info(t`Please wait...`, t`Installing extension`); @@ -1578,11 +1636,11 @@ export async function installExtension(url, global, branch = '') { const text = await request.text(); toastr.warning(text || request.statusText, t`Extension installation failed`, { timeOut: 5000 }); console.error('Extension installation failed', request.status, request.statusText, text); - return; + return false; } const response = await request.json(); - toastr.success(t`Extension '${response.display_name}' by ${response.author} (version ${response.version}) has been installed successfully!`, t`Extension installation successful`); + toastr.success(t`Extension '${response.display_name}' has been installed successfully!`, t`Extension installation successful`); console.debug(`Extension "${response.display_name}" has been installed successfully at ${response.extensionPath}`); await loadExtensionSettings({}, false, false); await eventSource.emit(event_types.EXTENSION_SETTINGS_LOADED, response); @@ -1591,6 +1649,8 @@ export async function installExtension(url, global, branch = '') { const extensionName = `third-party/${response.folderName}`; await callExtensionHook(extensionName, 'install'); } + + return true; } /** @@ -1701,6 +1761,18 @@ async function checkForUpdatesManual(sortFn, abortSignal) { } } + const authorElement = extensionBlock.querySelector('.extension_author'); + if (authorElement) { + const author = getAuthorFromUrl(origin) || EMPTY_AUTHOR; + if (author.name) { + const icon = document.createElement('i'); + icon.classList.add('fa-solid', 'fa-at', 'fa-xs'); + const name = document.createElement('span'); + name.textContent = author.name; + authorElement.append(icon, name); + } + } + const versionElement = extensionBlock.querySelector('.extension_version'); if (versionElement) { versionElement.textContent += ` (${branch}-${commitHash.substring(0, 7)})`; @@ -2057,6 +2129,39 @@ export async function openThirdPartyExtensionMenu(suggestUrl = '') { await installExtension(url, global, branchName); } +/** + * Sentinel value representing an empty author, used when author information cannot be extracted from a URL. + * @type {{name: string, url: string}} + */ +export const EMPTY_AUTHOR = Object.freeze({ + name: '', + url: '', +}); + +/** + * Extracts the repository author from a given URL. + * @param {string} url - The URL of the repository. + * @returns {{name: string, url: string}} Object containing the author's name and URL, or empty strings if not found. + */ +export function getAuthorFromUrl(url) { + const result = structuredClone(EMPTY_AUTHOR); + + try { + const parsedUrl = new URL(url); + const pathSegments = parsedUrl.pathname.split('/').filter(s => s.length > 0); + + // TODO: Handle non-GitHub URLs if needed + if (parsedUrl.host === 'github.com' && pathSegments.length >= 2) { + result.name = pathSegments[0]; + result.url = `${parsedUrl.protocol}//${parsedUrl.hostname}/${result.name}`; + } + } catch (error) { + console.debug('Error parsing URL:', error); + } + + return result; +} + export async function initExtensions() { await addExtensionsButtonAndMenu(); $('#extensionsMenuButton').css('display', 'flex'); diff --git a/public/scripts/extensions/assets/index.js b/public/scripts/extensions/assets/index.js index 965d59199..09d791e6e 100644 --- a/public/scripts/extensions/assets/index.js +++ b/public/scripts/extensions/assets/index.js @@ -5,7 +5,7 @@ TODO: import { DOMPurify } from '../../../lib.js'; import { getRequestHeaders, processDroppedFiles, eventSource, event_types } from '../../../script.js'; -import { deleteExtension, extensionNames, getContext, installExtension, renderExtensionTemplateAsync } from '../../extensions.js'; +import { deleteExtension, EMPTY_AUTHOR, extensionNames, getAuthorFromUrl, getContext, installExtension, renderExtensionTemplateAsync, isOfficialExtension } from '../../extensions.js'; import { POPUP_TYPE, Popup, callGenericPopup } from '../../popup.js'; import { executeSlashCommandsWithOptions } from '../../slash-commands.js'; import { accountStorage } from '../../util/AccountStorage.js'; @@ -60,35 +60,6 @@ const KNOWN_TYPES = { 'blip': t`Blip sounds`, }; -const EMPTY_AUTHOR = { - name: '', - url: '', -}; - -/** - * Extracts the repository author from a given URL. - * @param {string} url - The URL of the repository. - * @returns {{name: string, url: string}} Object containing the author's name and URL, or empty strings if not found. - */ -function getAuthorFromUrl(url) { - const result = structuredClone(EMPTY_AUTHOR); - - try { - const parsedUrl = new URL(url); - const pathSegments = parsedUrl.pathname.split('/').filter(s => s.length > 0); - - // TODO: Handle non-GitHub URLs if needed - if (parsedUrl.host === 'github.com' && pathSegments.length >= 2) { - result.name = pathSegments[0]; - result.url = `${parsedUrl.protocol}//${parsedUrl.hostname}/${result.name}`; - } - } catch (error) { - console.debug(DEBUG_PREFIX, 'Error parsing URL:', error); - } - - return result; -} - async function downloadAssetsList(url) { updateCurrentAssets().then(async function () { fetch(url, { cache: 'no-cache' }) @@ -153,7 +124,15 @@ async function downloadAssetsList(url) { element.off('click'); label.removeClass('fa-download'); this.classList.add('asset-download-button-loading'); - await installAsset(asset.url, assetType, asset.id); + const result = await installAsset(asset.url, assetType, asset.id); + if (!result) { + this.classList.remove('asset-download-button-loading'); + label.addClass('fa-download'); + label.removeClass('fa-spinner'); + label.removeClass('fa-spin'); + element.on('click', assetInstall); + return; + } label.addClass('fa-check'); this.classList.remove('asset-download-button-loading'); element.on('click', assetDelete); @@ -248,7 +227,14 @@ async function downloadAssetsList(url) { assetBlock.addClass('asset-block'); - assetTypeMenu.append(assetBlock); + if (assetType === 'extension') { + const extensionBlockList = isOfficialExtension(asset.url) + ? assetTypeMenu.find('.assets-list-extensions-official .assets-list-extensions') + : assetTypeMenu.find('.assets-list-extensions-community .assets-list-extensions'); + extensionBlockList.append(assetBlock); + } else { + assetTypeMenu.append(assetBlock); + } } assetTypeMenu.appendTo('#assets_menu'); assetTypeMenu.on('click', 'a.asset_preview', previewAsset); @@ -322,9 +308,9 @@ async function installAsset(url, assetType, filename) { try { if (category === 'extension') { console.debug(DEBUG_PREFIX, 'Installing extension ', url); - await installExtension(url, false); + const result = await installExtension(url, false); console.debug(DEBUG_PREFIX, 'Extension installed.'); - return; + return result; } const body = { url, category, filename }; @@ -343,10 +329,12 @@ async function installAsset(url, assetType, filename) { await processDroppedFiles([file]); console.debug(DEBUG_PREFIX, 'Character downloaded.'); } + return true; } + return false; } catch (err) { console.log(err); - return []; + return false; } } @@ -398,9 +386,13 @@ async function openCharacterBrowser(forceDefault) { downloadButton.toggle(!isInstalled).on('click', async () => { downloadButton.toggleClass('fa-download fa-spinner fa-spin'); - await installAsset(character.url, 'character', character.id); - downloadButton.hide(); - checkMark.show(); + const result = await installAsset(character.url, 'character', character.id); + if (result) { + downloadButton.hide(); + checkMark.show(); + } else { + downloadButton.toggleClass('fa-download fa-spinner fa-spin'); + } }); checkMark.toggle(isInstalled); diff --git a/public/scripts/extensions/assets/installation.html b/public/scripts/extensions/assets/installation.html index 33e1f644c..56eb8e305 100644 --- a/public/scripts/extensions/assets/installation.html +++ b/public/scripts/extensions/assets/installation.html @@ -1,4 +1,22 @@
To download extensions from this page, you need to have Git installed.
Click the icon to visit the Extension's repo for tips on how to use it. -
\ No newline at end of file + +
+

Official Extensions

+
+ + These extensions are maintained by the SillyTavern team. + +
+
+
+
+

Community Extensions

+
+ + Community extensions are not reviewed or verified by the SillyTavern team. Please exercise caution when installing. + +
+
+
diff --git a/public/scripts/extensions/assets/style.css b/public/scripts/extensions/assets/style.css index ce46ee8bc..40cc0755f 100644 --- a/public/scripts/extensions/assets/style.css +++ b/public/scripts/extensions/assets/style.css @@ -27,15 +27,20 @@ margin-bottom: 0.25em; } +.assets-list-div h2 { + margin: 0; + font-size: 1.1em; +} + .assets-list-div h3 { text-transform: capitalize; } -.assets-list-div i a { +.assets-list-div .asset-block a { color: inherit; } -.assets-list-div>i { +.assets-list-div .asset-block { display: flex; flex-direction: row; align-items: center; @@ -46,7 +51,7 @@ border-bottom: 1px solid var(--SmartThemeBorderColor); } -.assets-list-div i span:first-of-type { +.assets-list-div .asset-block span:first-of-type { font-weight: bold; } @@ -198,3 +203,11 @@ .asset-name>b { font-weight: 600; } + +div:is(.assets-list-extensions-official, .assets-list-extensions-community):has(.assets-list-extensions:empty) { + display: none; +} + +div:is(.assets-list-extensions-official, .assets-list-extensions-community) { + margin-top: 10px; +} diff --git a/public/scripts/popup.js b/public/scripts/popup.js index cb00a7496..1e72559ac 100644 --- a/public/scripts/popup.js +++ b/public/scripts/popup.js @@ -718,6 +718,10 @@ export class Popup { } } + if (!control) { + return; + } + if (applyAutoFocus) { control.setAttribute('autofocus', ''); // Manually enable tabindex too, as this might only be applied by the interactable functionality in the background, but too late for HTML autofocus diff --git a/public/scripts/templates/thirdPartyExtensionWarning.html b/public/scripts/templates/thirdPartyExtensionWarning.html new file mode 100644 index 000000000..0b5979aa3 --- /dev/null +++ b/public/scripts/templates/thirdPartyExtensionWarning.html @@ -0,0 +1,18 @@ +

+ + The URL you provided does not seem to be an official SillyTavern extension repository. + +

+

+ + Using third-party extensions can have unintended side effects and may pose security risks. + + + Always make sure you trust the source before importing an extension. We are not responsible for any damage caused by third-party extensions. + +

+

+ + Are you sure you want to proceed? + +

diff --git a/src/endpoints/extensions.js b/src/endpoints/extensions.js index 0bfa72178..fe4cbc1f6 100644 --- a/src/endpoints/extensions.js +++ b/src/endpoints/extensions.js @@ -6,7 +6,7 @@ import sanitize from 'sanitize-filename'; import { CheckRepoActions, default as simpleGit } from 'simple-git'; import { PUBLIC_DIRECTORIES } from '../constants.js'; -import { getConfigValue } from '../util.js'; +import { getConfigValue, isValidUrl } from '../util.js'; import { createGitClient } from '../git/client.js'; const gitBackend = getConfigValue('git.backend', 'auto'); @@ -65,6 +65,15 @@ async function checkIfRepoIsUpToDate(extensionPath) { export const router = express.Router(); +// Feature flag guard: don't allow calling any of the endpoints if extensions are disabled +router.use((_, response, next) => { + const enabled = !!getConfigValue('extensions.enabled', true, 'boolean'); + if (!enabled) { + return response.status(400).send('Bad Request: Extensions are disabled.'); + } + next(); +}); + /** * HTTP POST handler function to clone a git repository from a provided URL, read the extension manifest, * and return extension information and path. @@ -75,11 +84,23 @@ export const router = express.Router(); * @returns {void} */ router.post('/install', async (request, response) => { - if (!request.body.url) { - return response.status(400).send('Bad Request: URL is required in the request body.'); - } - try { + const { url, global, branch } = request.body; + + if (global && !request.user.profile.admin) { + console.error(`User ${request.user.profile.handle} does not have permission to install global extensions.`); + return response.status(403).send('Forbidden: No permission to install global extensions.'); + } + + if (!isValidUrl(url)) { + return response.status(400).send('Bad Request: A valid URL is required in the request body.'); + } + + const parsedUrl = new URL(url); + if (!['http:', 'https:'].includes(parsedUrl.protocol)) { + return response.status(400).send('Bad Request: Only HTTP and HTTPS protocols are supported for the Extension URL.'); + } + const git = createGitClient({ backend: gitBackend }); // make sure the third-party directory exists @@ -91,15 +112,13 @@ router.post('/install', async (request, response) => { fs.mkdirSync(PUBLIC_DIRECTORIES.globalExtensions); } - const { url, global, branch } = request.body; - - if (global && !request.user.profile.admin) { - console.error(`User ${request.user.profile.handle} does not have permission to install global extensions.`); - return response.status(403).send('Forbidden: No permission to install global extensions.'); + const basePath = global ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions; + const extensionNameSanitized = sanitize(path.basename(parsedUrl.pathname, '.git')); + if (!extensionNameSanitized) { + return response.status(400).send('Could not determine the extension name from the URL. Please provide a valid git repository URL.'); } - const basePath = global ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions; - const extensionPath = path.join(basePath, sanitize(path.basename(url, '.git'))); + const extensionPath = path.join(basePath, extensionNameSanitized); if (fs.existsSync(extensionPath)) { return response.status(409).send(`Directory already exists at ${extensionPath}`); @@ -109,16 +128,16 @@ router.post('/install', async (request, response) => { if (branch) { cloneOptions.branch = branch; } - await git.clone(url, extensionPath, cloneOptions); - console.info(`Extension has been cloned to ${extensionPath} from ${url} at ${branch || '(default)'} branch`); + await git.clone(parsedUrl.href, extensionPath, cloneOptions); + console.info(`Extension has been cloned to ${extensionPath} from ${parsedUrl.href} at ${branch || '(default)'} branch`); const { version, author, display_name } = await getManifest(extensionPath); const folderName = path.basename(extensionPath); return response.send({ version, author, display_name, extensionPath, folderName }); } catch (error) { - console.error('Importing custom content failed', error); - return response.status(500).send(`Server Error: ${error.message}`); + console.error('Importing extension failed', error); + return response.status(500).send('Internal Server Error. Check the server logs for more details.'); } }); @@ -134,12 +153,16 @@ router.post('/install', async (request, response) => { * @returns {void} */ router.post('/update', async (request, response) => { - if (!request.body.extensionName) { - return response.status(400).send('Bad Request: extensionName is required in the request body.'); - } - try { + if (typeof request.body.extensionName !== 'string') { + return response.status(400).send('Bad Request: A valid extensionName is required in the request body.'); + } + const { extensionName, global } = request.body; + const extensionNameSanitized = sanitize(extensionName); + if (!extensionNameSanitized) { + return response.status(400).send('Bad Request: A valid extensionName is required in the request body.'); + } if (global && !request.user.profile.admin) { console.error(`User ${request.user.profile.handle} does not have permission to update global extensions.`); @@ -147,7 +170,7 @@ router.post('/update', async (request, response) => { } const basePath = global ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions; - const extensionPath = path.join(basePath, sanitize(extensionName)); + const extensionPath = path.join(basePath, extensionNameSanitized); if (!fs.existsSync(extensionPath)) { return response.status(404).send(`Directory does not exist at ${extensionPath}`); @@ -179,10 +202,14 @@ router.post('/update', async (request, response) => { router.post('/branches', async (request, response) => { try { - const { extensionName, global } = request.body; + if (typeof request.body.extensionName !== 'string') { + return response.status(400).send('Bad Request: A valid extensionName is required in the request body.'); + } - if (!extensionName) { - return response.status(400).send('Bad Request: extensionName is required in the request body.'); + const { extensionName, global } = request.body; + const extensionNameSanitized = sanitize(extensionName); + if (!extensionNameSanitized) { + return response.status(400).send('Bad Request: A valid extensionName is required in the request body.'); } if (global && !request.user.profile.admin) { @@ -191,7 +218,7 @@ router.post('/branches', async (request, response) => { } const basePath = global ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions; - const extensionPath = path.join(basePath, sanitize(extensionName)); + const extensionPath = path.join(basePath, extensionNameSanitized); if (!fs.existsSync(extensionPath)) { return response.status(404).send(`Directory does not exist at ${extensionPath}`); @@ -224,10 +251,14 @@ router.post('/branches', async (request, response) => { router.post('/switch', async (request, response) => { try { - const { extensionName, branch, global } = request.body; + if (typeof request.body.extensionName !== 'string') { + return response.status(400).send('Bad Request: A valid extensionName is required in the request body.'); + } - if (!extensionName || !branch) { - return response.status(400).send('Bad Request: extensionName and branch are required in the request body.'); + const { extensionName, branch, global } = request.body; + const extensionNameSanitized = sanitize(extensionName); + if (!extensionNameSanitized || !branch) { + return response.status(400).send('Bad Request: A valid extensionName and branch are required in the request body.'); } if (global && !request.user.profile.admin) { @@ -236,7 +267,7 @@ router.post('/switch', async (request, response) => { } const basePath = global ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions; - const extensionPath = path.join(basePath, sanitize(extensionName)); + const extensionPath = path.join(basePath, extensionNameSanitized); if (!fs.existsSync(extensionPath)) { return response.status(404).send(`Directory does not exist at ${extensionPath}`); @@ -283,10 +314,14 @@ router.post('/switch', async (request, response) => { router.post('/move', async (request, response) => { try { - const { extensionName, source, destination } = request.body; + if (typeof request.body.extensionName !== 'string') { + return response.status(400).send('Bad Request: A valid extensionName is required in the request body.'); + } - if (!extensionName || !source || !destination) { - return response.status(400).send('Bad Request. Not all required parameters are provided.'); + const { extensionName, source, destination } = request.body; + const extensionNameSanitized = sanitize(extensionName); + if (!extensionNameSanitized || !source || !destination) { + return response.status(400).send('Bad Request: A valid extensionName, source, and destination are required in the request body.'); } if (!request.user.profile.admin) { @@ -296,8 +331,8 @@ router.post('/move', async (request, response) => { const sourceDirectory = source === 'global' ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions; const destinationDirectory = destination === 'global' ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions; - const sourcePath = path.join(sourceDirectory, sanitize(extensionName)); - const destinationPath = path.join(destinationDirectory, sanitize(extensionName)); + const sourcePath = path.join(sourceDirectory, extensionNameSanitized); + const destinationPath = path.join(destinationDirectory, extensionNameSanitized); if (!fs.existsSync(sourcePath) || !fs.statSync(sourcePath).isDirectory()) { console.error(`Source directory does not exist at ${sourcePath}`); @@ -336,14 +371,19 @@ router.post('/move', async (request, response) => { * @returns {void} */ router.post('/version', async (request, response) => { - if (!request.body.extensionName) { - return response.status(400).send('Bad Request: extensionName is required in the request body.'); - } - try { + if (typeof request.body.extensionName !== 'string') { + return response.status(400).send('Bad Request: A valid extensionName is required in the request body.'); + } + const { extensionName, global } = request.body; + const extensionNameSanitized = sanitize(extensionName); + if (!extensionNameSanitized) { + return response.status(400).send('Bad Request: A valid extensionName is required in the request body.'); + } + const basePath = global ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions; - const extensionPath = path.join(basePath, sanitize(extensionName)); + const extensionPath = path.join(basePath, extensionNameSanitized); if (!fs.existsSync(extensionPath)) { return response.status(404).send(`Directory does not exist at ${extensionPath}`); @@ -367,31 +407,35 @@ router.post('/version', async (request, response) => { // get only the working branch const currentBranchName = currentBranch.current; await git.fetch('origin'); - console.debug(extensionName, currentBranchName, currentCommitHash); + console.debug(extensionNameSanitized, currentBranchName, currentCommitHash); const { isUpToDate, remoteUrl } = await checkIfRepoIsUpToDate(extensionPath); return response.send({ currentBranchName, currentCommitHash, isUpToDate, remoteUrl }); } catch (error) { console.error('Getting extension version failed', error); - return response.status(500).send(`Server Error: ${error.message}`); + return response.status(500).send('Internal Server Error. Check the server logs for more details.'); } }); /** * HTTP POST handler function to delete a git repository based on the extension name provided in the request body. * - * @param {Object} request - HTTP Request object, expects a JSON body with a 'url' property. + * @param {Object} request - HTTP Request object, expects a JSON body with a 'extensionName' property. * @param {Object} response - HTTP Response object used to respond to the HTTP request. * * @returns {void} */ router.post('/delete', async (request, response) => { - if (!request.body.extensionName) { - return response.status(400).send('Bad Request: extensionName is required in the request body.'); - } - try { + if (typeof request.body.extensionName !== 'string') { + return response.status(400).send('Bad Request: A valid extensionName is required in the request body.'); + } + const { extensionName, global } = request.body; + const extensionNameSanitized = sanitize(extensionName); + if (!extensionNameSanitized) { + return response.status(400).send('Bad Request: A valid extensionName is required in the request body.'); + } if (global && !request.user.profile.admin) { console.error(`User ${request.user.profile.handle} does not have permission to delete global extensions.`); @@ -399,7 +443,7 @@ router.post('/delete', async (request, response) => { } const basePath = global ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions; - const extensionPath = path.join(basePath, sanitize(extensionName)); + const extensionPath = path.join(basePath, extensionNameSanitized); if (!fs.existsSync(extensionPath)) { return response.status(404).send(`Directory does not exist at ${extensionPath}`); @@ -410,8 +454,8 @@ router.post('/delete', async (request, response) => { return response.send(`Extension has been deleted at ${extensionPath}`); } catch (error) { - console.error('Deleting custom content failed', error); - return response.status(500).send(`Server Error: ${error.message}`); + console.error('Deleting extension failed', error); + return response.status(500).send('Internal Server Error. Check the server logs for more details.'); } });