Extension management improvements (#5552)

* feat: enhance asset management with extension categories

Co-authored-by: Copilot <copilot@github.com>

* fix: enhance extension name validation in server endpoints

* feat: display extension author in the extensions list

* fix: unify server error response format

Co-authored-by: Copilot <copilot@github.com>

* feat: add splash on installing third-party for the first time

* fix: add URL format validation, unify validation error messages

Co-authored-by: Copilot <copilot@github.com>

* fix: apply object freeze to EMPTY_AUTHOR value

Co-authored-by: Copilot <copilot@github.com>

* fix: typecheck extensionName in API requests

Co-authored-by: Copilot <copilot@github.com>

* feat: add feature flag guard to extensions endpoints

Co-authored-by: Copilot <copilot@github.com>

* fix: parse URL before checking

Co-authored-by: Copilot <copilot@github.com>

* fix: use case insensitive regex check

* fix: make debug log more useful

Co-authored-by: Copilot <copilot@github.com>

* fix: add pre-validation of URL format and protocol

Co-authored-by: Copilot <copilot@github.com>

* fix: leaner installation success toast

* fix: settings data loss when extensions are disabled

* fix: don't try to auto-focus elements that don't exist

Co-authored-by: Copilot <copilot@github.com>

* fix: set Popup.defaultResult to negative

Co-authored-by: Copilot <copilot@github.com>

* revert: restore undefined default result

---------

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Cohee
2026-04-30 23:31:50 +03:00
committed by GitHub
parent 45f2951854
commit 5512473b29
9 changed files with 304 additions and 96 deletions
+12 -2
View File
@@ -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;
}
}
+4
View File
@@ -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;
+108 -3
View File
@@ -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}
<span class="${isActive ? 'extension_enabled' : isDisabled ? 'extension_disabled' : 'extension_missing'}">
<span class="extension_name">${DOMPurify.sanitize(displayName)}</span>
<span class="extension_author"></span>
<span class="extension_version">${DOMPurify.sanitize(displayVersion)}</span>
${modulesInfo}
</span>
@@ -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<void>}
* @param {string} [branch] Optional branch to install, if not provided the default branch will be used
* @returns {Promise<boolean>} 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');
+30 -38
View File
@@ -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);
@@ -1,4 +1,22 @@
<div class="assets-list-git">
<span data-i18n="extension_install_1">To download extensions from this page, you need to have </span><a href="https://git-scm.com/downloads" target="_blank">Git</a><span data-i18n="extension_install_2"> installed.</span><br>
<span data-i18n="extension_install_3">Click the </span><i class="fa-solid fa-sm fa-arrow-up-right-from-square"></i><span data-i18n="extension_install_4"> icon to visit the Extension's repo for tips on how to use it.</span>
</div>
</div>
<div class="assets-list-extensions-official">
<h2 data-i18n="Official Extensions">Official Extensions</h2>
<div class="info-block hint">
<small class="assets-list-description" data-i18n="These extensions are maintained by the SillyTavern team.">
These extensions are maintained by the SillyTavern team.
</small>
</div>
<div class="assets-list-extensions"></div>
</div>
<div class="assets-list-extensions-community">
<h2 data-i18n="Community Extensions">Community Extensions</h2>
<div class="info-block warning">
<small data-i18n="Community extensions are not reviewed or verified by the SillyTavern team. Please exercise caution when installing.">
Community extensions are not reviewed or verified by the SillyTavern team. Please exercise caution when installing.
</small>
</div>
<div class="assets-list-extensions"></div>
</div>
+16 -3
View File
@@ -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;
}
+4
View File
@@ -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
@@ -0,0 +1,18 @@
<p>
<em data-i18n="The URL you provided does not seem to be an official SillyTavern extension repository.">
The URL you provided does not seem to be an official SillyTavern extension repository.
</em>
</p>
<p class="info-block error">
<span data-i18n="Using third-party extensions can have unintended side effects and may pose security risks.">
Using third-party extensions can have unintended side effects and may pose security risks.
</span>
<span data-i18n="Always make sure you trust the source before importing an extension. We are not responsible for any damage caused by third-party extensions.">
Always make sure you trust the source before importing an extension. We are not responsible for any damage caused by third-party extensions.
</span>
</p>
<p>
<b data-i18n="Are you sure you want to proceed?">
Are you sure you want to proceed?
</b>
</p>
+93 -49
View File
@@ -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.');
}
});