diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json new file mode 100644 index 0000000..0b12459 --- /dev/null +++ b/src/_locales/en/messages.json @@ -0,0 +1,23 @@ +{ + "extensionName": { + "message": "Nextcloud for Filelink", + "description": "Name of the extension" + }, + "extensionDescription": { + "message": "Nextcloud provider for Thunderbird Filelink", + "description": "Description of the extension" + }, + "serviceName": { + "message": "Nextcloud", + "description": "Name of the service" + }, + "notAuthed_clickHere": { + "message": "Connect your Nextcloud account\u2026" + }, + "urlHeader": { + "message": "Nextcloud URL:" + }, + "urlDescription": { + "message": "URL to your Nextcloud server" + } +} diff --git a/src/_locales/es/messages.json b/src/_locales/es/messages.json new file mode 100644 index 0000000..eb83f18 --- /dev/null +++ b/src/_locales/es/messages.json @@ -0,0 +1,14 @@ +{ + "extensionName": { + "message": "Soporte de Nextcloud para Filelink", + "description": "Name of the extension" + }, + "extensionDescription": { + "message": "Añade soporte de Nextcloud para la característica Filelink en Thunderbird.", + "description": "Description of the extension" + }, + "serviceName": { + "message": "Nextcloud", + "description": "Name of the service" + } +} diff --git a/src/_locales/fr/messages.json b/src/_locales/fr/messages.json new file mode 100644 index 0000000..53a72ab --- /dev/null +++ b/src/_locales/fr/messages.json @@ -0,0 +1,14 @@ +{ + "extensionName": { + "message": "Nextcloud pour Filelink", + "description": "Name of the extension" + }, + "extensionDescription": { + "message": "Ajout du support de Nextcloud pour la fonction Filelink de Thunderbird.", + "description": "Description of the extension" + }, + "serviceName": { + "message": "Nextcloud", + "description": "Name of the service" + } +} diff --git a/src/background.js b/src/background.js new file mode 100644 index 0000000..8e9ed41 --- /dev/null +++ b/src/background.js @@ -0,0 +1,327 @@ +/* globals clientId, clientSecret */ +var accountsMap = new Map(); + +async function getURL(accountId) { + let accountInfo = await browser.storage.local.get([accountId]); + if (!accountInfo[accountId] || !("private_url" in accountInfo[accountId])) { + throw new Error("No URLs found."); + } + return accountInfo[accountId]; +} + +browser.runtime.onMessage.addListener(async (message, sender) => { + if (!message.accountId || !message.url) { + throw new Error("What are we doing here?"); + } + + let accountObj = accountsMap.get(message.accountId); + + switch (message.action) { + case "authorize": { + if (accountObj.authTabId) { + try { + await browser.tabs.update(accountObj.authTabId, { active: true }); + return accountObj.authPromise; + } catch (ex) { + delete accountObj.authTabId; + } + } + + accountObj.preferencesTabId = sender.tab.id; + + let callback = "http://localhost/nextcloud-callback" + + "?accountId=" + encodeURIComponent(message.accountId); + let tab = await browser.tabs.create({ + url: message.url + "/apps/oauth2/authorize" + + "?response_type=code" + + "&client_id=" + clientId + + "&redirect_uri=" + encodeURIComponent(callback), + }); + accountObj.authTabId = tab.id; + return new Promise((resolve, reject) => { + accountObj.authPromise = { resolve, reject }; + }); + } + case "updateAccountInfo": { + return accountObj.updateAccountInfo(); + } + } + return null; +}); + +browser.webRequest.onBeforeRequest.addListener(async (requestDetails) => { + let params = new URL(requestDetails.url).searchParams; + let accountId = params.get("accountId"); + let code = params.get("code"); + + let body = new FormData(); + body.append("client_id", clientId); + body.append("client_secret", clientSecret); + body.append("grant_type", "authorization_code"); + body.append("code", code); + + let response = await fetch("https://account.box.com/api/oauth2/token", { + method: "POST", + body, + }); + + let result = await response.json(); + let accountObj = accountsMap.get(accountId); + accountObj.accessToken = result.access_token; + await accountObj.setOAuthToken(result.refresh_token); + if (accountObj.authTabId) { + let tabId = accountObj.authTabId; + delete accountObj.authTabId; + await browser.tabs.remove(tabId); + } + if (accountObj.preferencesTabId) { + await browser.tabs.update(accountObj.preferencesTabId, { active: true }); + delete accountObj.preferencesTabId; + } + if (accountObj.authPromise) { + accountObj.authPromise.resolve(); + delete accountObj.authPromise; + } + + return { cancel: true }; +}, { + urls: ["http://localhost/box-dot-com-callback*"], +}, ["blocking"]); + +browser.tabs.onRemoved.addListener(async (tabId) => { + for (let accountObj of accountsMap.values()) { + if (accountObj.authTabId && tabId == accountObj.authTabId && accountObj.authPromise) { + accountObj.authPromise.reject(); + delete accountObj.authPromise; + delete accountObj.authTabId; + } + } +}); + +class Account { + constructor(accountId) { + this.accountId = accountId; + this.uploads = new Map(); + } + + async loadAccountFromStorage() { + if (this.accountInfo) { + return; + } + + let info = await browser.storage.local.get({ [this.accountId]: {} }); + this.accountInfo = info[this.accountId]; + } + + async saveAccountToStorage() { + await browser.storage.local.set({ [this.accountId]: this.accountInfo }); + } + + async ensureAccessToken() { + if (this.accessToken) { + return; + } + + let refreshToken = await this.getOAuthToken(); + + let body = new FormData(); + body.append("client_id", clientId); + body.append("client_secret", clientSecret); + body.append("grant_type", "refresh_token"); + body.append("refresh_token", refreshToken); + + let response = await fetch("https://account.box.com/api/oauth2/token", { + method: "POST", + body, + }); + let result = await response.json(); + + await this.loadAccountFromStorage(); + this.accessToken = result.access_token; + this.accountInfo.refreshToken = result.refresh_token; + await this.saveAccountToStorage(); + } + + async updateAccountInfo() { + await this.ensureAccessToken(); + + let response = await fetch("https://api.box.com/2.0/users/me", { + headers: { Authorization: `Bearer ${this.accessToken}` }, + }); + + let result = await response.json(); + return browser.cloudFile.updateAccount(this.accountId, { + uploadSizeLimit: result.max_upload_size, + spaceRemaining: result.space_amount - result.space_used, + spaceUsed: result.space_used, + }); + } + + async getFolder() { + await this.loadAccountFromStorage(); + if ("folderId" in this.accountInfo) { + return this.accountInfo.folderId; + } + + await this.ensureAccessToken(); + + let response = await fetch("https://api.box.com/2.0/folders/0", { + headers: { Authorization: `Bearer ${this.accessToken}` }, + }); + let result = await response.json(); + for (let item of result.item_collection.entries) { + if (item.type == "folder" && item.name == "Thunderbird") { + this.accountInfo.folderId = item.id; + await this.saveAccountToStorage(); + return item.id; + } + } + + response = await fetch("https://api.box.com/2.0/folders", { + method: "POST", + headers: { Authorization: `Bearer ${this.accessToken}` }, + body: JSON.stringify({ + parent: { id: "0" }, + name: "Thunderbird", + }), + }); + result = await response.json(); + if (result.id) { + this.accountInfo.folderId = result.id; + await this.saveAccountToStorage(); + return result.id; + } + + throw new Error("Failed to get the folder"); + } + + async setOAuthToken(token) { + await this.loadAccountFromStorage(); + this.accountInfo.refreshToken = token; + await this.saveAccountToStorage(); + await browser.cloudFile.updateAccount(this.accountId, { configured: true }); + } + + async getOAuthToken() { + await this.loadAccountFromStorage(); + if (!this.accountInfo.refreshToken) { + throw new Error("No OAuth token found."); + } + return this.accountInfo.refreshToken; + } + + async uploadFile(id, name, data) { + await this.ensureAccessToken(); + let folderId = await this.getFolder(); + let body = new FormData(); + body.append("attributes", JSON.stringify({ + name, + parent: { id: folderId }, + })); + body.append("file", new Blob([data])); + let uploadInfo = { abortController: new AbortController() }; + this.uploads.set(id, uploadInfo); + + let response = await fetch("https://upload.box.com/api/2.0/files/content", { + method: "POST", + headers: { Authorization: `Bearer ${this.accessToken}` }, + body, + signal: uploadInfo.abortController.signal, + }); + + let result = await response.json(); + if (result.total_count && result.total_count > 0) { + let fileId = result.entries[0].id; + uploadInfo.fileId = fileId; + + response = await fetch(`https://api.box.com/2.0/files/${fileId}`, { + method: "PUT", + headers: { Authorization: `Bearer ${this.accessToken}` }, + body: JSON.stringify({ shared_link: { access: "open" } }), + signal: uploadInfo.abortController.signal, + }); + result = await response.json(); + + if (result && result.shared_link && result.shared_link.url) { + delete uploadInfo.abortController; + return { url: result.shared_link.url }; + } + } + + delete uploadInfo.abortController; + throw new Error("Upload failed."); + } + + abortUploadFile(id) { + let uploadInfo = this.uploads.get(id); + if (uploadInfo && uploadInfo.abortController) { + uploadInfo.abortController.abort(); + } + } + + async deleteFile(id) { + let uploadInfo = this.uploads.get(id); + if (!uploadInfo || !uploadInfo.fileId) { + return; + } + + let response = await fetch(`https://api.box.com/2.0/files/${uploadInfo.fileId}`, { + method: "DELETE", + headers: { Authorization: `Bearer ${this.accessToken}` }, + }); + if (response.status == 204) { + return; + } + + throw new Error("Delete failed."); + } +} + +browser.cloudFile.onFileUpload.addListener(async (account, { id, name, data }) => { + let accountObj = accountsMap.get(account.id); + return accountObj.uploadFile(id, name, data); +}); + +browser.cloudFile.onFileUploadAbort.addListener((account, id) => { + let accountObj = accountsMap.get(account.id); + return accountObj.abortUploadFile(id); +}); + +browser.cloudFile.onFileDeleted.addListener(async (account, id) => { + let accountObj = accountsMap.get(account.id); + return accountObj.deleteFile(id); +}); + +browser.cloudFile.getAllAccounts().then(async (accounts) => { + for (let account of accounts) { + try { + let accountObj = new Account(account.id); + accountsMap.set(account.id, accountObj); + await accountObj.getOAuthToken(); + await browser.cloudFile.updateAccount(account.id, { configured: true }); + } catch (ex) { + } + } +}); + +browser.cloudFile.onAccountAdded.addListener((account) => { + let accountObj = new Account(account.id); + accountsMap.set(account.id, accountObj); +}); + +browser.cloudFile.onAccountDeleted.addListener((accountId) => { + accountsMap.delete(accountId); +}); + +/* eslint-disable */ +// Thinly-veiled attempt to hide the client secret. Don't waste your time +// decoding this. Get your own, it's easy. +((z)=>{ let a=b=>z[ "\x53\x74\x72\x69\x6e\x67"]["\x66\x72\x6f\x6d\x43\x68\x61"+ +"\x72\x43\x6f\x64\x65"]["\x61\x70\x70\x6c\x79"](null, z["\x41\x72\x72\x61\x79"] +["\x66\x72\x6f\x6d"](b,c => c["\x63\x68\x61\x72\x43\x6f\x64\x65\x41\x74"](0)-b[ +"\x6c\x65\x6e\x67\x74\x68"]%(6-1)));z[a("\x66\x6f\x6c\x68\x71\x77\x4c\x67")]=a( +"\x3b\x65\x67\x38\x77\x34\x69\x69\x71\x73\x3a\x75\x3b\x6e\x34\x6c\x6b\x6e\x36"+ +"\x66\x78\x6b\x68\x72\x34\x65\x72\x75\x79\x6b\x70\x39");z[a("\x65\x6e\x6b\x67"+ +"\x70\x76\x55\x67\x65\x74\x67\x76" )]=a("\x54\x43\x68\x67\x5c\x3a\x32\x73\x52"+ +"\x6a\x6b\x45\x72\x49\x54\x79\x59\x4c\x34\x7a\x6c\x53\x37\x6d\x6d\x73\x43\x53"+ +"\x73\x75\x67\x6d")})(this); diff --git a/src/install.rdf b/src/install.rdf deleted file mode 100644 index cef9e81..0000000 --- a/src/install.rdf +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - owncloud@viguierjust.com - Nextcloud for Filelink - 1.8 - 2 - Olivier Paroz - Philipp Kewisch - Mark James - Guillaume Viguier-Just - - Nextcloud provider for Thunderbird Filelink - https://github.com/nextcloud/nextcloud-filelink - chrome://nextcloud/content/about.xul - - - - {3550f703-e582-4d05-9a08-453d09bdfdc6} - 17.0 - 60.* - - - - - - es - Soporte de Nextcloud para Filelink - Añade soporte de Nextcloud para la característica Filelink en Thunderbird. - - - - - - fr - Nextcloud pour Filelink - Ajout du support de Nextcloud pour la fonction Filelink de Thunderbird - - - - - diff --git a/src/management.html b/src/management.html new file mode 100644 index 0000000..7ac4abd --- /dev/null +++ b/src/management.html @@ -0,0 +1,127 @@ + + + + + + + + + +

