Skip to content

Commit

Permalink
#80 - Thunderbird 68 support
Browse files Browse the repository at this point in the history
  • Loading branch information
guillaumev committed Oct 14, 2019
1 parent 0a1135f commit cc749a4
Show file tree
Hide file tree
Showing 8 changed files with 650 additions and 45 deletions.
23 changes: 23 additions & 0 deletions src/_locales/en/messages.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
14 changes: 14 additions & 0 deletions src/_locales/es/messages.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
14 changes: 14 additions & 0 deletions src/_locales/fr/messages.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
327 changes: 327 additions & 0 deletions src/background.js
Original file line number Diff line number Diff line change
@@ -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);
Loading

0 comments on commit cc749a4

Please sign in to comment.