| 1 | import path from 'node:path'; |
| 2 | import fs from 'node:fs'; |
| 3 | |
| 4 | import express from 'express'; |
| 5 | import sanitize from 'sanitize-filename'; |
| 6 | import { CheckRepoActions, default as simpleGit } from 'simple-git'; |
| 7 | |
| 8 | import { PUBLIC_DIRECTORIES } from '../constants.js'; |
| 9 | import { getConfigValue, isValidUrl } from '../util.js'; |
| 10 | import { createGitClient } from '../git/client.js'; |
| 11 | |
| 12 | const gitBackend = getConfigValue('git.backend', 'auto'); |
| 13 | |
| 14 | /** |
| 15 | * @type {Partial<import('simple-git').SimpleGitOptions>} |
| 16 | */ |
| 17 | const OPTIONS = Object.freeze({ timeout: { block: 5 * 60 * 1000 } }); |
| 18 | |
| 19 | /** |
| 20 | * This function extracts the extension information from the manifest file. |
| 21 | * @param {string} extensionPath - The path of the extension folder |
| 22 | * @returns {Promise<Object>} - Returns the manifest data as an object |
| 23 | */ |
| 24 | async function getManifest(extensionPath) { |
| 25 | const manifestPath = path.join(extensionPath, 'manifest.json'); |
| 26 | |
| 27 | // Check if manifest.json exists |
| 28 | if (!fs.existsSync(manifestPath)) { |
| 29 | throw new Error(`Manifest file not found at ${manifestPath}`); |
| 30 | } |
| 31 | |
| 32 | const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); |
| 33 | return manifest; |
| 34 | } |
| 35 | |
| 36 | /** |
| 37 | * This function checks if the local repository is up-to-date with the remote repository. |
| 38 | * @param {string} extensionPath - The path of the extension folder |
| 39 | * @returns {Promise<Object>} - Returns the extension information as an object |
| 40 | */ |
| 41 | async function checkIfRepoIsUpToDate(extensionPath) { |
| 42 | const git = simpleGit({ baseDir: extensionPath, ...OPTIONS }); |
| 43 | await git.fetch('origin'); |
| 44 | const currentBranch = await git.branch(); |
| 45 | const currentCommitHash = await git.revparse(['HEAD']); |
| 46 | const log = await git.log({ |
| 47 | from: currentCommitHash, |
| 48 | to: `origin/${currentBranch.current}`, |
| 49 | }); |
| 50 | |
| 51 | // Fetch remote repository information |
| 52 | const remotes = await git.getRemotes(true); |
| 53 | if (remotes.length === 0) { |
| 54 | return { |
| 55 | isUpToDate: true, |
| 56 | remoteUrl: '', |
| 57 | }; |
| 58 | } |
| 59 | |
| 60 | return { |
| 61 | isUpToDate: log.total === 0, |
| 62 | remoteUrl: remotes[0].refs.fetch, // URL of the remote repository |
| 63 | }; |
| 64 | } |
| 65 | |
| 66 | export const router = express.Router(); |
| 67 | |
| 68 | /** |
| 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) => { |
| 73 | const enabled = !!getConfigValue('extensions.enabled', true, 'boolean'); |
| 74 | if (!enabled) { |
| 75 | response.sendStatus(404); |
| 76 | return; |
| 77 | } |
| 78 | next(); |
| 79 | }; |
| 80 | |
| 81 | router.use(extensionsEnabledFeatureGuard); |
| 82 | |
| 83 | /** |
| 84 | * HTTP POST handler function to clone a git repository from a provided URL, read the extension manifest, |
| 85 | * and return extension information and path. |
| 86 | * |
| 87 | * @param {Object} request - HTTP Request object, expects a JSON body with a 'url' property. |
| 88 | * @param {Object} response - HTTP Response object used to respond to the HTTP request. |
| 89 | * |
| 90 | * @returns {void} |
| 91 | */ |
| 92 | router.post('/install', async (request, response) => { |
| 93 | try { |
| 94 | const { url, global, branch } = request.body; |
| 95 | |
| 96 | if (global && !request.user.profile.admin) { |
| 97 | console.error(`User ${request.user.profile.handle} does not have permission to install global extensions.`); |
| 98 | return response.status(403).send('Forbidden: No permission to install global extensions.'); |
| 99 | } |
| 100 | |
| 101 | if (!isValidUrl(url)) { |
| 102 | return response.status(400).send('Bad Request: A valid URL is required in the request body.'); |
| 103 | } |
| 104 | |
| 105 | const parsedUrl = new URL(url); |
| 106 | if (!['http:', 'https:'].includes(parsedUrl.protocol)) { |
| 107 | return response.status(400).send('Bad Request: Only HTTP and HTTPS protocols are supported for the Extension URL.'); |
| 108 | } |
| 109 | |
| 110 | const git = createGitClient({ backend: gitBackend }); |
| 111 | |
| 112 | // make sure the third-party directory exists |
| 113 | if (!fs.existsSync(path.join(request.user.directories.extensions))) { |
| 114 | fs.mkdirSync(path.join(request.user.directories.extensions)); |
| 115 | } |
| 116 | |
| 117 | if (!fs.existsSync(PUBLIC_DIRECTORIES.globalExtensions)) { |
| 118 | fs.mkdirSync(PUBLIC_DIRECTORIES.globalExtensions); |
| 119 | } |
| 120 | |
| 121 | const basePath = global ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions; |
| 122 | const extensionNameSanitized = sanitize(path.basename(parsedUrl.pathname, '.git')); |
| 123 | if (!extensionNameSanitized) { |
| 124 | return response.status(400).send('Could not determine the extension name from the URL. Please provide a valid git repository URL.'); |
| 125 | } |
| 126 | |
| 127 | const extensionPath = path.join(basePath, extensionNameSanitized); |
| 128 | const folderName = path.basename(extensionPath); |
| 129 | |
| 130 | if (fs.existsSync(extensionPath)) { |
| 131 | return response.status(409).send(`Directory already exists at ${extensionPath}`); |
| 132 | } |
| 133 | |
| 134 | const cloneOptions = { depth: 1 }; |
| 135 | if (branch) { |
| 136 | cloneOptions.branch = branch; |
| 137 | } |
| 138 | await git.clone(parsedUrl.href, extensionPath, cloneOptions); |
| 139 | console.info(`Extension has been cloned to ${extensionPath} from ${parsedUrl.href} at ${branch || '(default)'} branch`); |
| 140 | |
| 141 | try { |
| 142 | const manifest = await getManifest(extensionPath); |
| 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; |
| 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 | } |
| 152 | } catch (error) { |
| 153 | console.error('Importing extension failed', error); |
| 154 | return response.status(500).send('Internal Server Error. Check the server logs for more details.'); |
| 155 | } |
| 156 | }); |
| 157 | |
| 158 | /** |
| 159 | * HTTP POST handler function to pull the latest updates from a git repository |
| 160 | * based on the extension name provided in the request body. It returns the latest commit hash, |
| 161 | * the path of the extension, the status of the repository (whether it's up-to-date or not), |
| 162 | * and the remote URL of the repository. |
| 163 | * |
| 164 | * @param {Object} request - HTTP Request object, expects a JSON body with an 'extensionName' property. |
| 165 | * @param {Object} response - HTTP Response object used to respond to the HTTP request. |
| 166 | * |
| 167 | * @returns {void} |
| 168 | */ |
| 169 | router.post('/update', async (request, response) => { |
| 170 | try { |
| 171 | if (typeof request.body.extensionName !== 'string') { |
| 172 | return response.status(400).send('Bad Request: A valid extensionName is required in the request body.'); |
| 173 | } |
| 174 | |
| 175 | const { extensionName, global } = request.body; |
| 176 | const extensionNameSanitized = sanitize(extensionName); |
| 177 | if (!extensionNameSanitized) { |
| 178 | return response.status(400).send('Bad Request: A valid extensionName is required in the request body.'); |
| 179 | } |
| 180 | |
| 181 | if (global && !request.user.profile.admin) { |
| 182 | console.error(`User ${request.user.profile.handle} does not have permission to update global extensions.`); |
| 183 | return response.status(403).send('Forbidden: No permission to update global extensions.'); |
| 184 | } |
| 185 | |
| 186 | const basePath = global ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions; |
| 187 | const extensionPath = path.join(basePath, extensionNameSanitized); |
| 188 | |
| 189 | if (!fs.existsSync(extensionPath)) { |
| 190 | return response.status(404).send(`Directory does not exist at ${extensionPath}`); |
| 191 | } |
| 192 | |
| 193 | const { isUpToDate, remoteUrl } = await checkIfRepoIsUpToDate(extensionPath); |
| 194 | const git = simpleGit({ baseDir: extensionPath, ...OPTIONS }); |
| 195 | const isRepo = await git.checkIsRepo(CheckRepoActions.IS_REPO_ROOT); |
| 196 | if (!isRepo) { |
| 197 | throw new Error(`Directory is not a Git repository at ${extensionPath}`); |
| 198 | } |
| 199 | const currentBranch = await git.branch(); |
| 200 | if (!isUpToDate) { |
| 201 | await git.pull('origin', currentBranch.current); |
| 202 | console.info(`Extension has been updated at ${extensionPath}`); |
| 203 | } else { |
| 204 | console.info(`Extension is up to date at ${extensionPath}`); |
| 205 | } |
| 206 | await git.fetch('origin'); |
| 207 | const fullCommitHash = await git.revparse(['HEAD']); |
| 208 | const shortCommitHash = fullCommitHash.slice(0, 7); |
| 209 | |
| 210 | return response.send({ shortCommitHash, extensionPath, isUpToDate, remoteUrl }); |
| 211 | } catch (error) { |
| 212 | console.error('Updating extension failed', error); |
| 213 | return response.status(500).send('Internal Server Error. Check the server logs for more details.'); |
| 214 | } |
| 215 | }); |
| 216 | |
| 217 | router.post('/branches', async (request, response) => { |
| 218 | try { |
| 219 | if (typeof request.body.extensionName !== 'string') { |
| 220 | return response.status(400).send('Bad Request: A valid extensionName is required in the request body.'); |
| 221 | } |
| 222 | |
| 223 | const { extensionName, global } = request.body; |
| 224 | const extensionNameSanitized = sanitize(extensionName); |
| 225 | if (!extensionNameSanitized) { |
| 226 | return response.status(400).send('Bad Request: A valid extensionName is required in the request body.'); |
| 227 | } |
| 228 | |
| 229 | if (global && !request.user.profile.admin) { |
| 230 | console.error(`User ${request.user.profile.handle} does not have permission to list branches of global extensions.`); |
| 231 | return response.status(403).send('Forbidden: No permission to list branches of global extensions.'); |
| 232 | } |
| 233 | |
| 234 | const basePath = global ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions; |
| 235 | const extensionPath = path.join(basePath, extensionNameSanitized); |
| 236 | |
| 237 | if (!fs.existsSync(extensionPath)) { |
| 238 | return response.status(404).send(`Directory does not exist at ${extensionPath}`); |
| 239 | } |
| 240 | |
| 241 | const git = simpleGit({ baseDir: extensionPath, ...OPTIONS }); |
| 242 | // Unshallow the repository if it is shallow |
| 243 | const isShallow = await git.revparse(['--is-shallow-repository']) === 'true'; |
| 244 | if (isShallow) { |
| 245 | console.info(`Unshallowing the repository at ${extensionPath}`); |
| 246 | await git.fetch('origin', ['--unshallow']); |
| 247 | } |
| 248 | |
| 249 | // Fetch all branches |
| 250 | await git.remote(['set-branches', 'origin', '*']); |
| 251 | await git.fetch('origin'); |
| 252 | const localBranches = await git.branchLocal(); |
| 253 | const remoteBranches = await git.branch(['-r', '--list', 'origin/*']); |
| 254 | const result = [ |
| 255 | ...Object.values(localBranches.branches), |
| 256 | ...Object.values(remoteBranches.branches), |
| 257 | ].map(b => ({ current: b.current, commit: b.commit, name: b.name, label: b.label })); |
| 258 | |
| 259 | return response.send(result); |
| 260 | } catch (error) { |
| 261 | console.error('Getting branches failed', error); |
| 262 | return response.status(500).send('Internal Server Error. Check the server logs for more details.'); |
| 263 | } |
| 264 | }); |
| 265 | |
| 266 | router.post('/switch', async (request, response) => { |
| 267 | try { |
| 268 | if (typeof request.body.extensionName !== 'string') { |
| 269 | return response.status(400).send('Bad Request: A valid extensionName is required in the request body.'); |
| 270 | } |
| 271 | |
| 272 | const { extensionName, branch, global } = request.body; |
| 273 | const extensionNameSanitized = sanitize(extensionName); |
| 274 | if (!extensionNameSanitized || !branch) { |
| 275 | return response.status(400).send('Bad Request: A valid extensionName and branch are required in the request body.'); |
| 276 | } |
| 277 | |
| 278 | if (global && !request.user.profile.admin) { |
| 279 | console.error(`User ${request.user.profile.handle} does not have permission to switch branches of global extensions.`); |
| 280 | return response.status(403).send('Forbidden: No permission to switch branches of global extensions.'); |
| 281 | } |
| 282 | |
| 283 | const basePath = global ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions; |
| 284 | const extensionPath = path.join(basePath, extensionNameSanitized); |
| 285 | |
| 286 | if (!fs.existsSync(extensionPath)) { |
| 287 | return response.status(404).send(`Directory does not exist at ${extensionPath}`); |
| 288 | } |
| 289 | |
| 290 | const git = simpleGit({ baseDir: extensionPath, ...OPTIONS }); |
| 291 | const branches = await git.branchLocal(); |
| 292 | |
| 293 | if (String(branch).startsWith('origin/')) { |
| 294 | const localBranch = branch.replace('origin/', ''); |
| 295 | if (branches.all.includes(localBranch)) { |
| 296 | console.info(`Branch ${localBranch} already exists locally, checking it out`); |
| 297 | await git.checkout(localBranch); |
| 298 | return response.sendStatus(204); |
| 299 | } |
| 300 | |
| 301 | console.info(`Branch ${localBranch} does not exist locally, creating it from ${branch}`); |
| 302 | await git.checkoutBranch(localBranch, branch); |
| 303 | return response.sendStatus(204); |
| 304 | } |
| 305 | |
| 306 | if (!branches.all.includes(branch)) { |
| 307 | console.error(`Branch ${branch} does not exist locally`); |
| 308 | return response.status(404).send(`Branch ${branch} does not exist locally`); |
| 309 | } |
| 310 | |
| 311 | // Check if the branch is already checked out |
| 312 | const currentBranch = await git.branch(); |
| 313 | if (currentBranch.current === branch) { |
| 314 | console.info(`Branch ${branch} is already checked out`); |
| 315 | return response.sendStatus(204); |
| 316 | } |
| 317 | |
| 318 | // Checkout the branch |
| 319 | await git.checkout(branch); |
| 320 | console.info(`Checked out branch ${branch} at ${extensionPath}`); |
| 321 | |
| 322 | return response.sendStatus(204); |
| 323 | } catch (error) { |
| 324 | console.error('Switching branches failed', error); |
| 325 | return response.status(500).send('Internal Server Error. Check the server logs for more details.'); |
| 326 | } |
| 327 | }); |
| 328 | |
| 329 | router.post('/move', async (request, response) => { |
| 330 | try { |
| 331 | if (typeof request.body.extensionName !== 'string') { |
| 332 | return response.status(400).send('Bad Request: A valid extensionName is required in the request body.'); |
| 333 | } |
| 334 | |
| 335 | const { extensionName, source, destination } = request.body; |
| 336 | const extensionNameSanitized = sanitize(extensionName); |
| 337 | if (!extensionNameSanitized || !source || !destination) { |
| 338 | return response.status(400).send('Bad Request: A valid extensionName, source, and destination are required in the request body.'); |
| 339 | } |
| 340 | |
| 341 | if (!request.user.profile.admin) { |
| 342 | console.error(`User ${request.user.profile.handle} does not have permission to move extensions.`); |
| 343 | return response.status(403).send('Forbidden: No permission to move extensions.'); |
| 344 | } |
| 345 | |
| 346 | const sourceDirectory = source === 'global' ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions; |
| 347 | const destinationDirectory = destination === 'global' ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions; |
| 348 | const sourcePath = path.join(sourceDirectory, extensionNameSanitized); |
| 349 | const destinationPath = path.join(destinationDirectory, extensionNameSanitized); |
| 350 | |
| 351 | if (!fs.existsSync(sourcePath) || !fs.statSync(sourcePath).isDirectory()) { |
| 352 | console.error(`Source directory does not exist at ${sourcePath}`); |
| 353 | return response.status(404).send('Source directory does not exist.'); |
| 354 | } |
| 355 | |
| 356 | if (fs.existsSync(destinationPath)) { |
| 357 | console.error(`Destination directory already exists at ${destinationPath}`); |
| 358 | return response.status(409).send('Destination directory already exists.'); |
| 359 | } |
| 360 | |
| 361 | if (source === destination) { |
| 362 | console.error('Source and destination directories are the same'); |
| 363 | return response.status(409).send('Source and destination directories are the same.'); |
| 364 | } |
| 365 | |
| 366 | fs.cpSync(sourcePath, destinationPath, { recursive: true, force: true }); |
| 367 | fs.rmSync(sourcePath, { recursive: true, force: true }); |
| 368 | console.info(`Extension has been moved from ${sourcePath} to ${destinationPath}`); |
| 369 | |
| 370 | return response.sendStatus(204); |
| 371 | } catch (error) { |
| 372 | console.error('Moving extension failed', error); |
| 373 | return response.status(500).send('Internal Server Error. Check the server logs for more details.'); |
| 374 | } |
| 375 | }); |
| 376 | |
| 377 | /** |
| 378 | * HTTP POST handler function to get the current git commit hash and branch name for a given extension. |
| 379 | * It checks whether the repository is up-to-date with the remote, and returns the status along with |
| 380 | * the remote URL of the repository. |
| 381 | * |
| 382 | * @param {Object} request - HTTP Request object, expects a JSON body with an 'extensionName' property. |
| 383 | * @param {Object} response - HTTP Response object used to respond to the HTTP request. |
| 384 | * |
| 385 | * @returns {void} |
| 386 | */ |
| 387 | router.post('/version', async (request, response) => { |
| 388 | try { |
| 389 | if (typeof request.body.extensionName !== 'string') { |
| 390 | return response.status(400).send('Bad Request: A valid extensionName is required in the request body.'); |
| 391 | } |
| 392 | |
| 393 | const { extensionName, global } = request.body; |
| 394 | const extensionNameSanitized = sanitize(extensionName); |
| 395 | if (!extensionNameSanitized) { |
| 396 | return response.status(400).send('Bad Request: A valid extensionName is required in the request body.'); |
| 397 | } |
| 398 | |
| 399 | const basePath = global ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions; |
| 400 | const extensionPath = path.join(basePath, extensionNameSanitized); |
| 401 | |
| 402 | if (!fs.existsSync(extensionPath)) { |
| 403 | return response.status(404).send(`Directory does not exist at ${extensionPath}`); |
| 404 | } |
| 405 | |
| 406 | const git = simpleGit({ baseDir: extensionPath, ...OPTIONS }); |
| 407 | let currentCommitHash; |
| 408 | try { |
| 409 | const isRepo = await git.checkIsRepo(CheckRepoActions.IS_REPO_ROOT); |
| 410 | if (!isRepo) { |
| 411 | throw new Error(`Directory is not a Git repository at ${extensionPath}`); |
| 412 | } |
| 413 | currentCommitHash = await git.revparse(['HEAD']); |
| 414 | } catch (error) { |
| 415 | // it is not a git repo, or has no commits yet, or is a bare repo |
| 416 | // not possible to update it, most likely can't get the branch name either |
| 417 | return response.send({ currentBranchName: '', currentCommitHash: '', isUpToDate: true, remoteUrl: '' }); |
| 418 | } |
| 419 | |
| 420 | const currentBranch = await git.branch(); |
| 421 | // get only the working branch |
| 422 | const currentBranchName = currentBranch.current; |
| 423 | await git.fetch('origin'); |
| 424 | console.debug(extensionNameSanitized, currentBranchName, currentCommitHash); |
| 425 | const { isUpToDate, remoteUrl } = await checkIfRepoIsUpToDate(extensionPath); |
| 426 | |
| 427 | return response.send({ currentBranchName, currentCommitHash, isUpToDate, remoteUrl }); |
| 428 | } catch (error) { |
| 429 | console.error('Getting extension version failed', error); |
| 430 | return response.status(500).send('Internal Server Error. Check the server logs for more details.'); |
| 431 | } |
| 432 | }); |
| 433 | |
| 434 | /** |
| 435 | * HTTP POST handler function to delete a git repository based on the extension name provided in the request body. |
| 436 | * |
| 437 | * @param {Object} request - HTTP Request object, expects a JSON body with a 'extensionName' property. |
| 438 | * @param {Object} response - HTTP Response object used to respond to the HTTP request. |
| 439 | * |
| 440 | * @returns {void} |
| 441 | */ |
| 442 | router.post('/delete', async (request, response) => { |
| 443 | try { |
| 444 | if (typeof request.body.extensionName !== 'string') { |
| 445 | return response.status(400).send('Bad Request: A valid extensionName is required in the request body.'); |
| 446 | } |
| 447 | |
| 448 | const { extensionName, global } = request.body; |
| 449 | const extensionNameSanitized = sanitize(extensionName); |
| 450 | if (!extensionNameSanitized) { |
| 451 | return response.status(400).send('Bad Request: A valid extensionName is required in the request body.'); |
| 452 | } |
| 453 | |
| 454 | if (global && !request.user.profile.admin) { |
| 455 | console.error(`User ${request.user.profile.handle} does not have permission to delete global extensions.`); |
| 456 | return response.status(403).send('Forbidden: No permission to delete global extensions.'); |
| 457 | } |
| 458 | |
| 459 | const basePath = global ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions; |
| 460 | const extensionPath = path.join(basePath, extensionNameSanitized); |
| 461 | |
| 462 | if (!fs.existsSync(extensionPath)) { |
| 463 | return response.status(404).send(`Directory does not exist at ${extensionPath}`); |
| 464 | } |
| 465 | |
| 466 | await fs.promises.rm(extensionPath, { recursive: true }); |
| 467 | console.info(`Extension has been deleted at ${extensionPath}`); |
| 468 | |
| 469 | return response.send(`Extension has been deleted at ${extensionPath}`); |
| 470 | } catch (error) { |
| 471 | console.error('Deleting extension failed', error); |
| 472 | return response.status(500).send('Internal Server Error. Check the server logs for more details.'); |
| 473 | } |
| 474 | }); |
| 475 | |
| 476 | /** |
| 477 | * Discover the extension folders |
| 478 | * If the folder is called third-party, search for subfolders instead |
| 479 | */ |
| 480 | router.get('/discover', function (request, response) { |
| 481 | if (!fs.existsSync(path.join(request.user.directories.extensions))) { |
| 482 | fs.mkdirSync(path.join(request.user.directories.extensions)); |
| 483 | } |
| 484 | |
| 485 | if (!fs.existsSync(PUBLIC_DIRECTORIES.globalExtensions)) { |
| 486 | fs.mkdirSync(PUBLIC_DIRECTORIES.globalExtensions); |
| 487 | } |
| 488 | |
| 489 | // Get all folders in system extensions folder, excluding third-party |
| 490 | const builtInExtensions = fs |
| 491 | .readdirSync(PUBLIC_DIRECTORIES.extensions) |
| 492 | .filter(f => fs.statSync(path.join(PUBLIC_DIRECTORIES.extensions, f)).isDirectory()) |
| 493 | .filter(f => f !== 'third-party') |
| 494 | .map(f => ({ type: 'system', name: f })); |
| 495 | |
| 496 | // Get all folders in local extensions folder |
| 497 | const userExtensions = fs |
| 498 | .readdirSync(path.join(request.user.directories.extensions)) |
| 499 | .filter(f => fs.statSync(path.join(request.user.directories.extensions, f)).isDirectory()) |
| 500 | .map(f => ({ type: 'local', name: `third-party/${f}` })); |
| 501 | |
| 502 | // Get all folders in global extensions folder |
| 503 | // In case of a conflict, the extension will be loaded from the user folder |
| 504 | const globalExtensions = fs |
| 505 | .readdirSync(PUBLIC_DIRECTORIES.globalExtensions) |
| 506 | .filter(f => fs.statSync(path.join(PUBLIC_DIRECTORIES.globalExtensions, f)).isDirectory()) |
| 507 | .map(f => ({ type: 'global', name: `third-party/${f}` })) |
| 508 | .filter(f => !userExtensions.some(e => e.name === f.name)); |
| 509 | |
| 510 | // Combine all extensions |
| 511 | const allExtensions = [...builtInExtensions, ...userExtensions, ...globalExtensions]; |
| 512 | console.debug('Extensions available for', request.user.profile.handle, allExtensions); |
| 513 | |
| 514 | return response.send(allExtensions); |
| 515 | }); |