From bb6ff352b3b2447474a57aa5f5f15608c1ed3e44 Mon Sep 17 00:00:00 2001 From: BlipRanger <1860540+BlipRanger@users.noreply.github.com> Date: Tue, 11 Jul 2023 16:10:42 -0400 Subject: [PATCH 01/16] External extension support start --- public/index.html | 1 + public/script.js | 32 +++++++++++++++++++++ server.js | 72 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+) diff --git a/public/index.html b/public/index.html index a2be10373..18342d2bd 100644 --- a/public/index.html +++ b/public/index.html @@ -2853,6 +2853,7 @@ + +
@@ -2853,7 +2854,6 @@ - `; + if (isActive || isDisabled) { + toggleElement = ``; } - html += `

${iconString} ${DOMPurify.sanitize(manifest.display_name)} ${toggleElement}

`; + + let updateButton = isExternal ? `` : ''; + let extensionHtml = `

${DOMPurify.sanitize(displayName)}${displayVersion} ${toggleElement}${updateButton}

`; if (isActive) { if (Array.isArray(manifest.optional)) { @@ -405,7 +418,7 @@ function showExtensionsDetails() { modules.forEach(x => optional.delete(x)); if (optional.size > 0) { const optionalString = DOMPurify.sanitize([...optional].join(', ')); - html += `

Optional modules: ${optionalString}

`; + extensionHtml += `

Optional modules: ${optionalString}

`; } } } @@ -413,13 +426,69 @@ function showExtensionsDetails() { const requirements = new Set(manifest.requires); modules.forEach(x => requirements.delete(x)); const requirementsString = DOMPurify.sanitize([...requirements].join(', ')); - html += `

Missing modules: ${requirementsString}

` + extensionHtml += `

Missing modules: ${requirementsString}

` + } + + // Append the HTML to the correct section + if (isExternal) { + htmlExternal += extensionHtml; + } else { + htmlDefault += extensionHtml; } }); + let html = '

Modules provided by your Extensions API:

'; + html += modules.length ? `

${DOMPurify.sanitize(modules.join(', '))}

` : '

Not connected to the API!

'; + html += htmlDefault + htmlExternal; + callPopup(`
${html}
`, 'text'); } + + +async function onUpdateClick() { + const extensionName = $(this).data('name'); + try { + const response = await fetch('/update_extension', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify({ extensionName }) + }); + + console.log('Response', response); + const data = await response.json(); + console.log('Data', data); + if (data.isUpToDate) { + console.log('Extension is up to date'); + toastr.success('Extension is already up to date'); + } else { + console.log('Extension updated'); + toastr.success(`Extension updated to ${data.shortCommitHash}`); + } + $(this).text(data.shortCommitHash); + console.log(data); + } catch (error) { + console.error('Error:', error); + } +}; + +async function getExtensionVersion(extensionName) { + try { + const response = await fetch('/get_extension_version', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify({ extensionName }) + }); + + const data = await response.json(); + return data; + } catch (error) { + console.error('Error:', error); + } +} + + + async function loadExtensionSettings(settings) { if (settings.extension_settings) { Object.assign(extension_settings, settings.extension_settings); @@ -463,4 +532,5 @@ $(document).ready(async function () { $("#extensions_details").on('click', showExtensionsDetails); $(document).on('click', '.toggle_disable', onDisableExtensionClick); $(document).on('click', '.toggle_enable', onEnableExtensionClick); + $(document).on('click', '.btn_update', onUpdateClick); }); diff --git a/server.js b/server.js index 6f7f3a9e8..f857bf9d1 100644 --- a/server.js +++ b/server.js @@ -2731,7 +2731,7 @@ app.get('/discover_extensions', jsonParser, function (_, response) { if (!fs.existsSync(path.join(directories.extensions, 'third-party'))) { return response.send(extensions); } - + const thirdPartyExtensions = fs .readdirSync(path.join(directories.extensions, 'third-party')) .filter(f => fs.statSync(path.join(directories.extensions, 'third-party', f)).isDirectory()); @@ -4426,3 +4426,83 @@ app.post('/get_extension', jsonParser, async (request, response) => { return response.status(500).send(`Server Error: ${error.message}`); } }); + +/** + * HTTP POST handler function to pull the latest updates from a given git repository + * based on the extension name and return the latest commit hash. + * + * @param {Object} request - HTTP Request object, expects a JSON body with an 'extensionName' property. + * @param {Object} response - HTTP Response object used to respond to the HTTP request. + * + * @returns {void} + */ +app.post('/update_extension', jsonParser, async (request, response) => { + if (!request.body.extensionName) { + return response.status(400).send('Bad Request: extensionName is required in the request body.'); + } + + try { + const extensionName = request.body.extensionName; + const extensionPath = path.join(directories.extensions, 'third-party', extensionName); + + if (!fs.existsSync(extensionPath)) { + return response.status(404).send(`Directory does not exist at ${extensionPath}`); + } + + const currentBranch = await git.cwd(extensionPath).branch(); + const currentCommitHash = await git.cwd(extensionPath).revparse(['HEAD']); + const isUpToDate = await git.cwd(extensionPath).log({ + from: currentCommitHash, + to: `origin/${currentBranch.current}`, + }); + + if (isUpToDate.total === 0) { + await git.cwd(extensionPath).pull('origin', currentBranch.current); + console.log(`Extension has been updated at ${extensionPath}`); + } else { + console.log(`Extension is up to date at ${extensionPath}`); + } + + const fullCommitHash = await git.cwd(extensionPath).revparse(['HEAD']); + const shortCommitHash = fullCommitHash.slice(0, 7); + + return response.send({ shortCommitHash, extensionPath, isUpToDate: isUpToDate.total === 0 }); + + } catch (error) { + console.log('Updating custom content failed', error); + return response.status(500).send(`Server Error: ${error.message}`); + } +}); + +/** + * Function to get current git commit hash and branch name for a given extension. + */ +app.post('/get_extension_version', jsonParser, async (request, response) => { + if (!request.body.extensionName) { + return response.status(400).send('Bad Request: extensionName is required in the request body.'); + } + + try { + const extensionName = request.body.extensionName; + const extensionPath = path.join(directories.extensions, 'third-party', extensionName); + + if (!fs.existsSync(extensionPath)) { + return response.status(404).send(`Directory does not exist at ${extensionPath}`); + } + + const currentBranch = await git.cwd(extensionPath).branch(); + // get only the working branch + const currentBranchName = currentBranch.current; + const currentCommitHash = await git.cwd(extensionPath).revparse(['HEAD']); + console.log(currentBranch, currentCommitHash); + return response.send({ currentBranchName, currentCommitHash }); + + } catch (error) { + console.log('Getting extension version failed', error); + return response.status(500).send(`Server Error: ${error.message}`); + } + } +); + + + From 396aaaf6e9c8adc31d468762736bbfe3d41a5872 Mon Sep 17 00:00:00 2001 From: BlipRanger <1860540+BlipRanger@users.noreply.github.com> Date: Wed, 12 Jul 2023 22:26:01 -0400 Subject: [PATCH 08/16] Add update button and check for external extenisons --- public/script.js | 13 +++++++------ public/scripts/extensions.js | 23 ++++++++++++----------- server.js | 34 +++++++++++++++++++++------------- 3 files changed, 40 insertions(+), 30 deletions(-) diff --git a/public/script.js b/public/script.js index ce84993a5..7335df735 100644 --- a/public/script.js +++ b/public/script.js @@ -8496,10 +8496,10 @@ $(document).ready(function () { $('#third_party_extension_button').on('click', async () => { const html = `

