[wip] Add global extensions
| @@ -84,7 +84,7 @@ label[for="extensions_autoconnect"] { | |||
| 84 | .extensions_info .extension_block { | 84 | .extensions_info .extension_block { |
| 85 | display: flex; | 85 | display: flex; |
| 86 | flex-wrap: wrap; | 86 | flex-wrap: wrap; |
| 87 | padding: 10px; | 87 | padding: 5px 10px; |
| 88 | margin-bottom: 5px; | 88 | margin-bottom: 5px; |
| 89 | border: 1px solid var(--SmartThemeBorderColor); | 89 | border: 1px solid var(--SmartThemeBorderColor); |
| 90 | border-radius: 10px; | 90 | border-radius: 10px; |
| @@ -6,6 +6,8 @@ import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; | |||
| 6 | import { renderTemplate, renderTemplateAsync } from './templates.js'; | 6 | import { renderTemplate, renderTemplateAsync } from './templates.js'; |
| 7 | import { isSubsetOf, setValueByPath } from './utils.js'; | 7 | import { isSubsetOf, setValueByPath } from './utils.js'; |
| 8 | import { getContext } from './st-context.js'; | 8 | import { getContext } from './st-context.js'; |
| 9 | import { isAdmin } from './user.js'; | ||
| 10 | import { t } from './i18n.js'; | ||
| 9 | export { | 11 | export { |
| 10 | getContext, | 12 | getContext, |
| 11 | getApiUrl, | 13 | getApiUrl, |
| @@ -19,6 +21,8 @@ export { | |||
| 19 | 21 | ||
| 20 | /** @type {string[]} */ | 22 | /** @type {string[]} */ |
| 21 | export let extensionNames = []; | 23 | export let extensionNames = []; |
| 24 | /** @type {Record<string, string>} */ | ||
| 25 | export let extensionTypes = {}; | ||
| 22 | 26 | ||
| 23 | let manifests = {}; | 27 | let manifests = {}; |
| 24 | const defaultUrl = 'http://localhost:5100'; | 28 | const defaultUrl = 'http://localhost:5100'; |
| @@ -217,6 +221,10 @@ async function doExtrasFetch(endpoint, args) { | |||
| 217 | return response; | 221 | return response; |
| 218 | } | 222 | } |
| 219 | 223 | ||
| 224 | /** | ||
| 225 | * Discovers extensions from the API. | ||
| 226 | * @returns {Promise<{name: string, type: string}[]>} | ||
| 227 | */ | ||
| 220 | async function discoverExtensions() { | 228 | async function discoverExtensions() { |
| 221 | try { | 229 | try { |
| 222 | const response = await fetch('/api/extensions/discover'); | 230 | const response = await fetch('/api/extensions/discover'); |
| @@ -702,7 +710,14 @@ async function showExtensionsDetails() { | |||
| 702 | * If the extension is not up to date, it updates the extension and displays a success message with the new commit hash. | 710 | * If the extension is not up to date, it updates the extension and displays a success message with the new commit hash. |
| 703 | */ | 711 | */ |
| 704 | async function onUpdateClick() { | 712 | async function onUpdateClick() { |
| 713 | const isCurrentUserAdmin = isAdmin(); | ||
| 705 | const extensionName = $(this).data('name'); | 714 | const extensionName = $(this).data('name'); |
| 715 | const isGlobal = extensionTypes[extensionName] === 'global'; | ||
| 716 | if (isGlobal && !isCurrentUserAdmin) { | ||
| 717 | toastr.error(t`You don't have permission to update global extensions.`); | ||
| 718 | return; | ||
| 719 | } | ||
| 720 | |||
| 706 | $(this).find('i').addClass('fa-spin'); | 721 | $(this).find('i').addClass('fa-spin'); |
| 707 | await updateExtension(extensionName, false); | 722 | await updateExtension(extensionName, false); |
| 708 | } | 723 | } |
| @@ -717,7 +732,10 @@ async function updateExtension(extensionName, quiet) { | |||
| 717 | const response = await fetch('/api/extensions/update', { | 732 | const response = await fetch('/api/extensions/update', { |
| 718 | method: 'POST', | 733 | method: 'POST', |
| 719 | headers: getRequestHeaders(), | 734 | headers: getRequestHeaders(), |
| 720 | body: JSON.stringify({ extensionName }), | 735 | body: JSON.stringify({ |
| 736 | extensionName, | ||
| 737 | global: extensionTypes[extensionName] === 'global', | ||
| 738 | }), | ||
| 721 | }); | 739 | }); |
| 722 | 740 | ||
| 723 | const data = await response.json(); | 741 | const data = await response.json(); |
| @@ -746,6 +764,13 @@ async function updateExtension(extensionName, quiet) { | |||
| 746 | */ | 764 | */ |
| 747 | async function onDeleteClick() { | 765 | async function onDeleteClick() { |
| 748 | const extensionName = $(this).data('name'); | 766 | const extensionName = $(this).data('name'); |
| 767 | const isCurrentUserAdmin = isAdmin(); | ||
| 768 | const isGlobal = extensionTypes[extensionName] === 'global'; | ||
| 769 | if (isGlobal && !isCurrentUserAdmin) { | ||
| 770 | toastr.error(t`You don't have permission to delete global extensions.`); | ||
| 771 | return; | ||
| 772 | } | ||
| 773 | |||
| 749 | // use callPopup to create a popup for the user to confirm before delete | 774 | // use callPopup to create a popup for the user to confirm before delete |
| 750 | const confirmation = await callGenericPopup(`Are you sure you want to delete ${extensionName}?`, POPUP_TYPE.CONFIRM, '', {}); | 775 | const confirmation = await callGenericPopup(`Are you sure you want to delete ${extensionName}?`, POPUP_TYPE.CONFIRM, '', {}); |
| 751 | if (confirmation === POPUP_RESULT.AFFIRMATIVE) { | 776 | if (confirmation === POPUP_RESULT.AFFIRMATIVE) { |
| @@ -753,12 +778,19 @@ async function onDeleteClick() { | |||
| 753 | } | 778 | } |
| 754 | } | 779 | } |
| 755 | 780 | ||
| 781 | /** | ||
| 782 | * Deletes an extension via the API. | ||
| 783 | * @param {string} extensionName Extension name to delete | ||
| 784 | */ | ||
| 756 | export async function deleteExtension(extensionName) { | 785 | export async function deleteExtension(extensionName) { |
| 757 | try { | 786 | try { |
| 758 | await fetch('/api/extensions/delete', { | 787 | await fetch('/api/extensions/delete', { |
| 759 | method: 'POST', | 788 | method: 'POST', |
| 760 | headers: getRequestHeaders(), | 789 | headers: getRequestHeaders(), |
| 761 | body: JSON.stringify({ extensionName }), | 790 | body: JSON.stringify({ |
| 791 | extensionName, | ||
| 792 | global: extensionTypes[extensionName] === 'global', | ||
| 793 | }), | ||
| 762 | }); | 794 | }); |
| 763 | } catch (error) { | 795 | } catch (error) { |
| 764 | console.error('Error:', error); | 796 | console.error('Error:', error); |
| @@ -796,9 +828,10 @@ async function getExtensionVersion(extensionName) { | |||
| 796 | /** | 828 | /** |
| 797 | * Installs a third-party extension via the API. | 829 | * Installs a third-party extension via the API. |
| 798 | * @param {string} url Extension repository URL | 830 | * @param {string} url Extension repository URL |
| 831 | * @param {boolean} global Is the extension global? | ||
| 799 | * @returns {Promise<void>} | 832 | * @returns {Promise<void>} |
| 800 | */ | 833 | */ |
| 801 | export async function installExtension(url) { | 834 | export async function installExtension(url, global) { |
| 802 | console.debug('Extension installation started', url); | 835 | console.debug('Extension installation started', url); |
| 803 | 836 | ||
| 804 | toastr.info('Please wait...', 'Installing extension'); | 837 | toastr.info('Please wait...', 'Installing extension'); |
| @@ -806,7 +839,10 @@ export async function installExtension(url) { | |||
| 806 | const request = await fetch('/api/extensions/install', { | 839 | const request = await fetch('/api/extensions/install', { |
| 807 | method: 'POST', | 840 | method: 'POST', |
| 808 | headers: getRequestHeaders(), | 841 | headers: getRequestHeaders(), |
| 809 | body: JSON.stringify({ url }), | 842 | body: JSON.stringify({ |
| 843 | url, | ||
| 844 | global, | ||
| 845 | }), | ||
| 810 | }); | 846 | }); |
| 811 | 847 | ||
| 812 | if (!request.ok) { | 848 | if (!request.ok) { |
| @@ -841,7 +877,9 @@ async function loadExtensionSettings(settings, versionChanged, enableAutoUpdate) | |||
| 841 | 877 | ||
| 842 | // Activate offline extensions | 878 | // Activate offline extensions |
| 843 | await eventSource.emit(event_types.EXTENSIONS_FIRST_LOAD); | 879 | await eventSource.emit(event_types.EXTENSIONS_FIRST_LOAD); |
| 844 | extensionNames = await discoverExtensions(); | 880 | const extensions = await discoverExtensions(); |
| 881 | extensionNames = extensions.map(x => x.name); | ||
| 882 | extensionTypes = Object.fromEntries(extensions.map(x => [x.name, x.type])); | ||
| 845 | manifests = await getManifests(extensionNames); | 883 | manifests = await getManifests(extensionNames); |
| 846 | 884 | ||
| 847 | if (versionChanged && enableAutoUpdate) { | 885 | if (versionChanged && enableAutoUpdate) { |
| @@ -926,10 +964,16 @@ async function checkForExtensionUpdates(force) { | |||
| 926 | localStorage.setItem(STORAGE_NAG_KEY, currentDate); | 964 | localStorage.setItem(STORAGE_NAG_KEY, currentDate); |
| 927 | } | 965 | } |
| 928 | 966 | ||
| 967 | const isCurrentUserAdmin = isAdmin(); | ||
| 929 | const updatesAvailable = []; | 968 | const updatesAvailable = []; |
| 930 | const promises = []; | 969 | const promises = []; |
| 931 | 970 | ||
| 932 | for (const [id, manifest] of Object.entries(manifests)) { | 971 | for (const [id, manifest] of Object.entries(manifests)) { |
| 972 | const isGlobal = extensionTypes[id] === 'global'; | ||
| 973 | if (isGlobal && !isCurrentUserAdmin) { | ||
| 974 | console.debug(`Skipping global extension: ${manifest.display_name} (${id}) for non-admin user`); | ||
| 975 | continue; | ||
| 976 | } | ||
| 933 | if (manifest.auto_update && id.startsWith('third-party')) { | 977 | if (manifest.auto_update && id.startsWith('third-party')) { |
| 934 | const promise = new Promise(async (resolve, reject) => { | 978 | const promise = new Promise(async (resolve, reject) => { |
| 935 | try { | 979 | try { |
| @@ -965,8 +1009,14 @@ async function autoUpdateExtensions(forceAll) { | |||
| 965 | } | 1009 | } |
| 966 | 1010 | ||
| 967 | const banner = toastr.info('Auto-updating extensions. This may take several minutes.', 'Please wait...', { timeOut: 10000, extendedTimeOut: 10000 }); | 1011 | const banner = toastr.info('Auto-updating extensions. This may take several minutes.', 'Please wait...', { timeOut: 10000, extendedTimeOut: 10000 }); |
| 1012 | const isCurrentUserAdmin = isAdmin(); | ||
| 968 | const promises = []; | 1013 | const promises = []; |
| 969 | for (const [id, manifest] of Object.entries(manifests)) { | 1014 | for (const [id, manifest] of Object.entries(manifests)) { |
| 1015 | const isGlobal = extensionTypes[id] === 'global'; | ||
| 1016 | if (isGlobal && !isCurrentUserAdmin) { | ||
| 1017 | console.debug(`Skipping global extension: ${manifest.display_name} (${id}) for non-admin user`); | ||
| 1018 | continue; | ||
| 1019 | } | ||
| 970 | if ((forceAll || manifest.auto_update) && id.startsWith('third-party')) { | 1020 | if ((forceAll || manifest.auto_update) && id.startsWith('third-party')) { |
| 971 | console.debug(`Auto-updating 3rd-party extension: ${manifest.display_name} (${id})`); | 1021 | console.debug(`Auto-updating 3rd-party extension: ${manifest.display_name} (${id})`); |
| 972 | promises.push(updateExtension(id.replace('third-party', ''), true)); | 1022 | promises.push(updateExtension(id.replace('third-party', ''), true)); |
| @@ -1068,8 +1118,23 @@ export async function writeExtensionField(characterId, key, value) { | |||
| 1068 | * @returns {Promise<void>} | 1118 | * @returns {Promise<void>} |
| 1069 | */ | 1119 | */ |
| 1070 | export async function openThirdPartyExtensionMenu(suggestUrl = '') { | 1120 | export async function openThirdPartyExtensionMenu(suggestUrl = '') { |
| 1071 | const html = await renderTemplateAsync('installExtension'); | 1121 | const isCurrentUserAdmin = isAdmin(); |
| 1072 | const input = await callGenericPopup(html, POPUP_TYPE.INPUT, suggestUrl ?? ''); | 1122 | const html = await renderTemplateAsync('installExtension', { isCurrentUserAdmin }); |
| 1123 | const okButton = isCurrentUserAdmin ? t`Install just for me` : t`Install`; | ||
| 1124 | |||
| 1125 | let global = false; | ||
| 1126 | const installForAllButton = { | ||
| 1127 | text: t`Install for all`, | ||
| 1128 | appendAtEnd: false, | ||
| 1129 | action: async () => { | ||
| 1130 | global = true; | ||
| 1131 | await popup.complete(POPUP_RESULT.AFFIRMATIVE); | ||
| 1132 | }, | ||
| 1133 | }; | ||
| 1134 | |||
| 1135 | const customButtons = isCurrentUserAdmin ? [installForAllButton] : []; | ||
| 1136 | const popup = new Popup(html, POPUP_TYPE.INPUT, suggestUrl ?? '', { okButton, customButtons }); | ||
| 1137 | const input = await popup.show(); | ||
| 1073 | 1138 | ||
| 1074 | if (!input) { | 1139 | if (!input) { |
| 1075 | console.debug('Extension install cancelled'); | 1140 | console.debug('Extension install cancelled'); |
| @@ -1077,11 +1142,9 @@ export async function openThirdPartyExtensionMenu(suggestUrl = '') { | |||
| 1077 | } | 1142 | } |
| 1078 | 1143 | ||
| 1079 | const url = String(input).trim(); | 1144 | const url = String(input).trim(); |
| 1080 | await installExtension(url); | 1145 | await installExtension(url, global); |
| 1081 | } | 1146 | } |
| 1082 | 1147 | ||
| 1083 | |||
| 1084 | |||
| 1085 | export async function initExtensions() { | 1148 | export async function initExtensions() { |
| 1086 | await addExtensionsButtonAndMenu(); | 1149 | await addExtensionsButtonAndMenu(); |
| 1087 | $('#extensionsMenuButton').css('display', 'flex'); | 1150 | $('#extensionsMenuButton').css('display', 'flex'); |
| @@ -31,7 +31,7 @@ export async function setUserControls(isEnabled) { | |||
| 31 | * Check if the current user is an admin. | 31 | * Check if the current user is an admin. |
| 32 | * @returns {boolean} True if the current user is an admin | 32 | * @returns {boolean} True if the current user is an admin |
| 33 | */ | 33 | */ |
| 34 | function isAdmin() { | 34 | export function isAdmin() { |
| 35 | if (!currentUser) { | 35 | if (!currentUser) { |
| 36 | return false; | 36 | return false; |
| 37 | } | 37 | } |
| @@ -3,6 +3,7 @@ export const PUBLIC_DIRECTORIES = { | |||
| 3 | backups: 'backups/', | 3 | backups: 'backups/', |
| 4 | sounds: 'public/sounds', | 4 | sounds: 'public/sounds', |
| 5 | extensions: 'public/scripts/extensions', | 5 | extensions: 'public/scripts/extensions', |
| 6 | globalExtensions: 'public/scripts/extensions/third-party', | ||
| 6 | }; | 7 | }; |
| 7 | 8 | ||
| 8 | export const SETTINGS_FILE = 'settings.json'; | 9 | export const SETTINGS_FILE = 'settings.json'; |
| @@ -73,8 +73,18 @@ router.post('/install', jsonParser, async (request, response) => { | |||
| 73 | fs.mkdirSync(path.join(request.user.directories.extensions)); | 73 | fs.mkdirSync(path.join(request.user.directories.extensions)); |
| 74 | } | 74 | } |
| 75 | 75 | ||
| 76 | const url = request.body.url; | 76 | if (!fs.existsSync(PUBLIC_DIRECTORIES.globalExtensions)) { |
| 77 | const extensionPath = path.join(request.user.directories.extensions, path.basename(url, '.git')); | 77 | fs.mkdirSync(PUBLIC_DIRECTORIES.globalExtensions); |
| 78 | } | ||
| 79 | |||
| 80 | const { url, global } = request.body; | ||
| 81 | |||
| 82 | if (global && !request.user.profile.admin) { | ||
| 83 | return response.status(403).send('Forbidden: No permission to install global extensions.'); | ||
| 84 | } | ||
| 85 | |||
| 86 | const basePath = global ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions; | ||
| 87 | const extensionPath = path.join(basePath, sanitize(path.basename(url, '.git'))); | ||
| 78 | 88 | ||
| 79 | if (fs.existsSync(extensionPath)) { | 89 | if (fs.existsSync(extensionPath)) { |
| 80 | return response.status(409).send(`Directory already exists at ${extensionPath}`); | 90 | return response.status(409).send(`Directory already exists at ${extensionPath}`); |
| @@ -83,10 +93,8 @@ router.post('/install', jsonParser, async (request, response) => { | |||
| 83 | await git.clone(url, extensionPath, { '--depth': 1 }); | 93 | await git.clone(url, extensionPath, { '--depth': 1 }); |
| 84 | console.log(`Extension has been cloned at ${extensionPath}`); | 94 | console.log(`Extension has been cloned at ${extensionPath}`); |
| 85 | 95 | ||
| 86 | |||
| 87 | const { version, author, display_name } = await getManifest(extensionPath); | 96 | const { version, author, display_name } = await getManifest(extensionPath); |
| 88 | 97 | ||
| 89 | |||
| 90 | return response.send({ version, author, display_name, extensionPath }); | 98 | return response.send({ version, author, display_name, extensionPath }); |
| 91 | } catch (error) { | 99 | } catch (error) { |
| 92 | console.log('Importing custom content failed', error); | 100 | console.log('Importing custom content failed', error); |
| @@ -112,8 +120,14 @@ router.post('/update', jsonParser, async (request, response) => { | |||
| 112 | } | 120 | } |
| 113 | 121 | ||
| 114 | try { | 122 | try { |
| 115 | const extensionName = request.body.extensionName; | 123 | const { extensionName, global } = request.body; |
| 116 | const extensionPath = path.join(request.user.directories.extensions, extensionName); | 124 | |
| 125 | if (global && !request.user.profile.admin) { | ||
| 126 | return response.status(403).send('Forbidden: No permission to update global extensions.'); | ||
| 127 | } | ||
| 128 | |||
| 129 | const basePath = global ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions; | ||
| 130 | const extensionPath = path.join(basePath, extensionName); | ||
| 117 | 131 | ||
| 118 | if (!fs.existsSync(extensionPath)) { | 132 | if (!fs.existsSync(extensionPath)) { |
| 119 | return response.status(404).send(`Directory does not exist at ${extensionPath}`); | 133 | return response.status(404).send(`Directory does not exist at ${extensionPath}`); |
| @@ -122,7 +136,6 @@ router.post('/update', jsonParser, async (request, response) => { | |||
| 122 | const { isUpToDate, remoteUrl } = await checkIfRepoIsUpToDate(extensionPath); | 136 | const { isUpToDate, remoteUrl } = await checkIfRepoIsUpToDate(extensionPath); |
| 123 | const currentBranch = await git.cwd(extensionPath).branch(); | 137 | const currentBranch = await git.cwd(extensionPath).branch(); |
| 124 | if (!isUpToDate) { | 138 | if (!isUpToDate) { |
| 125 | |||
| 126 | await git.cwd(extensionPath).pull('origin', currentBranch.current); | 139 | await git.cwd(extensionPath).pull('origin', currentBranch.current); |
| 127 | console.log(`Extension has been updated at ${extensionPath}`); | 140 | console.log(`Extension has been updated at ${extensionPath}`); |
| 128 | } else { | 141 | } else { |
| @@ -157,8 +170,9 @@ router.post('/version', jsonParser, async (request, response) => { | |||
| 157 | } | 170 | } |
| 158 | 171 | ||
| 159 | try { | 172 | try { |
| 160 | const extensionName = request.body.extensionName; | 173 | const { extensionName, global } = request.body; |
| 161 | const extensionPath = path.join(request.user.directories.extensions, extensionName); | 174 | const basePath = global ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions; |
| 175 | const extensionPath = path.join(basePath, sanitize(extensionName)); | ||
| 162 | 176 | ||
| 163 | if (!fs.existsSync(extensionPath)) { | 177 | if (!fs.existsSync(extensionPath)) { |
| 164 | return response.status(404).send(`Directory does not exist at ${extensionPath}`); | 178 | return response.status(404).send(`Directory does not exist at ${extensionPath}`); |
| @@ -193,11 +207,15 @@ router.post('/delete', jsonParser, async (request, response) => { | |||
| 193 | return response.status(400).send('Bad Request: extensionName is required in the request body.'); | 207 | return response.status(400).send('Bad Request: extensionName is required in the request body.'); |
| 194 | } | 208 | } |
| 195 | 209 | ||
| 196 | // Sanitize the extension name to prevent directory traversal | ||
| 197 | const extensionName = sanitize(request.body.extensionName); | ||
| 198 | |||
| 199 | try { | 210 | try { |
| 200 | const extensionPath = path.join(request.user.directories.extensions, extensionName); | 211 | const { extensionName, global } = request.body; |
| 212 | |||
| 213 | if (global && !request.user.profile.admin) { | ||
| 214 | return response.status(403).send('Forbidden: No permission to delete global extensions.'); | ||
| 215 | } | ||
| 216 | |||
| 217 | const basePath = global ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions; | ||
| 218 | const extensionPath = path.join(basePath, sanitize(extensionName)); | ||
| 201 | 219 | ||
| 202 | if (!fs.existsSync(extensionPath)) { | 220 | if (!fs.existsSync(extensionPath)) { |
| 203 | return response.status(404).send(`Directory does not exist at ${extensionPath}`); | 221 | return response.status(404).send(`Directory does not exist at ${extensionPath}`); |
| @@ -219,26 +237,36 @@ router.post('/delete', jsonParser, async (request, response) => { | |||
| 219 | * If the folder is called third-party, search for subfolders instead | 237 | * If the folder is called third-party, search for subfolders instead |
| 220 | */ | 238 | */ |
| 221 | router.get('/discover', jsonParser, function (request, response) { | 239 | router.get('/discover', jsonParser, function (request, response) { |
| 222 | // get all folders in the extensions folder, except third-party | 240 | if (!fs.existsSync(path.join(request.user.directories.extensions))) { |
| 223 | const extensions = fs | 241 | fs.mkdirSync(path.join(request.user.directories.extensions)); |
| 242 | } | ||
| 243 | |||
| 244 | if (!fs.existsSync(PUBLIC_DIRECTORIES.globalExtensions)) { | ||
| 245 | fs.mkdirSync(PUBLIC_DIRECTORIES.globalExtensions); | ||
| 246 | } | ||
| 247 | |||
| 248 | // Get all folders in system extensions folder, excluding third-party | ||
| 249 | const buildInExtensions = fs | ||
| 224 | .readdirSync(PUBLIC_DIRECTORIES.extensions) | 250 | .readdirSync(PUBLIC_DIRECTORIES.extensions) |
| 225 | .filter(f => fs.statSync(path.join(PUBLIC_DIRECTORIES.extensions, f)).isDirectory()) | 251 | .filter(f => fs.statSync(path.join(PUBLIC_DIRECTORIES.extensions, f)).isDirectory()) |
| 226 | .filter(f => f !== 'third-party'); | 252 | .filter(f => f !== 'third-party') |
| 227 | 253 | .map(f => ({ type: 'system', name: f })); | |
| 228 | // get all folders in the third-party folder, if it exists | ||
| 229 | 254 | ||
| 230 | if (!fs.existsSync(path.join(request.user.directories.extensions))) { | 255 | // Get all folders in global extensions folder |
| 231 | return response.send(extensions); | 256 | const globalExtensions = fs |
| 232 | } | 257 | .readdirSync(PUBLIC_DIRECTORIES.globalExtensions) |
| 258 | .filter(f => fs.statSync(path.join(PUBLIC_DIRECTORIES.globalExtensions, f)).isDirectory()) | ||
| 259 | .map(f => ({ type: 'global', name: `third-party/${f}` })); | ||
| 233 | 260 | ||
| 234 | const thirdPartyExtensions = fs | 261 | // Get all folders in local extensions folder |
| 262 | const userExtensions = fs | ||
| 235 | .readdirSync(path.join(request.user.directories.extensions)) | 263 | .readdirSync(path.join(request.user.directories.extensions)) |
| 236 | .filter(f => fs.statSync(path.join(request.user.directories.extensions, f)).isDirectory()); | 264 | .filter(f => fs.statSync(path.join(request.user.directories.extensions, f)).isDirectory()) |
| 237 | 265 | .map(f => ({ type: 'local', name: `third-party/${f}` })); | |
| 238 | // add the third-party extensions to the extensions array | ||
| 239 | extensions.push(...thirdPartyExtensions.map(f => `third-party/${f}`)); | ||
| 240 | console.log(extensions); | ||
| 241 | 266 | ||
| 267 | // Combine all extensions | ||
| 268 | const allExtensions = Array.from(new Set([...buildInExtensions, ...globalExtensions, ...userExtensions])); | ||
| 269 | console.log(allExtensions); | ||
| 242 | 270 | ||
| 243 | return response.send(extensions); | 271 | return response.send(allExtensions); |
| 244 | }); | 272 | }); |
| @@ -783,6 +783,34 @@ function createRouteHandler(directoryFn) { | |||
| 783 | } | 783 | } |
| 784 | 784 | ||
| 785 | /** | 785 | /** |
| 786 | * Creates a route handler for serving extensions. | ||
| 787 | * @param {(req: import('express').Request) => string} directoryFn A function that returns the directory path to serve files from | ||
| 788 | * @returns {import('express').RequestHandler} | ||
| 789 | */ | ||
| 790 | function createExtensionsRouteHandler(directoryFn) { | ||
| 791 | return async (req, res) => { | ||
| 792 | try { | ||
| 793 | const directory = directoryFn(req); | ||
| 794 | const filePath = decodeURIComponent(req.params[0]); | ||
| 795 | |||
| 796 | const existsLocal = fs.existsSync(path.join(directory, filePath)); | ||
| 797 | if (existsLocal) { | ||
| 798 | return res.sendFile(filePath, { root: directory }); | ||
| 799 | } | ||
| 800 | |||
| 801 | const existsGlobal = fs.existsSync(path.join(PUBLIC_DIRECTORIES.globalExtensions, filePath)); | ||
| 802 | if (existsGlobal) { | ||
| 803 | return res.sendFile(filePath, { root: PUBLIC_DIRECTORIES.globalExtensions }); | ||
| 804 | } | ||
| 805 | |||
| 806 | return res.sendStatus(404); | ||
| 807 | } catch (error) { | ||
| 808 | return res.sendStatus(500); | ||
| 809 | } | ||
| 810 | }; | ||
| 811 | } | ||
| 812 | |||
| 813 | /** | ||
| 786 | * Verifies that the current user is an admin. | 814 | * Verifies that the current user is an admin. |
| 787 | * @param {import('express').Request} request Request object | 815 | * @param {import('express').Request} request Request object |
| 788 | * @param {import('express').Response} response Response object | 816 | * @param {import('express').Response} response Response object |
| @@ -872,4 +900,4 @@ router.use('/User%20Avatars/*', createRouteHandler(req => req.user.directories.a | |||
| 872 | router.use('/assets/*', createRouteHandler(req => req.user.directories.assets)); | 900 | router.use('/assets/*', createRouteHandler(req => req.user.directories.assets)); |
| 873 | router.use('/user/images/*', createRouteHandler(req => req.user.directories.userImages)); | 901 | router.use('/user/images/*', createRouteHandler(req => req.user.directories.userImages)); |
| 874 | router.use('/user/files/*', createRouteHandler(req => req.user.directories.files)); | 902 | router.use('/user/files/*', createRouteHandler(req => req.user.directories.files)); |
| 875 | router.use('/scripts/extensions/third-party/*', createRouteHandler(req => req.user.directories.extensions)); | 903 | router.use('/scripts/extensions/third-party/*', createExtensionsRouteHandler(req => req.user.directories.extensions)); |