diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..46eed58 --- /dev/null +++ b/build.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cd src; zip -r ../../nextcloud-filelink-$1.xpi . diff --git a/screenshots/addon-about.png b/screenshots/addon-about.png deleted file mode 100644 index 4efa825..0000000 Binary files a/screenshots/addon-about.png and /dev/null differ diff --git a/screenshots/config.png b/screenshots/config.png new file mode 100644 index 0000000..42b139f Binary files /dev/null and b/screenshots/config.png differ diff --git a/screenshots/configured.png b/screenshots/configured.png deleted file mode 100644 index b98b07e..0000000 Binary files a/screenshots/configured.png and /dev/null differ diff --git a/screenshots/setup.png b/screenshots/setup.png deleted file mode 100644 index 74afdcd..0000000 Binary files a/screenshots/setup.png and /dev/null differ diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json new file mode 100644 index 0000000..309a393 --- /dev/null +++ b/src/_locales/en/messages.json @@ -0,0 +1,38 @@ +{ + "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" + }, + "serverURLLabel": { + "message": "WebDAV URL" + }, + "serverURLDesc": { + "message": "Your Personal WebDAV file path can be found under Settings in the lower left of the Files screen." + }, + "usernameLabel": { + "message": "Username" + }, + "usernameDesc": { + "message": "Your NextCloud username" + }, + "tokenLabel": { + "message": "App Token / Password" + }, + "tokenDesc": { + "message": "An app token (preferrable) or password. Generate an app token via User -> Settings -> Security -> App Token. WARNING: this token is stored insecurely. NOTE: this may require the OAuth2 plugin on the server." + }, + "pathLabel": { + "message": "Storage Path" + }, + "pathDesc": { + "message": "The path where the files are stored in OwnCloud (it must already exist on the server)" + } +} diff --git a/src/background.js b/src/background.js new file mode 100644 index 0000000..ca64ca8 --- /dev/null +++ b/src/background.js @@ -0,0 +1,184 @@ +/** + * @copyright Copyright (c) 2020, Thomas Spellman (thos37@gmail.com) + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +var uploads = new Map(); + +// browser.cloudFile.onAccountAdded.addListener(async (account) => { +// //console.log("Account Added", account.id) +// }) + +async function getAccountInfo(accountId) { + let accountInfo = await browser.storage.local.get([accountId]); + if (!accountInfo[accountId] || !("webdavUrl" in accountInfo[accountId])) { + throw new Error("No Accounts found."); + } + return accountInfo[accountId]; +} + +browser.cloudFile.onFileUpload.addListener(async (account, params) => { + + let { id, name, data } = params; + + name = "" + Date.now() + "_" + name; + + console.log("onFileUpload", id, account, name); + + let accountInfo = await getAccountInfo(account.id); + + //console.log("accountInfo", accountInfo); + + let uploadInfo = { + id, + name, + abortController: new AbortController(), + }; + + uploads.set(id, uploadInfo); + + let {webdavUrl, username, token, path} = accountInfo; + + const authHeader = "Basic " + btoa(username + ":" + token); + + let url = webdavUrl + path + encodeURIComponent(name); + + let headers = { + "Content-Type": "application/octet-stream", + Authorization: authHeader + }; + let fetchInfo = { + method: "PUT", + headers, + body: data, + signal: uploadInfo.abortController.signal, + }; + + //console.log("uploading to ", url, fetchInfo); + + let response = await fetch(url, fetchInfo); + + //console.log("file upload response", response); + + delete uploadInfo.abortController; + if (response.status > 299) { + throw new Error("response was not ok: server status code: " + response.status + ", response message: " + response.statusText); + } + + const serverUrl = webdavUrl.substr(0, webdavUrl.indexOf("remote.php")); + const shareUrl = serverUrl + "ocs/v1.php/apps/files_sharing/api/v1/shares?format=json"; + + uploadInfo.abortController = new AbortController(); + uploads.set(id, uploadInfo); + + headers = { + "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", + Authorization: authHeader, + "OCS-APIRequest": true + }; + + fetchInfo = { + method: "POST", + headers, + body: "shareType=3&path=" + encodeURIComponent(path + name), + signal: uploadInfo.abortController.signal, + }; + + console.log("requesting public link", shareUrl, fetchInfo); + + response = await fetch(shareUrl, fetchInfo); + + //console.log("public link response", response); + + if(response.ok) + { + let respJson = await response.json(); + + uploadInfo.shareID = respJson.ocs.data.id; + uploads.set(id, uploadInfo); + + return {url: respJson.ocs.data.url + "/download"}; + } + else + return {aborted: true} + +}); + +browser.cloudFile.onFileUploadAbort.addListener((account, id) => { + //console.log("aborting upload", id); + let uploadInfo = uploads.get(id); + if (uploadInfo && uploadInfo.abortController) { + uploadInfo.abortController.abort(); + } +}); + +browser.cloudFile.onFileDeleted.addListener(async (account, id) => { + //console.log("delete upload", id); + let uploadInfo = uploads.get(id); + if (!uploadInfo) { + return; + } + + // FIXME how do we get a confirmation popup in TB MailExtensions? + // let wishDelete = confirm("Do you wish to delete the file on the server?"); + // if(!wishDelete){ + // return; + // } + + let accountInfo = await getAccountInfo(account.id); + + let {shareID} = uploadInfo; + let {webdavUrl, username, token} = accountInfo; + + const authHeader = "Basic " + btoa(username + ":" + token); + + const serverUrl = webdavUrl.substr(0, webdavUrl.indexOf("remote.php")); + const shareUrl = serverUrl + "ocs/v1.php/apps/files_sharing/api/v1/shares/" + shareID; + + let headers = { + Authorization: authHeader, + "OCS-APIRequest": true + }; + + let fetchInfo = { + headers, + method: "DELETE", + }; + + //console.log("sending delete", url, fetchInfo); + + let response = await fetch(shareUrl, fetchInfo); + + //console.log("delete response", response); + + uploads.delete(id); + + if (response.status > 299) { + throw new Error("response was not ok: server status code: " + response.status + ", response message: " + response.statusText); + } + +}); + +browser.cloudFile.getAllAccounts().then(async (accounts) => { + let allAccountsInfo = await browser.storage.local.get(); + for (let account of accounts) { + await browser.cloudFile.updateAccount(account.id, { + configured: account.id in allAccountsInfo, + }); + } +}); diff --git a/src/chrome.manifest b/src/chrome.manifest deleted file mode 100644 index c53401f..0000000 --- a/src/chrome.manifest +++ /dev/null @@ -1,10 +0,0 @@ -content nextcloud chrome/content/ -locale nextcloud en chrome/locale/en/ -locale nextcloud fr chrome/locale/fr/ -locale nextcloud pl chrome/locale/pl/ -locale nextcloud de chrome/locale/de/ -locale nextcloud nl chrome/locale/nl/ -locale nextcloud es chrome/locale/es/ -component {ad8c3b77-7dc8-41d1-8985-5be88b254ff3} components/nsNextcloud.js -contract @mozilla.org/mail/nextcloud;1 {ad8c3b77-7dc8-41d1-8985-5be88b254ff3} -category cloud-files Nextcloud @mozilla.org/mail/nextcloud;1 diff --git a/src/chrome/content/about.xul b/src/chrome/content/about.xul deleted file mode 100644 index 1e1a7c1..0000000 --- a/src/chrome/content/about.xul +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + diff --git a/src/management.js b/src/management.js new file mode 100644 index 0000000..f578aa8 --- /dev/null +++ b/src/management.js @@ -0,0 +1,183 @@ +/** + * @copyright Copyright (c) 2020, Thomas Spellman (thos37@gmail.com) + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +let form = document.querySelector("form"); +let webdavUrl = form.querySelector(`input[name="webdavUrl"]`); +let username = form.querySelector(`input[name="username"]`); +let token = form.querySelector(`input[name="token"]`); +let path = form.querySelector(`input[name="path"]`); +let button = form.querySelector("button"); +let accountId = new URL(location.href).searchParams.get("accountId"); + +(() => { + for (let element of document.querySelectorAll("[data-message]")) { + element.textContent = browser.i18n.getMessage(element.dataset.message); + } +})(); + + +browser.storage.local.get([accountId]).then(accountInfo => { + if (accountId in accountInfo) { + if ("webdavUrl" in accountInfo[accountId]) { + webdavUrl.value = accountInfo[accountId].webdavUrl; + } + if ("username" in accountInfo[accountId]) { + username.value = accountInfo[accountId].username; + } + if ("token" in accountInfo[accountId]) { + token.value = accountInfo[accountId].token; + } + if ("path" in accountInfo[accountId]) { + path.value = accountInfo[accountId].path; + } + } +}); + +button.onclick = async () => { + + if (!form.checkValidity()) { + console.log("form is invalid"); + return; + } + + webdavUrl.disabled = username.disabled = token.disabled = path.disabled = button.disabled = true; + let webdavUrl_value = webdavUrl.value; + if (!webdavUrl_value.endsWith("/")) { + webdavUrl_value += "/"; + webdavUrl.value = webdavUrl_value; + } + let path_value = path.value; + if (!path_value.endsWith("/")) { + path_value += "/"; + path.value = path_value; + } + if (!path_value.startsWith("/")) { + path_value = "/" + path_value; + path.value = path_value; + } + + let start = Date.now(); + await browser.storage.local.set({ + [accountId]: { + webdavUrl: webdavUrl_value, + username: username.value, + token: token.value, + path: path_value + }, + }); + await browser.cloudFile.updateAccount(accountId, { configured: true }); + setTimeout(() => { + webdavUrl.disabled = username.disabled = token.disabled = path.disabled = button.disabled = false; + }, Math.max(0, start + 500 - Date.now())); +}; + +// let notAuthed = document.getElementById("notAuthed"); +// let authed = document.getElementById("authed"); +// let loading = document.getElementById("provider-loading"); +// let spaceBox = document.getElementById("provider-spacebox"); + + +// browser.storage.local.get([accountId]).then(accountInfo => { +// if (accountId in accountInfo) { +// if ("webdavUrl" in accountInfo[accountId]) { +// webdavUrl.value = accountInfo[accountId].webdavUrl; +// } +// } +// }); + +// 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..365f90c --- /dev/null +++ b/src/manifest.json @@ -0,0 +1,36 @@ +{ + "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": "Thomas Spellman", + "developer": { + "name": "Thomas Spellman", + "url": "https://code.thosmos.com" + }, + "version": "2.0", + "default_locale": "en", + "icons": { + "128": "icon.png", + "64": "icon64.png" + }, + "background": { + "scripts": [ + "background.js" + ] + }, + "permissions": [ + "", + "storage" + ], + "cloud_file": { + "name": "__MSG_serviceName__", + "management_url": "management.html" + } +}