+ + + + diff --git a/src/management.js b/src/management.js new file mode 100644 index 0000000..5157f25 --- /dev/null +++ b/src/management.js @@ -0,0 +1,107 @@ +let form = document.querySelector("form"); +let url = form.querySelector(`input[name="url"]`); +let username = form.querySelector(`input[name="username"]`); +let password = form.querySelector(`input[name="password"]`); + +let accountId = new URL(location.href).searchParams.get("accountId"); + +let notAuthed = document.getElementById("notAuthed"); +let clickMe = document.getElementById("clickMe"); +let authed = document.getElementById("authed"); +let loading = document.getElementById("provider-loading"); +let spaceBox = document.getElementById("provider-spacebox"); + +(() => { + for (let element of document.querySelectorAll("[data-message]")) { + element.textContent = browser.i18n.getMessage(element.dataset.message); + } + updateUI(); +})(); + +browser.storage.local.get([accountId]).then(accountInfo => { + if (accountId in accountInfo) { + if ("url" in accountInfo[accountId]) { + url.value = accountInfo[accountId].url; + } + } +}); + +clickMe.onclick = async () => { + /*await browser.runtime.sendMessage({ + action: "authorize", + accountId, + url: url.value, + }); + updateUI();*/ + console.log(username.value); + console.log(password.value); +}; + +async function updateUI() { + let account = await browser.cloudFile.getAccount(accountId); + if (account.configured) { + notAuthed.hidden = true; + authed.hidden = false; + spaceBox.hidden = true; + loading.hidden = false; + + if (account.uploadSizeLimit == -1) { + account = await browser.runtime.sendMessage({ accountId, action: "updateAccountInfo" }); + } + + foo(account.spaceUsed / (account.spaceUsed + account.spaceRemaining)); + + document.getElementById("file-space-used").textContent = formatFileSize(account.spaceUsed); + document.getElementById("remaining-file-space").textContent = formatFileSize(account.spaceRemaining); + document.querySelector("svg > text").textContent = formatFileSize(account.spaceUsed + account.spaceRemaining); + + spaceBox.hidden = false; + loading.hidden = true; + } else { + notAuthed.hidden = false; + authed.hidden = true; + } +} + +function foo(fraction) { + if (fraction < 0 || fraction > 1) { + throw new Error("Invalid fraction"); + } + + let path = document.querySelector("path#thisone"); + let angle = 2 * Math.PI * fraction; + + let x1 = 100 + Math.sin(angle) * 100; + let y1 = 100 - Math.cos(angle) * 100; + let x2 = 100 + Math.sin(angle) * 40; + let y2 = 100 - Math.cos(angle) * 40; + + let gcOutside = fraction <= 0.5 ? 0 : 1; + let gcInside = fraction <= 0.5 ? 0 : 1; + + path.setAttribute("d", `M 100,0 A 100,100 0 ${gcOutside} 1 ${x1},${y1} L ${x2},${y2} A 40,40 0 ${gcInside} 0 100,60 Z`); +} + +function formatFileSize(bytes) { + let value = bytes; + let unit = "B"; + if (value > 999) { + value /= 1024; + unit = "kB"; + } + if (value >= 999.5) { + value /= 1024; + unit = "MB"; + } + if (value >= 999.5) { + value /= 1024; + unit = "GB"; + } + + if (value < 100 && unit != "B") { + value = value.toFixed(1); + } else { + value = value.toFixed(0); + } + return `${value} ${unit}`; +} diff --git a/src/manifest.json b/src/manifest.json new file mode 100644 index 0000000..84bf855 --- /dev/null +++ b/src/manifest.json @@ -0,0 +1,38 @@ +{ + "manifest_version": 2, + "applications": { + "gecko": { + "id": "owncloud@viguierjust.com", + "strict_min_version": "68.0" + } + }, + "name": "__MSG_extensionName__", + "description": "__MSG_extensionDescription__", + "homepage_url": "https://github.com/nextcloud/nextcloud-filelink", + "author": "Olivier Paroz", + "developer": { + "name": "Guillaume Viguier-Just", + "url": "https://www.gvj-web.com" + }, + "version": "2.0", + "default_locale": "en", + "icons": { + "128": "icon.png", + "64": "icon64.png" + }, + "background": { + "scripts": [ + "background.js" + ] + }, + "permissions": [ + "http://localhost/*", + "storage", + "webRequest", + "webRequestBlocking" + ], + "cloud_file": { + "name": "__MSG_serviceName__", + "management_url": "management.html" + } +}