Extension clone improvements (part 2) (#5571) * fix: remove the cloned directory if it contains no manifest * fix: apply feature flag guard to user extension data hosting * fix: disable inactive controls when feature flag is off * fix: change response status to 404

3eb38615960c971b540c2c5e41337eb020bc2c47

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
3 files changed, +29 -9Ignore whitespace
public/script.js+5 -0
@@ -7957,6 +7957,11 @@ export async function getSettings(initLoaderHandle = null) {
7957 Object.assign(extension_settings, (settings.extension_settings ?? {}));7957 Object.assign(extension_settings, (settings.extension_settings ?? {}));
7958 $('#third_party_extension_button').addClass('disabled');7958 $('#third_party_extension_button').addClass('disabled');
7959 $('#extensions_details').addClass('disabled');7959 $('#extensions_details').addClass('disabled');
7960 $('#extensions_connect').addClass('disabled');
7961 $('#extensions_notify_updates').attr('disabled', 'disabled');
7962 $('#extensions_autoconnect').attr('disabled', 'disabled');
7963 $('#extensions_url').attr('disabled', 'disabled');
7964 $('#extensions_api_key').attr('disabled', 'disabled');
7960 }7965 }
79617966
7962 firstRun = !!settings.firstRun;7967 firstRun = !!settings.firstRun;
src/endpoints/extensions.js+22 -8
@@ -65,14 +65,20 @@ async function checkIfRepoIsUpToDate(extensionPath) {
6565
66export const router = express.Router();66export const router = express.Router();
6767
68// Feature flag guard: don't allow calling any of the endpoints if extensions are disabled68/**
69router.use((_, response, next) => {69 * Feature flag guard: don't allow calling any of the endpoints if extensions are disabled
70 * @type {import('express').RequestHandler}
71 */
72export const extensionsEnabledFeatureGuard = (_, response, next) => {
70 const enabled = !!getConfigValue('extensions.enabled', true, 'boolean');73 const enabled = !!getConfigValue('extensions.enabled', true, 'boolean');
71 if (!enabled) {74 if (!enabled) {
72 return response.status(400).send('Bad Request: Extensions are disabled.');75 response.sendStatus(404);
76 return;
73 }77 }
74 next();78 next();
75});79};
80
81router.use(extensionsEnabledFeatureGuard);
7682
77/**83/**
78 * HTTP POST handler function to clone a git repository from a provided URL, read the extension manifest,84 * HTTP POST handler function to clone a git repository from a provided URL, read the extension manifest,
@@ -119,6 +125,7 @@ router.post('/install', async (request, response) => {
119 }125 }
120126
121 const extensionPath = path.join(basePath, extensionNameSanitized);127 const extensionPath = path.join(basePath, extensionNameSanitized);
128 const folderName = path.basename(extensionPath);
122129
123 if (fs.existsSync(extensionPath)) {130 if (fs.existsSync(extensionPath)) {
124 return response.status(409).send(`Directory already exists at ${extensionPath}`);131 return response.status(409).send(`Directory already exists at ${extensionPath}`);
@@ -131,10 +138,17 @@ router.post('/install', async (request, response) => {
131 await git.clone(parsedUrl.href, extensionPath, cloneOptions);138 await git.clone(parsedUrl.href, extensionPath, cloneOptions);
132 console.info(`Extension has been cloned to ${extensionPath} from ${parsedUrl.href} at ${branch || '(default)'} branch`);139 console.info(`Extension has been cloned to ${extensionPath} from ${parsedUrl.href} at ${branch || '(default)'} branch`);
133140
134 const { version, author, display_name } = await getManifest(extensionPath);141 try {
135 const folderName = path.basename(extensionPath);142 const manifest = await getManifest(extensionPath);
136143 if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
137 return response.send({ version, author, display_name, extensionPath, folderName });144 throw new Error('Manifest is not a valid JSON object.');
145 }
146 const { version, author, display_name } = manifest;
147 return response.send({ version, author, display_name, extensionPath, folderName });
148 } catch (manifestError) {
149 await fs.promises.rm(extensionPath, { recursive: true, force: true });
150 throw manifestError;
151 }
138 } catch (error) {152 } catch (error) {
139 console.error('Importing extension failed', error);153 console.error('Importing extension failed', error);
140 return response.status(500).send('Internal Server Error. Check the server logs for more details.');154 return response.status(500).send('Internal Server Error. Check the server logs for more details.');
src/users.js+2 -1
@@ -22,6 +22,7 @@ import { allowKeysExposure, readSecret, writeSecret, SECRETS_FILE } from './endp
22import { getContentOfType } from './endpoints/content-manager.js';22import { getContentOfType } from './endpoints/content-manager.js';
23import { serverDirectory } from './server-directory.js';23import { serverDirectory } from './server-directory.js';
24import { filterValidIpPatterns, getIpFromRequest } from './express-common.js';24import { filterValidIpPatterns, getIpFromRequest } from './express-common.js';
25import { extensionsEnabledFeatureGuard } from './endpoints/extensions.js';
2526
26export const KEY_PREFIX = 'user:';27export const KEY_PREFIX = 'user:';
27const AVATAR_PREFIX = 'avatar:';28const AVATAR_PREFIX = 'avatar:';
@@ -1215,4 +1216,4 @@ router.use('/User%20Avatars/*', createRouteHandler(req => req.user.directories.a
1215router.use('/assets/*', createRouteHandler(req => req.user.directories.assets));1216router.use('/assets/*', createRouteHandler(req => req.user.directories.assets));
1216router.use('/user/images/*', createRouteHandler(req => req.user.directories.userImages));1217router.use('/user/images/*', createRouteHandler(req => req.user.directories.userImages));
1217router.use('/user/files/*', createRouteHandler(req => req.user.directories.files));1218router.use('/user/files/*', createRouteHandler(req => req.user.directories.files));
1218router.use('/scripts/extensions/third-party/*', createExtensionsRouteHandler(req => req.user.directories.extensions));1219router.use('/scripts/extensions/third-party/*', extensionsEnabledFeatureGuard, createExtensionsRouteHandler(req => req.user.directories.extensions));