Enter the Git URL of the extension to import

-
-

Disclaimer: Please be aware that using external extensions can have unintended side effects and may pose security risks. Always make sure you trust the source before importing an extension. We are not responsible for any damage caused by third-party extensions.

-
-

Example: https://github.com/author/extension-name

` +
+

Disclaimer: Please be aware that using external extensions can have unintended side effects and may pose security risks. Always make sure you trust the source before importing an extension. We are not responsible for any damage caused by third-party extensions.

+
+

Example: https://github.com/author/extension-name

` const input = await callPopup(html, 'input'); if (!input) { @@ -8523,12 +8523,13 @@ $(document).ready(function () { } const response = await request.json(); - await loadExtensionSettings(settings); - eventSource.emit(event_types.EXTENSION_SETTINGS_LOADED); toastr.success(`Extension "${response.display_name}" by ${response.author} (version ${response.version}) has been imported successfully!`, 'Extension import successful'); console.debug(`Extension "${response.display_name}" has been imported successfully at ${response.extensionPath}`); + await loadExtensionSettings(settings); + eventSource.emit(event_types.EXTENSION_SETTINGS_LOADED); }); + const $dropzone = $(document.body); $dropzone.on('dragover', (event) => { diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js index dc8dfbe89..c551cc971 100644 --- a/public/scripts/extensions.js +++ b/public/scripts/extensions.js @@ -378,12 +378,13 @@ function addExtensionScript(name, manifest) { - -function showExtensionsDetails() { +async function showExtensionsDetails() { let htmlDefault = '

Default Extensions:

'; let htmlExternal = '

External Extensions:

'; - Object.entries(manifests).sort((a, b) => a[1].loading_order - b[1].loading_order).forEach(extension => async () =>{ + const extensions = Object.entries(manifests).sort((a, b) => a[1].loading_order - b[1].loading_order); + + for (const extension of extensions) { const name = extension[0]; const manifest = extension[1]; const isActive = activeExtensions.has(name); @@ -394,23 +395,23 @@ function showExtensionsDetails() { const checkboxClass = isDisabled ? "checkbox_disabled" : ""; const displayName = manifest.display_name; - // if external, get the version from a requst to get_extension_verion let displayVersion = manifest.version ? ` v${manifest.version}` : ""; - if(isExternal) { + let isUpToDate = true; + if (isExternal) { let data = await getExtensionVersion(name.replace('third-party', '')); let branch = data.currentBranchName; let commitHash = data.currentCommitHash; - displayVersion = ` v${branch}-${commitHash}`; + isUpToDate = data.isUpToDate; + displayVersion = ` (${branch}-${commitHash.substring(0, 7)})`; } - let toggleElement = ``; if (isActive || isDisabled) { toggleElement = ``; } - let updateButton = isExternal ? `` : ''; - let extensionHtml = `

${DOMPurify.sanitize(displayName)}${displayVersion} ${toggleElement}${updateButton}

`; + let updateButton = isExternal && !isUpToDate ? `` : ''; + let extensionHtml = `

${updateButton} ${DOMPurify.sanitize(displayName)}${displayVersion} ${toggleElement}

`; if (isActive) { if (Array.isArray(manifest.optional)) { @@ -435,8 +436,8 @@ function showExtensionsDetails() { } else { htmlDefault += extensionHtml; } - }); - + } + // Do something with htmlDefault and htmlExternal here let html = '

Modules provided by your Extensions API:

'; html += modules.length ? `

${DOMPurify.sanitize(modules.join(', '))}

` : '

Not connected to the API!

'; html += htmlDefault + htmlExternal; diff --git a/server.js b/server.js index f857bf9d1..1e501e8f9 100644 --- a/server.js +++ b/server.js @@ -4390,6 +4390,17 @@ async function getManifest(extensionPath) { return manifest; } +async function checkIfRepoIsUpToDate(extensionPath) { + const currentBranch = await git.cwd(extensionPath).branch(); + const currentCommitHash = await git.cwd(extensionPath).revparse(['HEAD']); + const log = await git.cwd(extensionPath).log({ + from: currentCommitHash, + to: `origin/${currentBranch.current}`, + }); + + return log.total === 0; +} + /** * HTTP POST handler function to clone a git repository from a provided URL, read the extension manifest, * and return extension information and path. @@ -4449,19 +4460,14 @@ app.post('/update_extension', jsonParser, async (request, response) => { return response.status(404).send(`Directory does not exist at ${extensionPath}`); } - const currentBranch = await git.cwd(extensionPath).branch(); - const currentCommitHash = await git.cwd(extensionPath).revparse(['HEAD']); - const isUpToDate = await git.cwd(extensionPath).log({ - from: currentCommitHash, - to: `origin/${currentBranch.current}`, - }); + const isUpToDate = await checkIfRepoIsUpToDate(extensionPath); - if (isUpToDate.total === 0) { - await git.cwd(extensionPath).pull('origin', currentBranch.current); - console.log(`Extension has been updated at ${extensionPath}`); - } else { - console.log(`Extension is up to date at ${extensionPath}`); - } + if (!isUpToDate) { + await git.cwd(extensionPath).pull('origin', currentBranch.current); + console.log(`Extension has been updated at ${extensionPath}`); + } else { + console.log(`Extension is up to date at ${extensionPath}`); + } const fullCommitHash = await git.cwd(extensionPath).revparse(['HEAD']); const shortCommitHash = fullCommitHash.slice(0, 7); @@ -4495,7 +4501,9 @@ app.post('/get_extension_version', jsonParser, async (request, response) => { const currentBranchName = currentBranch.current; const currentCommitHash = await git.cwd(extensionPath).revparse(['HEAD']); console.log(currentBranch, currentCommitHash); - return response.send({ currentBranchName, currentCommitHash }); + const isUpToDate = await checkIfRepoIsUpToDate(extensionPath); + + return response.send({ currentBranchName, currentCommitHash, isUpToDate }); } catch (error) { console.log('Getting extension version failed', error); From 75080394ac1743e31296de5ea7c09cb46042cd21 Mon Sep 17 00:00:00 2001 From: BlipRanger <1860540+BlipRanger@users.noreply.github.com> Date: Wed, 12 Jul 2023 22:26:23 -0400 Subject: [PATCH 09/16] Style for greyed out checkboxes --- public/style.css | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/public/style.css b/public/style.css index 3b870f68f..05cbeff81 100644 --- a/public/style.css +++ b/public/style.css @@ -3793,12 +3793,20 @@ label[for="extensions_autoconnect"] { .extensions_info .extension_missing { color: gray; } +input.extension_missing[type="checkbox"] { + opacity: 0.5; +} #extensions_list .disabled { text-decoration: line-through; color: lightgray; } +.update-button { + margin-right: 10px; + /* Adjust the value as needed */ +} + /* possible place for WI Entry header styling */ /* .world_entry_form .inline-drawer-header { background-color: var(--SmartThemeShadowColor); From 4fb65a92354eac8db7e43f2934f6b284cffd95b8 Mon Sep 17 00:00:00 2001 From: BlipRanger <1860540+BlipRanger@users.noreply.github.com> Date: Wed, 12 Jul 2023 22:37:24 -0400 Subject: [PATCH 10/16] Code refactor to make this more readable --- public/scripts/extensions.js | 122 +++++++++++++++++++++-------------- 1 file changed, 72 insertions(+), 50 deletions(-) diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js index c551cc971..7ac782e20 100644 --- a/public/scripts/extensions.js +++ b/public/scripts/extensions.js @@ -378,6 +378,72 @@ function addExtensionScript(name, manifest) { +async function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal, checkboxClass) { + const displayName = manifest.display_name; + let displayVersion = manifest.version ? ` v${manifest.version}` : ""; + let isUpToDate = true; + if (isExternal) { + let data = await getExtensionVersion(name.replace('third-party', '')); + let branch = data.currentBranchName; + let commitHash = data.currentCommitHash; + isUpToDate = data.isUpToDate; + displayVersion = ` (${branch}-${commitHash.substring(0, 7)})`; + } + + let toggleElement = isActive || isDisabled ? + `` : + ``; + + let updateButton = isExternal && !isUpToDate ? `` : ''; + let extensionHtml = `
+

+ ${updateButton} + + ${DOMPurify.sanitize(displayName)}${displayVersion} + + ${toggleElement} +

`; + + if (isActive && Array.isArray(manifest.optional)) { + const optional = new Set(manifest.optional); + modules.forEach(x => optional.delete(x)); + if (optional.size > 0) { + const optionalString = DOMPurify.sanitize([...optional].join(', ')); + extensionHtml += `

Optional modules: ${optionalString}

`; + } + } else if (!isDisabled) { // Neither active nor disabled + const requirements = new Set(manifest.requires); + modules.forEach(x => requirements.delete(x)); + const requirementsString = DOMPurify.sanitize([...requirements].join(', ')); + extensionHtml += `

Missing modules: ${requirementsString}

`; + } + + return extensionHtml; +} + +async function getExtensionData(extension) { + const name = extension[0]; + const manifest = extension[1]; + const isActive = activeExtensions.has(name); + const isDisabled = extension_settings.disabledExtensions.includes(name); + const isExternal = name.startsWith('third-party'); + + const checkboxClass = isDisabled ? "checkbox_disabled" : ""; + + const extensionHtml = await generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal, checkboxClass); + + return { isExternal, extensionHtml }; +} + + +function getModuleInformation() { + let moduleInfo = modules.length ? `

${DOMPurify.sanitize(modules.join(', '))}

` : '

Not connected to the API!

'; + return ` +

Modules provided by your Extensions API:

+ ${moduleInfo} + `; +} + async function showExtensionsDetails() { let htmlDefault = '

Default Extensions:

'; let htmlExternal = '

External Extensions:

'; @@ -385,63 +451,19 @@ async function showExtensionsDetails() { const extensions = Object.entries(manifests).sort((a, b) => a[1].loading_order - b[1].loading_order); for (const extension of extensions) { - const name = extension[0]; - const manifest = extension[1]; - const isActive = activeExtensions.has(name); - const isDisabled = extension_settings.disabledExtensions.includes(name); - const isExternal = name.startsWith('third-party'); - - const titleClass = isActive ? "extension_enabled" : isDisabled ? "extension_disabled" : "extension_missing"; - const checkboxClass = isDisabled ? "checkbox_disabled" : ""; - - const displayName = manifest.display_name; - let displayVersion = manifest.version ? ` v${manifest.version}` : ""; - let isUpToDate = true; - if (isExternal) { - let data = await getExtensionVersion(name.replace('third-party', '')); - let branch = data.currentBranchName; - let commitHash = data.currentCommitHash; - isUpToDate = data.isUpToDate; - displayVersion = ` (${branch}-${commitHash.substring(0, 7)})`; - } - - let toggleElement = ``; - if (isActive || isDisabled) { - toggleElement = ``; - } - - let updateButton = isExternal && !isUpToDate ? `` : ''; - let extensionHtml = `

${updateButton} ${DOMPurify.sanitize(displayName)}${displayVersion} ${toggleElement}

`; - - if (isActive) { - if (Array.isArray(manifest.optional)) { - const optional = new Set(manifest.optional); - modules.forEach(x => optional.delete(x)); - if (optional.size > 0) { - const optionalString = DOMPurify.sanitize([...optional].join(', ')); - extensionHtml += `

Optional modules: ${optionalString}

`; - } - } - } - else if (!isDisabled) { // Neither active nor disabled - const requirements = new Set(manifest.requires); - modules.forEach(x => requirements.delete(x)); - const requirementsString = DOMPurify.sanitize([...requirements].join(', ')); - extensionHtml += `

Missing modules: ${requirementsString}

` - } - - // Append the HTML to the correct section + const { isExternal, extensionHtml } = await getExtensionData(extension); if (isExternal) { htmlExternal += extensionHtml; } else { htmlDefault += extensionHtml; } } - // Do something with htmlDefault and htmlExternal here - let html = '

Modules provided by your Extensions API:

'; - html += modules.length ? `

${DOMPurify.sanitize(modules.join(', '))}

` : '

Not connected to the API!

'; - html += htmlDefault + htmlExternal; + const html = ` + ${getModuleInformation()} + ${htmlDefault} + ${htmlExternal} + `; callPopup(`
${html}
`, 'text'); } From cc7c42232ef25aaf0bc126c617e1c286a7cdaeb2 Mon Sep 17 00:00:00 2001 From: BlipRanger <1860540+BlipRanger@users.noreply.github.com> Date: Wed, 12 Jul 2023 22:37:42 -0400 Subject: [PATCH 11/16] Fetch first, then check status --- server.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server.js b/server.js index 1e501e8f9..9c703ed19 100644 --- a/server.js +++ b/server.js @@ -4391,6 +4391,7 @@ async function getManifest(extensionPath) { } async function checkIfRepoIsUpToDate(extensionPath) { + await git.cwd(extensionPath).fetch('origin'); const currentBranch = await git.cwd(extensionPath).branch(); const currentCommitHash = await git.cwd(extensionPath).revparse(['HEAD']); const log = await git.cwd(extensionPath).log({ @@ -4401,6 +4402,7 @@ async function checkIfRepoIsUpToDate(extensionPath) { return log.total === 0; } + /** * HTTP POST handler function to clone a git repository from a provided URL, read the extension manifest, * and return extension information and path. From eccae1056f33b10362d73d3dddc44299c212b185 Mon Sep 17 00:00:00 2001 From: BlipRanger <1860540+BlipRanger@users.noreply.github.com> Date: Wed, 12 Jul 2023 23:54:17 -0400 Subject: [PATCH 12/16] Add clickable names to go to repos --- public/scripts/extensions.js | 17 ++++++-- public/style.css | 2 +- server.js | 77 ++++++++++++++++++++---------------- 3 files changed, 57 insertions(+), 39 deletions(-) diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js index 7ac782e20..35909254a 100644 --- a/public/scripts/extensions.js +++ b/public/scripts/extensions.js @@ -382,25 +382,35 @@ async function generateExtensionHtml(name, manifest, isActive, isDisabled, isExt const displayName = manifest.display_name; let displayVersion = manifest.version ? ` v${manifest.version}` : ""; let isUpToDate = true; + let updateButton = ''; + let originHtml = ''; if (isExternal) { let data = await getExtensionVersion(name.replace('third-party', '')); let branch = data.currentBranchName; let commitHash = data.currentCommitHash; + let origin = data.remoteUrl isUpToDate = data.isUpToDate; displayVersion = ` (${branch}-${commitHash.substring(0, 7)})`; + updateButton = isUpToDate ? + `` : + ``; + originHtml = ``; } let toggleElement = isActive || isDisabled ? `` : ``; + + // if external, wrap the name in a link to the repo - let updateButton = isExternal && !isUpToDate ? `` : ''; let extensionHtml = `

- ${updateButton} + ${updateButton} + ${originHtml} ${DOMPurify.sanitize(displayName)}${displayVersion} + ${isExternal ? '' : ''} ${toggleElement}

`; @@ -488,8 +498,7 @@ async function onUpdateClick() { console.log('Extension updated'); toastr.success(`Extension updated to ${data.shortCommitHash}`); } - $(this).text(data.shortCommitHash); - console.log(data); + showExtensionsDetails(); } catch (error) { console.error('Error:', error); } diff --git a/public/style.css b/public/style.css index 05cbeff81..333a998d0 100644 --- a/public/style.css +++ b/public/style.css @@ -3804,7 +3804,7 @@ input.extension_missing[type="checkbox"] { .update-button { margin-right: 10px; - /* Adjust the value as needed */ + display: inline-flex; } /* possible place for WI Entry header styling */ diff --git a/server.js b/server.js index 9c703ed19..a7a2255ae 100644 --- a/server.js +++ b/server.js @@ -4391,18 +4391,25 @@ async function getManifest(extensionPath) { } async function checkIfRepoIsUpToDate(extensionPath) { - await git.cwd(extensionPath).fetch('origin'); - const currentBranch = await git.cwd(extensionPath).branch(); - const currentCommitHash = await git.cwd(extensionPath).revparse(['HEAD']); - const log = await git.cwd(extensionPath).log({ - from: currentCommitHash, - to: `origin/${currentBranch.current}`, - }); + await git.cwd(extensionPath).fetch('origin'); + const currentBranch = await git.cwd(extensionPath).branch(); + const currentCommitHash = await git.cwd(extensionPath).revparse(['HEAD']); + const log = await git.cwd(extensionPath).log({ + from: currentCommitHash, + to: `origin/${currentBranch.current}`, + }); - return log.total === 0; + // Fetch remote repository information + const remotes = await git.cwd(extensionPath).getRemotes(true); + + return { + isUpToDate: log.total === 0, + remoteUrl: remotes[0].refs.fetch, // URL of the remote repository + }; } + /** * HTTP POST handler function to clone a git repository from a provided URL, read the extension manifest, * and return extension information and path. @@ -4450,36 +4457,37 @@ app.post('/get_extension', jsonParser, async (request, response) => { * @returns {void} */ app.post('/update_extension', jsonParser, async (request, response) => { - if (!request.body.extensionName) { - return response.status(400).send('Bad Request: extensionName is required in the request body.'); - } - - try { - const extensionName = request.body.extensionName; - const extensionPath = path.join(directories.extensions, 'third-party', extensionName); - - if (!fs.existsSync(extensionPath)) { - return response.status(404).send(`Directory does not exist at ${extensionPath}`); + if (!request.body.extensionName) { + return response.status(400).send('Bad Request: extensionName is required in the request body.'); } - const isUpToDate = await checkIfRepoIsUpToDate(extensionPath); + try { + const extensionName = request.body.extensionName; + const extensionPath = path.join(directories.extensions, 'third-party', extensionName); - if (!isUpToDate) { - await git.cwd(extensionPath).pull('origin', currentBranch.current); - console.log(`Extension has been updated at ${extensionPath}`); - } else { - console.log(`Extension is up to date at ${extensionPath}`); - } + if (!fs.existsSync(extensionPath)) { + return response.status(404).send(`Directory does not exist at ${extensionPath}`); + } - const fullCommitHash = await git.cwd(extensionPath).revparse(['HEAD']); - const shortCommitHash = fullCommitHash.slice(0, 7); + const { isUpToDate, remoteUrl } = await checkIfRepoIsUpToDate(extensionPath); + const currentBranch = await git.cwd(extensionPath).branch(); + if (!isUpToDate) { - return response.send({ shortCommitHash, extensionPath, isUpToDate: isUpToDate.total === 0 }); + await git.cwd(extensionPath).pull('origin', currentBranch.current); + console.log(`Extension has been updated at ${extensionPath}`); + } else { + console.log(`Extension is up to date at ${extensionPath}`); + } + await git.cwd(extensionPath).fetch('origin'); + const fullCommitHash = await git.cwd(extensionPath).revparse(['HEAD']); + const shortCommitHash = fullCommitHash.slice(0, 7); - } catch (error) { - console.log('Updating custom content failed', error); - return response.status(500).send(`Server Error: ${error.message}`); - } + return response.send({ shortCommitHash, extensionPath, isUpToDate, remoteUrl }); + + } catch (error) { + console.log('Updating custom content failed', error); + return response.status(500).send(`Server Error: ${error.message}`); + } }); /** @@ -4501,11 +4509,12 @@ app.post('/get_extension_version', jsonParser, async (request, response) => { const currentBranch = await git.cwd(extensionPath).branch(); // get only the working branch const currentBranchName = currentBranch.current; + await git.cwd(extensionPath).fetch('origin'); const currentCommitHash = await git.cwd(extensionPath).revparse(['HEAD']); console.log(currentBranch, currentCommitHash); - const isUpToDate = await checkIfRepoIsUpToDate(extensionPath); + const { isUpToDate, remoteUrl } = await checkIfRepoIsUpToDate(extensionPath); - return response.send({ currentBranchName, currentCommitHash, isUpToDate }); + return response.send({ currentBranchName, currentCommitHash, isUpToDate, remoteUrl }); } catch (error) { console.log('Getting extension version failed', error); From 07b42e0fb40c0730466aa2e3addbb18d8bfe4267 Mon Sep 17 00:00:00 2001 From: BlipRanger <1860540+BlipRanger@users.noreply.github.com> Date: Thu, 13 Jul 2023 00:03:23 -0400 Subject: [PATCH 13/16] Documentation update --- public/script.js | 10 ++++++++ public/scripts/extensions.js | 45 ++++++++++++++++++++++++++++++++---- server.js | 19 +++++++++++---- 3 files changed, 64 insertions(+), 10 deletions(-) diff --git a/public/script.js b/public/script.js index 7335df735..a3d8fa7a5 100644 --- a/public/script.js +++ b/public/script.js @@ -8494,6 +8494,16 @@ $(document).ready(function () { } }); + /** + * Handles the click event for the third-party extension import button. + * Prompts the user to enter the Git URL of the extension to import. + * After obtaining the Git URL, makes a POST request to '/get_extension' to import the extension. + * If the extension is imported successfully, a success message is displayed. + * If the extension import fails, an error message is displayed and the error is logged to the console. + * After successfully importing the extension, the extension settings are reloaded and a 'EXTENSION_SETTINGS_LOADED' event is emitted. + * + * @listens #third_party_extension_button#click - The click event of the '#third_party_extension_button' element. + */ $('#third_party_extension_button').on('click', async () => { const html = `

Enter the Git URL of the extension to import


diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js index 35909254a..7827b1d93 100644 --- a/public/scripts/extensions.js +++ b/public/scripts/extensions.js @@ -378,6 +378,17 @@ function addExtensionScript(name, manifest) { +/** + * Generates HTML string for displaying an extension in the UI. + * + * @param {string} name - The name of the extension. + * @param {object} manifest - The manifest of the extension. + * @param {boolean} isActive - Whether the extension is active or not. + * @param {boolean} isDisabled - Whether the extension is disabled or not. + * @param {boolean} isExternal - Whether the extension is external or not. + * @param {string} checkboxClass - The class for the checkbox HTML element. + * @return {string} - The HTML string that represents the extension. + */ async function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal, checkboxClass) { const displayName = manifest.display_name; let displayVersion = manifest.version ? ` v${manifest.version}` : ""; @@ -431,6 +442,12 @@ async function generateExtensionHtml(name, manifest, isActive, isDisabled, isExt return extensionHtml; } +/** + * Gets extension data and generates the corresponding HTML for displaying the extension. + * + * @param {Array} extension - An array where the first element is the extension name and the second element is the extension manifest. + * @return {object} - An object with 'isExternal' indicating whether the extension is external, and 'extensionHtml' for the extension's HTML string. + */ async function getExtensionData(extension) { const name = extension[0]; const manifest = extension[1]; @@ -446,6 +463,11 @@ async function getExtensionData(extension) { } +/** + * Gets the module information to be displayed. + * + * @return {string} - The HTML string for the module information. + */ function getModuleInformation() { let moduleInfo = modules.length ? `

${DOMPurify.sanitize(modules.join(', '))}

` : '

Not connected to the API!

'; return ` @@ -454,6 +476,9 @@ function getModuleInformation() { `; } +/** + * Generates the HTML strings for all extensions and displays them in a popup. + */ async function showExtensionsDetails() { let htmlDefault = '

Default Extensions:

'; let htmlExternal = '

External Extensions:

'; @@ -478,7 +503,12 @@ async function showExtensionsDetails() { } - +/** + * Handles the click event for the update button of an extension. + * This function makes a POST request to '/update_extension' with the extension's name. + * If the extension is already up to date, it displays a success message. + * If the extension is not up to date, it updates the extension and displays a success message with the new commit hash. + */ async function onUpdateClick() { const extensionName = $(this).data('name'); try { @@ -488,14 +518,10 @@ async function onUpdateClick() { body: JSON.stringify({ extensionName }) }); - console.log('Response', response); const data = await response.json(); - console.log('Data', data); if (data.isUpToDate) { - console.log('Extension is up to date'); toastr.success('Extension is already up to date'); } else { - console.log('Extension updated'); toastr.success(`Extension updated to ${data.shortCommitHash}`); } showExtensionsDetails(); @@ -504,6 +530,15 @@ async function onUpdateClick() { } }; + +/** + * Fetches the version details of a specific extension. + * + * @param {string} extensionName - The name of the extension. + * @return {object} - An object containing the extension's version details. + * This object includes the currentBranchName, currentCommitHash, isUpToDate, and remoteUrl. + * @throws {error} - If there is an error during the fetch operation, it logs the error to the console. + */ async function getExtensionVersion(extensionName) { try { const response = await fetch('/get_extension_version', { diff --git a/server.js b/server.js index a7a2255ae..5093f0381 100644 --- a/server.js +++ b/server.js @@ -4413,7 +4413,7 @@ async function checkIfRepoIsUpToDate(extensionPath) { /** * HTTP POST handler function to clone a git repository from a provided URL, read the extension manifest, * and return extension information and path. - * + * * @param {Object} request - HTTP Request object, expects a JSON body with a 'url' property. * @param {Object} response - HTTP Response object used to respond to the HTTP request. * @@ -4448,9 +4448,11 @@ app.post('/get_extension', jsonParser, async (request, response) => { }); /** - * HTTP POST handler function to pull the latest updates from a given git repository - * based on the extension name and return the latest commit hash. - * + * HTTP POST handler function to pull the latest updates from a git repository + * based on the extension name provided in the request body. It returns the latest commit hash, + * the path of the extension, the status of the repository (whether it's up-to-date or not), + * and the remote URL of the repository. + * * @param {Object} request - HTTP Request object, expects a JSON body with an 'extensionName' property. * @param {Object} response - HTTP Response object used to respond to the HTTP request. * @@ -4491,7 +4493,14 @@ app.post('/update_extension', jsonParser, async (request, response) => { }); /** - * Function to get current git commit hash and branch name for a given extension. + * HTTP POST handler function to get the current git commit hash and branch name for a given extension. + * It checks whether the repository is up-to-date with the remote, and returns the status along with + * the remote URL of the repository. + * + * @param {Object} request - HTTP Request object, expects a JSON body with an 'extensionName' property. + * @param {Object} response - HTTP Response object used to respond to the HTTP request. + * + * @returns {void} */ app.post('/get_extension_version', jsonParser, async (request, response) => { if (!request.body.extensionName) { From 35336b16468ff107056467aeae0789c5b79ccc85 Mon Sep 17 00:00:00 2001 From: BlipRanger <1860540+BlipRanger@users.noreply.github.com> Date: Thu, 13 Jul 2023 00:59:16 -0400 Subject: [PATCH 14/16] Delete method almost complete --- public/script.js | 3 +++ public/scripts/extensions.js | 33 ++++++++++++++++++++++++++++++++- public/style.css | 7 +++++++ server.js | 31 +++++++++++++++++++++++++++++++ 4 files changed, 73 insertions(+), 1 deletion(-) diff --git a/public/script.js b/public/script.js index a3d8fa7a5..b7cd56bb2 100644 --- a/public/script.js +++ b/public/script.js @@ -5553,6 +5553,9 @@ function callPopup(text, type, inputValue = '', { okButton, rows } = {}) { $("#dialogue_popup_ok").text(okButton ?? "Ok"); $("#dialogue_popup_cancel").css("display", "none"); break; + case "delete_extension": + $("#dialogue_popup_ok").text(okButton ?? "Ok"); + break; case "new_chat": case "confirm": $("#dialogue_popup_ok").text(okButton ?? "Yes"); diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js index 7827b1d93..892f521b1 100644 --- a/public/scripts/extensions.js +++ b/public/scripts/extensions.js @@ -412,16 +412,20 @@ async function generateExtensionHtml(name, manifest, isActive, isDisabled, isExt `` : ``; + let deleteButton = isExternal ? `` : ''; + // if external, wrap the name in a link to the repo let extensionHtml = `

- ${updateButton} + ${updateButton} + ${deleteButton} ${originHtml} ${DOMPurify.sanitize(displayName)}${displayVersion} ${isExternal ? '' : ''} + ${toggleElement}

`; @@ -530,6 +534,32 @@ async function onUpdateClick() { } }; +/** + * Handles the click event for the delete button of an extension. + * This function makes a POST request to '/delete_extension' with the extension's name. + * If the extension is deleted, it displays a success message. + * Creates a popup for the user to confirm before delete. + */ +async function onDeleteClick() { + const extensionName = $(this).data('name'); + // use callPopup to create a popup for the user to confirm before delete + const confirmation = await callPopup(`Are you sure you want to delete ${extensionName}?`, 'delete_extension'); + if (confirmation) { + try { + const response = await fetch('/delete_extension', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify({ extensionName }) + }); + } catch (error) { + console.error('Error:', error); + } + toastr.success(`Extension ${extensionName} deleted`); + showExtensionsDetails(); + } +}; + + /** * Fetches the version details of a specific extension. @@ -600,4 +630,5 @@ $(document).ready(async function () { $(document).on('click', '.toggle_disable', onDisableExtensionClick); $(document).on('click', '.toggle_enable', onEnableExtensionClick); $(document).on('click', '.btn_update', onUpdateClick); + $(document).on('click', '.btn_delete', onDeleteClick); }); diff --git a/public/style.css b/public/style.css index 333a998d0..a66763278 100644 --- a/public/style.css +++ b/public/style.css @@ -3807,6 +3807,13 @@ input.extension_missing[type="checkbox"] { display: inline-flex; } +/* Align the content of this span to the right */ +.delete-button { + margin-right: 10px; + display: inline-flex; + +} + /* possible place for WI Entry header styling */ /* .world_entry_form .inline-drawer-header { background-color: var(--SmartThemeShadowColor); diff --git a/server.js b/server.js index 5093f0381..078964939 100644 --- a/server.js +++ b/server.js @@ -4532,5 +4532,36 @@ app.post('/get_extension_version', jsonParser, async (request, response) => { } ); +/** + * 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} response - HTTP Response object used to respond to the HTTP request. + * + * @returns {void} + */ +app.post('/delete_extension', jsonParser, async (request, response) => { + if (!request.body.extensionName) { + return response.status(400).send('Bad Request: extensionName is required in the request body.'); + } + // Sanatize the extension name to prevent directory traversal + const extensionName = sanitize(request.body.extensionName); + try { + const extensionPath = path.join(directories.extensions, 'third-party', extensionName); + + if (!fs.existsSync(extensionPath)) { + return response.status(404).send(`Directory does not exist at ${extensionPath}`); + } + + await fs.promises.rmdir(extensionPath, { recursive: true }); + console.log(`Extension has been deleted at ${extensionPath}`); + + return response.send(`Extension has been deleted at ${extensionPath}`); + + } catch (error) { + console.log('Deleting custom content failed', error); + return response.status(500).send(`Server Error: ${error.message}`); + } +}); From 2b8db6350e22458675e2e7eb051428022cd0445e Mon Sep 17 00:00:00 2001 From: BlipRanger <1860540+BlipRanger@users.noreply.github.com> Date: Thu, 13 Jul 2023 01:12:57 -0400 Subject: [PATCH 15/16] Fixed a stupid delete bug, reload on delete. --- public/scripts/extensions.js | 2 ++ server.js | 10 +++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js index 892f521b1..26171b92d 100644 --- a/public/scripts/extensions.js +++ b/public/scripts/extensions.js @@ -556,6 +556,8 @@ async function onDeleteClick() { } toastr.success(`Extension ${extensionName} deleted`); showExtensionsDetails(); + // reload the page to remove the extension from the list + location.reload(); } }; diff --git a/server.js b/server.js index 078964939..7b5ed0cfc 100644 --- a/server.js +++ b/server.js @@ -35,7 +35,6 @@ const compression = require('compression'); const app = express(); const responseTime = require('response-time'); const simpleGit = require('simple-git'); -const git = simpleGit(); app.use(compression()); app.use(responseTime()); @@ -4391,6 +4390,7 @@ async function getManifest(extensionPath) { } async function checkIfRepoIsUpToDate(extensionPath) { + const git = simpleGit(); await git.cwd(extensionPath).fetch('origin'); const currentBranch = await git.cwd(extensionPath).branch(); const currentCommitHash = await git.cwd(extensionPath).revparse(['HEAD']); @@ -4420,11 +4420,17 @@ async function checkIfRepoIsUpToDate(extensionPath) { * @returns {void} */ app.post('/get_extension', jsonParser, async (request, response) => { + const git = simpleGit(); if (!request.body.url) { return response.status(400).send('Bad Request: URL is required in the request body.'); } try { + // make sure the third-party directory exists + if (!fs.existsSync(directories.extensions + '/third-party')) { + fs.mkdirSync(directories.extensions + '/third-party'); + } + const url = request.body.url; const extensionPath = path.join(directories.extensions, 'third-party', path.basename(url, '.git')); @@ -4459,6 +4465,7 @@ app.post('/get_extension', jsonParser, async (request, response) => { * @returns {void} */ app.post('/update_extension', jsonParser, async (request, response) => { + const git = simpleGit(); if (!request.body.extensionName) { return response.status(400).send('Bad Request: extensionName is required in the request body.'); } @@ -4503,6 +4510,7 @@ app.post('/update_extension', jsonParser, async (request, response) => { * @returns {void} */ app.post('/get_extension_version', jsonParser, async (request, response) => { + const git = simpleGit(); if (!request.body.extensionName) { return response.status(400).send('Bad Request: extensionName is required in the request body.'); } From ee6bfbf4cc8fbc094a73f4bf861a4ac809168f15 Mon Sep 17 00:00:00 2001 From: BlipRanger <1860540+BlipRanger@users.noreply.github.com> Date: Thu, 13 Jul 2023 01:30:37 -0400 Subject: [PATCH 16/16] Add simple-get to packages. --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 0c310b2f8..9ab26c760 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "rimraf": "^3.0.2", "sanitize-filename": "^1.6.3", "sentencepiece-js": "^1.1.0", + "simple-git": "^3.19.1", "uniqolor": "^1.1.0", "webp-converter": "2.3.2", "ws": "^8.13.0",