Skip to content

Commit debd1eb

Browse files
committed
Add basic export/import of user sign rules (#220)
1 parent 5027201 commit debd1eb

11 files changed

+309
-7
lines changed

RuntimeMessage.d.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,17 @@ namespace RuntimeMessage {
1717
readonly method: "getUserRules";
1818
}
1919

20+
interface exportUserRules extends SignRulesMessage {
21+
readonly method: "exportUserRules";
22+
}
23+
24+
interface importUserRules extends SignRulesMessage {
25+
readonly method: "importUserRules";
26+
readonly parameters: {
27+
readonly data: any,
28+
}
29+
}
30+
2031
interface addRule extends SignRulesMessage {
2132
readonly method: "addRule";
2233
readonly parameters: {
@@ -46,7 +57,7 @@ namespace RuntimeMessage {
4657
}
4758
}
4859

49-
type Messages = getDefaultRules | getUserRules | addRule | updateRule | deleteRule;
60+
type Messages = getDefaultRules | getUserRules | exportUserRules | importUserRules | addRule | updateRule | deleteRule;
5061
}
5162

5263
namespace KeyDb {

_locales/de/messages.json

+8
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,14 @@
638638
"treeviewSigners.deleteSelectedRows": {
639639
"message": "Entferne markierte Regeln"
640640
},
641+
"treeviewSigners.exportRules": {
642+
"message": "Regeln exportieren",
643+
"description": "Button for exporting sign rules"
644+
},
645+
"treeviewSigners.importRules": {
646+
"message": "Regeln importieren",
647+
"description": "Button for importing sign rules"
648+
},
641649
"addSignersRule.title": {
642650
"message": "Füge Signierregeln hinzu"
643651
},

_locales/en_US/messages.json

+8
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,14 @@
638638
"treeviewSigners.deleteSelectedRows": {
639639
"message": "Delete selected rules"
640640
},
641+
"treeviewSigners.exportRules": {
642+
"message": "Export rules",
643+
"description": "Button for exporting sign rules"
644+
},
645+
"treeviewSigners.importRules": {
646+
"message": "Import rules",
647+
"description": "Button for importing sign rules"
648+
},
641649
"addSignersRule.title": {
642650
"message": "Add signers rule"
643651
},

content/domUtils.mjs.js

+44
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
* included in all copies or substantial portions of the Software.
88
*/
99

10+
import { Deferred } from "../modules/utils.mjs.js";
11+
1012
// @ts-check
1113

1214
/**
@@ -20,3 +22,45 @@ export function getElementById(id) {
2022
}
2123
return element;
2224
}
25+
26+
/**
27+
* Upload JSON data.
28+
*
29+
* @returns {Promise<any>}
30+
*/
31+
export function uploadJsonData() {
32+
const deferredData = new Deferred();
33+
const inputElement = document.createElement("input");
34+
inputElement.type = "file";
35+
inputElement.accept = "application/json";
36+
inputElement.addEventListener("change", (event) => {
37+
try {
38+
console.log("event:", event);
39+
console.log("files:", inputElement.files);
40+
if (!inputElement.files) {
41+
throw new Error("Input element has no file");
42+
}
43+
44+
const fileReader = new FileReader();
45+
fileReader.addEventListener("error", () => {
46+
deferredData.reject(new Error("Error reading file"));
47+
});
48+
fileReader.addEventListener("load", () => {
49+
try {
50+
if (typeof fileReader.result !== "string") {
51+
throw new Error("File content has unexpected type");
52+
}
53+
deferredData.resolve(JSON.parse(fileReader.result));
54+
} catch (error) {
55+
deferredData.reject(error);
56+
}
57+
});
58+
fileReader.readAsText(inputElement.files[0]);
59+
60+
} catch (error) {
61+
deferredData.reject(error);
62+
}
63+
});
64+
inputElement.click();
65+
return deferredData.promise;
66+
}

content/signRulesUserView.html

+2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
<button id="addSignersRule" data-i18n="treeviewSigners.addSignersRule"></button>
3737
<button id="deleteSelectedRows" data-i18n="treeviewSigners.deleteSelectedRows"></button>
3838
<button id="buttonHelp" data-i18n="button.help"></button>
39+
<button id="exportRules" data-i18n="treeviewSigners.exportRules"></button>
40+
<button id="importRules" data-i18n="treeviewSigners.importRules"></button>
3941
</div>
4042
</body>
4143

content/signRulesUserView.mjs.js

+19-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import DataTable from "./table.mjs.js";
1515
import ExtensionUtils from "../modules/extensionUtils.mjs.js";
1616
import SignRulesProxy from "../modules/dkim/signRulesProxy.mjs.js";
17-
import { getElementById } from "./domUtils.mjs.js";
17+
import { getElementById, uploadJsonData } from "./domUtils.mjs.js";
1818

1919
document.addEventListener("DOMContentLoaded", async () => {
2020
const tableElement = getElementById("rulesTable");
@@ -66,4 +66,22 @@ document.addEventListener("DOMContentLoaded", async () => {
6666
browser.i18n.getMessage("signersRuleHelp.title"),
6767
);
6868
});
69+
70+
const exportRules = getElementById("exportRules");
71+
exportRules.addEventListener("click", async () => {
72+
const exportedUserRules = await SignRulesProxy.exportUserRules();
73+
ExtensionUtils.downloadDataAsJSON(exportedUserRules, "dkim_sign_rules");
74+
});
75+
76+
const importRules = getElementById("importRules");
77+
importRules.addEventListener("click", async () => {
78+
try {
79+
// TODO: warn user about overriding existing rules
80+
const data = await uploadJsonData();
81+
await SignRulesProxy.importUserRules(data);
82+
} catch (error) {
83+
// TODO: show visible error message to user
84+
console.error("Error importing sing rules.", error);
85+
}
86+
});
6987
}, { once: true });

manifest.json

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
},
1515
"permissions": [
1616
"accountsRead",
17+
"downloads",
1718
"messagesRead",
1819
"storage",
1920
"tabs"

modules/dkim/signRules.mjs.js

+80
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,28 @@ const AUTO_ADD_RULE_FOR = {
7272
BASE_DOMAIN: 2,
7373
};
7474

75+
/**
76+
* Exported DKIM user signing rule.
77+
*
78+
* @typedef {object} DkimExportedUserSignRule
79+
* @property {string} domain
80+
* @property {string} listId
81+
* @property {string} addr
82+
* @property {string} sdid - space separated list of SDIDs
83+
* @property {number} type
84+
* @property {number} priority
85+
* @property {boolean} enabled
86+
*/
87+
88+
/**
89+
* Exported DKIM user signing rules.
90+
*
91+
* @typedef {object} DkimExportedUserSignRules
92+
* @property {"DkimExportedUserSignRules"} dataId
93+
* @property {1} dataFormatVersion
94+
* @property {DkimExportedUserSignRule[]} rules
95+
*/
96+
7597
/**
7698
* DKIM user signing rule.
7799
*
@@ -372,6 +394,56 @@ export default class SignRules {
372394
return userRules;
373395
}
374396

397+
/**
398+
* Get the user sign rules in the export format.
399+
*
400+
* @returns {Promise<DkimExportedUserSignRules>}
401+
*/
402+
static async exportUserRules() {
403+
/** @type {{id?: number, domain: string, listId: string, addr: string, sdid: string, type: number, priority: number, enabled: boolean }[]} */
404+
const rules = copy(await this.getUserRules());
405+
rules.map(rule => { delete rule.id; return rule; });
406+
return {
407+
dataId: "DkimExportedUserSignRules",
408+
dataFormatVersion: 1,
409+
rules,
410+
};
411+
}
412+
413+
/**
414+
* Import the given user sign rules.
415+
* Existing rules will be overridden.
416+
*
417+
* @param {{dataId: string, dataFormatVersion: number}} data
418+
* @returns {Promise<void>}
419+
*/
420+
static async importUserRules(data) {
421+
if (data.dataId !== "DkimExportedUserSignRules") {
422+
// TODO: proper translated error message
423+
log.error(data.dataId);
424+
throw new Error("Unknown data");
425+
}
426+
if (data.dataFormatVersion !== 1) {
427+
// TODO: proper translated error message
428+
throw new Error("Unsupported format of exported rules");
429+
}
430+
/** @type {DkimExportedUserSignRules} */
431+
// @ts-expect-error
432+
const exportedRules = data;
433+
434+
// Costly but easy way to ensure a race condition with loading the rules does not happen.
435+
await loadUserRules();
436+
437+
let maxId = 0;
438+
userRules = exportedRules.rules.map(rule => ({ id: ++maxId, ...rule }));
439+
userRulesMaxId = maxId;
440+
441+
await storeUserRules();
442+
443+
browser.runtime.sendMessage({ event: "ruleAdded" }).
444+
catch(error => log.debug("Error sending ruleAdded event", error));
445+
}
446+
375447
/**
376448
* Checks the DKIM result against the sign rules.
377449
*
@@ -658,6 +730,14 @@ export function initSignRulesProxy() {
658730
// eslint-disable-next-line consistent-return
659731
return SignRules.getUserRules();
660732
}
733+
if (request.method === "exportUserRules") {
734+
// eslint-disable-next-line consistent-return
735+
return SignRules.exportUserRules();
736+
}
737+
if (request.method === "importUserRules") {
738+
// eslint-disable-next-line consistent-return
739+
return SignRules.importUserRules(request.parameters.data);
740+
}
661741
if (request.method === "updateRule") {
662742
// eslint-disable-next-line consistent-return
663743
return SignRules.updateRule(request.parameters.id, request.parameters.propertyName, request.parameters.newValue);

modules/dkim/signRulesProxy.mjs.js

+33
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,39 @@ export default class SignRulesProxy {
6363
return browser.runtime.sendMessage(message);
6464
}
6565

66+
/**
67+
* Get the user sign rules in the export format.
68+
*
69+
* @returns {Promise<import("./signRules.mjs.js").DkimExportedUserSignRules>}
70+
*/
71+
static exportUserRules() {
72+
/** @type {RuntimeMessage.SignRules.exportUserRules} */
73+
const message = {
74+
module: "SignRules",
75+
method: "exportUserRules",
76+
};
77+
return browser.runtime.sendMessage(message);
78+
}
79+
80+
/**
81+
* Import the given user sign rules.
82+
* Existing rules will be overridden.
83+
*
84+
* @param {any} data
85+
* @returns {Promise<void>}
86+
*/
87+
static importUserRules(data) {
88+
/** @type {RuntimeMessage.SignRules.importUserRules} */
89+
const message = {
90+
module: "SignRules",
91+
method: "importUserRules",
92+
parameters: {
93+
data,
94+
},
95+
};
96+
return browser.runtime.sendMessage(message);
97+
}
98+
6699
/**
67100
* Add user rule.
68101
*

modules/extensionUtils.mjs.js

+22-5
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
///<reference path="../WebExtensions.d.ts" />
1414
/* eslint-env browser, webextensions */
1515

16-
import { promiseWithTimeout, sleep } from "./utils.mjs.js";
16+
import { dateToString, promiseWithTimeout, sleep } from "./utils.mjs.js";
1717
import Logging from "./logging.mjs.js";
1818

1919
const log = Logging.getLogger("ExtensionUtils");
@@ -46,6 +46,22 @@ async function createOrRaisePopup(url, title, height = undefined, width = undefi
4646
});
4747
}
4848

49+
/**
50+
* Download data as JSON.
51+
*
52+
* @param {object} data
53+
* @param {string} dataName
54+
* @returns {void}
55+
*/
56+
function downloadDataAsJSON(data, dataName) {
57+
const jsonBlob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
58+
browser.downloads.download({
59+
'url': URL.createObjectURL(jsonBlob),
60+
'filename': `${dataName}_${dateToString(new Date())}.json`,
61+
'saveAs': true,
62+
});
63+
}
64+
4965
/**
5066
* Checks if a message is outgoing.
5167
*
@@ -128,9 +144,10 @@ async function safeGetLocalStorage() {
128144
}
129145

130146
const ExtensionUtils = {
131-
createOrRaisePopup: createOrRaisePopup,
132-
isOutgoing: isOutgoing,
133-
safeGetLocalStorage: safeGetLocalStorage,
134-
readFile: readFile,
147+
createOrRaisePopup,
148+
downloadDataAsJSON,
149+
isOutgoing,
150+
safeGetLocalStorage,
151+
readFile,
135152
};
136153
export default ExtensionUtils;

0 commit comments

Comments
 (0)