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
Signed| @@ -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 | } |
| 7961 | 7966 | ||
| 7962 | firstRun = !!settings.firstRun; | 7967 | firstRun = !!settings.firstRun; |
| @@ -65,14 +65,20 @@ async function checkIfRepoIsUpToDate(extensionPath) { | |||
| 65 | 65 | ||
| 66 | export const router = express.Router(); | 66 | export const router = express.Router(); |
| 67 | 67 | ||
| 68 | // Feature flag guard: don't allow calling any of the endpoints if extensions are disabled | 68 | /** |
| 69 | router.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 | */ | ||
| 72 | export 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 | |||
| 81 | router.use(extensionsEnabledFeatureGuard); | ||
| 76 | 82 | ||
| 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 | } |
| 120 | 126 | ||
| 121 | const extensionPath = path.join(basePath, extensionNameSanitized); | 127 | const extensionPath = path.join(basePath, extensionNameSanitized); |
| 128 | const folderName = path.basename(extensionPath); | ||
| 122 | 129 | ||
| 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`); |
| 133 | 140 | ||
| 134 | const { version, author, display_name } = await getManifest(extensionPath); | 141 | try { |
| 135 | const folderName = path.basename(extensionPath); | 142 | const manifest = await getManifest(extensionPath); |
| 136 | 143 | if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) { | |
| 144 | throw new Error('Manifest is not a valid JSON object.'); | ||
| 145 | } | ||
| 146 | const { version, author, display_name } = manifest; | ||
| 137 | return response.send({ version, author, display_name, extensionPath, folderName }); | 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.'); |
| @@ -22,6 +22,7 @@ import { allowKeysExposure, readSecret, writeSecret, SECRETS_FILE } from './endp | |||
| 22 | import { getContentOfType } from './endpoints/content-manager.js'; | 22 | import { getContentOfType } from './endpoints/content-manager.js'; |
| 23 | import { serverDirectory } from './server-directory.js'; | 23 | import { serverDirectory } from './server-directory.js'; |
| 24 | import { filterValidIpPatterns, getIpFromRequest } from './express-common.js'; | 24 | import { filterValidIpPatterns, getIpFromRequest } from './express-common.js'; |
| 25 | import { extensionsEnabledFeatureGuard } from './endpoints/extensions.js'; | ||
| 25 | 26 | ||
| 26 | export const KEY_PREFIX = 'user:'; | 27 | export const KEY_PREFIX = 'user:'; |
| 27 | const AVATAR_PREFIX = 'avatar:'; | 28 | const AVATAR_PREFIX = 'avatar:'; |
| @@ -1215,4 +1216,4 @@ router.use('/User%20Avatars/*', createRouteHandler(req => req.user.directories.a | |||
| 1215 | router.use('/assets/*', createRouteHandler(req => req.user.directories.assets)); | 1216 | router.use('/assets/*', createRouteHandler(req => req.user.directories.assets)); |
| 1216 | router.use('/user/images/*', createRouteHandler(req => req.user.directories.userImages)); | 1217 | router.use('/user/images/*', createRouteHandler(req => req.user.directories.userImages)); |
| 1217 | router.use('/user/files/*', createRouteHandler(req => req.user.directories.files)); | 1218 | router.use('/user/files/*', createRouteHandler(req => req.user.directories.files)); |
| 1218 | router.use('/scripts/extensions/third-party/*', createExtensionsRouteHandler(req => req.user.directories.extensions)); | 1219 | router.use('/scripts/extensions/third-party/*', extensionsEnabledFeatureGuard, createExtensionsRouteHandler(req => req.user.directories.extensions)); |