diff options
37 files changed, 1896 insertions, 1810 deletions
@@ -17,7 +17,6 @@ TODO: settings and settings for pages that currently happen to live in iframes - add some nice styling to settings page -- stop using modules (not available on all browsers) -- CRUCIAL - use non-predictable value in place of "myext-allow", utilizing hashes -- CRUCIAL - rename the extension to something good - port to gecko-based browsers -- CRUCIAL @@ -37,8 +36,11 @@ TODO: - check if prerendering has to be blocked -- CRUCIAL - block prefetch - rearrange files in extension, add some mechanism to build the extension +- all solutions to modularize js code SUCK; come up with own simple DSL + to manage imports/exports DONE: +- stop using modules (not available on all browsers) -- DONE 2021-05-12 - clean up the remnants of LibreJS -- DONE 2021-05-12 - implement whitelisting -- DONE 2021-05-07 - find way to also block scripts in non-http pages (e.g. file://) -- DONE 2021-05-07 (via content scripts, may not be perfect) diff --git a/background/background.html b/background/background.html index 519c2a2..d453c47 100644 --- a/background/background.html +++ b/background/background.html @@ -2,6 +2,18 @@ <html lang="en"> <head> <meta charset="utf-8"> - <script src="./main.mjs" type="module"></script> + <script src="/common/stored_types.js"></script> + <script src="/common/lock.js"></script> + <script src="/common/once.js"></script> + <script src="/common/browser.js"></script> + <script src="./storage.js"></script> + <script src="./message_server.js"></script> + <script src="/common/connection_types.js"></script> + <script src="./storage_server.js"></script> + <script src="./url_item.js"></script> + <script src="./sha256.js"></script> + <script src="./page_actions_server.js"></script> + <script src="./policy_smuggler.js"></script> + <script src="./main.js"></script> </head> </html> diff --git a/background/main.js b/background/main.js new file mode 100644 index 0000000..f4b01f8 --- /dev/null +++ b/background/main.js @@ -0,0 +1,168 @@ +/** +* Myext main background script +* +* Copyright (C) 2021 Wojtek Kosior +* +* Dual-licensed under: +* - 0BSD license +* - GPLv3 or (at your option) any later version +*/ + +"use strict"; + +(() => { + const TYPE_PREFIX = window.TYPE_PREFIX; + const get_storage = window.get_storage; + const start_storage_server = window.start_storage_server; + const start_page_actions_server = window.start_page_actions_server; + const start_policy_smuggler = window.start_policy_smuggler; + const browser = window.browser; + + start_storage_server(); + start_page_actions_server(); + start_policy_smuggler(); + + async function init_myext(install_details) + { + console.log("details:", install_details); + if (install_details.reason != "install") + return; + + let storage = await get_storage(); + + await storage.clear(); + + /* + * Below we add sample settings to the extension. + * Those should be considered example values for viewing in the options + * page. They won't make my.fsf.org work. The only scripts that does + * something useful right now is the opencores one. + */ + + let components = []; + for (let script_data of [ + {url: "http://127.0.0.1:8000/myfsf_define_CRM.js", + hash:"bf0cc81c7e8d5f800877b4bc3f14639f946f5ac6d4dc120255ffac5eba5e48fe"}, + {url: "https://my.fsf.org/misc/jquery.js?v=1.4.4", + hash:"261ae472fa0cbf27c80c9200a1599a60fde581a0e652eee4bf41def8cb61f2d0"}, + {url: "https://my.fsf.org/misc/jquery-extend-3.4.0.js?v=1.4.4", + hash:"c54103ba57ee210ca55c052e70415402707548a4e6a68dd6efb3895019bee392"}, + {url: "https://my.fsf.org/misc/jquery-html-prefilter-3.5.0-backport.js?v=1.4.4", + hash:"fad84efa145fb507e5df9b582fa01b1c4e6313de7f72ebdd55726d92fa4dbf06"}, + {url: "https://my.fsf.org/misc/jquery.once.js?v=1.2", + hash:"1430f42c0d760ba8e05bb3762480502e541f654fec5739ee40625ab22dc38c4f"}, + {url: "https://my.fsf.org/misc/drupal.js?qmaukd", + hash:"2e08dccbd4d8b728a6871562995a4636b89bfe0ed3b8fb0138191c922228b116"}, + {url: "https://my.fsf.org/sites/all/modules/civicrm/bower_components/jquery/dist/jquery.min.js?qmaukd", + hash:"a6d01520d28d15dbe476de84eea90eb3ee2d058722efc062ec73cb5fad78a17b"}, + {url: "https://my.fsf.org/sites/all/modules/civicrm/bower_components/jquery-ui/jquery-ui.min.js?qmaukd", + hash:"28ce75d953678c4942df47a11707a15e3c756021cf89090e3e6aa7ad6b6971c3"}, + {url: "https://my.fsf.org/sites/all/modules/civicrm/bower_components/lodash-compat/lodash.min.js?qmaukd", + hash:"f2871cc80c52fe8c04c582c4a49797c9c8fd80391cf1452e47f7fe97835ed5cc"}, + {url: "https://my.fsf.org/sites/all/modules/civicrm/packages/jquery/plugins/jquery.mousewheel.min.js?qmaukd", + hash:"f50233e84c2ac7ada37a094d3f7d3b3f7c97716d6b7b47bf69619d93ee4ac1ce"}, + {url: "https://my.fsf.org/sites/all/modules/civicrm/bower_components/select2/select2.min.js?qmaukd", + hash:"ce61298fb9aa4ec49ccd4172d097e36a9e5db3af06a7b82796659368f15b7c1b"}, + {url: "https://my.fsf.org/sites/all/modules/civi crm/packages/jquery/plugins/jquery.form.min.js?qmaukd", + hash:"c90f0e501d2948fbc2b61bffd654fa4ab64741fd48923782419eeb14d3816fb8"}, + {url: "https://my.fsf.org/sites/all/modules/civicrm/packages/jquery/plugins/jquery.timeentry.min.js?qmaukd", + hash:"8e85df981e8ad7049d06dfb075277d038734d36a7097c7f021021b2bdccfe9bb"}, + {url: "https://my.fsf.org/sites/all/modules/civicrm/packages/jquery/plugins/jquery.blockUI.min.js?qmaukd", + hash:"806aedff52ac822f2adc5797073e1e5c5cec32eb9f15f2319cb32a347dcd232b"}, + {url: "https://my.fsf.org/sites/all/modules/civicrm/bower_components/datatables/media/js/jquery.dataTables.min.js?qmaukd", + hash:"b796504d9b1b422f0dc6ccc2d740ac78a8c9e5078cc3934836d39742b1121925"}, + {url: "https://my.fsf.org/sites/all/modules/civicrm/bower_components/jquery-validation/dist/jquery.validate.min.js?qmaukd", + hash:"f0f5373ad203101ea91bf826c5a7ef8f7cd74887f06bad2cb9277a504503b9e2"}, + {url: "https://my.fsf.org/sites/all/modules/civicrm/packages/jquery/plugins/jquery.ui.datepicker.validation.min.js?qmaukd", + hash:"c6e6f6bf7f8fff25cca338045774e267e8eaa2d48ac9100540f3d59a6d2b3c61"}, + {url: "https://my.fsf.org/sites/all/modules/civicrm/js/Common.js?qmaukd", + hash:"17aa222a3af2e8958be16accb5e77ef39f67009cb3b500718d8fffd45b399148"}, + {url: "https://my.fsf.org/sites/all/modules/civicrm/js/crm.datepicker.js?qmaukd", + hash:"9bd8d10208aa99c156325f7819da6f0dd62ba221ac4119c3ccd4834e2cf36535"}, + {url: "https://my.fsf.org/sites/all/modules/civicrm/js/crm.ajax.js?qmaukd", + hash:"6401a4e257b7499ae4a00be2c200e4504a2c9b3d6b278a830c31a7b63374f0fe"}, + {url: "https://my.fsf.org/sites/all/modules/civicrm/js/wysiwyg/crm.wysiwyg.js?qmaukd", + hash:"fa962356072a36672c3b4b25bdeb657f020995a067e20a29cd5bb84b05157762"}, + {url: "https://my.fsf.org/sites/all/modules/civicrm/js/noconflict.js?qmaukd", + hash:"58d6d9f496a235d23cf891926d71f2104e4f2afe1d14bb4e2b5233f646c35e62"}, + {url: "https://my.fsf.org/sites/all/modules/matomo/matomo.js?qmaukd", + hash:"7f39ccd085f348189cd2fb62ea4d4a658d96f6bba266265880b98605e777e2de"}, + {url: "https://my.fsf.org/sites/all/themes/fsf_venture/js/global.js?qmaukd", + hash:"aa7983f6b902f9f4415cfc8346e0c3f194cc95b78f52f2ad09ec7effa1326b9c"}, + {url: "https://my.fsf.org/sites/all/themes/fsf_venture/js/jquery.superfish.min.js?qmaukd", + hash:"5ef1f93bf3901227056bf9ed0ed93a148eec4dda30f419756b12bedd1098815e"}, + {url: "https://my.fsf.org/sites/all/themes/fsf_venture/js/jquery.sidr.min.js?qmaukd", + hash:"c4914d415826676c6af2e61f16edb72c5388f8600ba6de9049892aee49d980a0"}, + {url: "https://my.fsf.org/sites/all/themes/fsf_venture/js/jquery.flexslider.min.js?qmaukd", + hash:"cefaf715761b4494913851249b9d40dacb4a8cb61242b0efc859dc586d56e0d4"}, + {url: "http://127.0.0.1:8000/myfsf_crap.js", + hash:"d91ccf21592d0f861ea0ba946bc257fc5d88269327cad0a91387da6cb8ff633e"}, + {url: "https://my.fsf.org/sites/all/modules/civicrm/templates/CRM/Core/BillingBlock.js?r=lp7Di", + hash:"2f25d35e7a0c0060ab0a444a577f09dd3c9934ae898a7ee0eb20b6c986ab5a1c"}, + {url: "https://my.fsf.org/extensions/com.aghstrategies.giftmemberships/js/giftpricefield.js?r=lp7Di", + hash:"f86080e6bd306fe46474039aeca2808235005bce5a2a29416d08210022039a45"}, + {url: "https://my.fsf.org/extensions/com.ginkgostreet.negativenegator/js/negativenegator.js?r=lp7Di", + hash:"d0e87bac832856db70947d82a7ab4e0b7c8b1070d5f1a32335345e033ece3a14"} + ]) { + let name_regex = /\/([^/]+)\.js/; + let name = name_regex.exec(script_data.url)[1]; + await storage.set(TYPE_PREFIX.SCRIPT, name, script_data); + components.push([TYPE_PREFIX.SCRIPT, name]); + } + + await storage.set(TYPE_PREFIX.PAGE, "https://my.fsf.org/join", {components}); + + let hello_script = { + text: "console.log(\"hello, every1!\");\n" + }; + await storage.set(TYPE_PREFIX.SCRIPT, "hello", hello_script); + await storage.set(TYPE_PREFIX.BUNDLE, "hello", + [[TYPE_PREFIX.SCRIPT, "hello"]]); + await storage.set(TYPE_PREFIX.PAGE, "https://my.fsf.org/", { + components: [[TYPE_PREFIX.BUNDLE, "hello"]], + allow: true + }); + + let opencores_script = { + text: `\ +let data = JSON.parse(document.getElementById("__NEXT_DATA__").textContent); +let sections = {}; +for (let h1 of document.getElementsByClassName("cMJCrc")) { + let ul = document.createElement("ul"); + if (h1.nextElementSibling !== null) + h1.parentNode.insertBefore(ul, h1.nextElementSibling); + else + h1.parentNode.appendChild(ul); + + sections[h1.children[1].firstChild.textContent] = ul; +} + +for (let prop of data.props.pageProps.list) { + let ul = sections[prop.category]; + if (ul === undefined) { + console.log(\`unknown category "\${prop.category}" for project "\${prop.title}"\`); + continue; + } + + let li = document.createElement("li"); + let a = document.createElement("a"); + a.setAttribute("href", "/projects/" + prop.slug); + a.textContent = prop.title; + + li.appendChild(a); + ul.appendChild(li); +} +` + }; + + await storage.set(TYPE_PREFIX.SCRIPT, "opencores", opencores_script); + await storage.set(TYPE_PREFIX.PAGE, "https://opencores.org/projects", { + components: [[TYPE_PREFIX.SCRIPT, "opencores"]], + allow: false + }); + } + + browser.runtime.onInstalled.addListener(init_myext); + + console.log("hello, myext"); +})(); diff --git a/background/main.mjs b/background/main.mjs deleted file mode 100644 index ee14c74..0000000 --- a/background/main.mjs +++ /dev/null @@ -1,166 +0,0 @@ -/** -* Myext main background script -* -* Copyright (C) 2021 Wojtek Kosior -* -* Dual-licensed under: -* - 0BSD license -* - GPLv3 or (at your option) any later version -*/ - -import {TYPE_PREFIX} from '/common/stored_types.mjs'; -import get_storage from './storage.mjs'; -import start_storage_server from './storage_server.mjs'; -import start_page_actions_server from './page_actions_server.mjs'; -import start_policy_smuggler from './policy_smuggler.mjs'; -import browser from '/common/browser.mjs'; - -"use strict"; - -start_storage_server(); -start_page_actions_server(); -start_policy_smuggler(); - -async function init_myext(install_details) -{ - console.log("details:", install_details); - if (install_details.reason != "install") - return; - - let storage = await get_storage(); - - await storage.clear(); - - /* - * Below we add sample settings to the extension. - * Those should be considered example values for viewing in the options - * page. They won't make my.fsf.org work. The only scripts that does - * something useful right now is the opencores one. - */ - - let components = []; - for (let script_data of [ - {url: "http://127.0.0.1:8000/myfsf_define_CRM.js", - hash:"bf0cc81c7e8d5f800877b4bc3f14639f946f5ac6d4dc120255ffac5eba5e48fe"}, - {url: "https://my.fsf.org/misc/jquery.js?v=1.4.4", - hash:"261ae472fa0cbf27c80c9200a1599a60fde581a0e652eee4bf41def8cb61f2d0"}, - {url: "https://my.fsf.org/misc/jquery-extend-3.4.0.js?v=1.4.4", - hash:"c54103ba57ee210ca55c052e70415402707548a4e6a68dd6efb3895019bee392"}, - {url: "https://my.fsf.org/misc/jquery-html-prefilter-3.5.0-backport.js?v=1.4.4", - hash:"fad84efa145fb507e5df9b582fa01b1c4e6313de7f72ebdd55726d92fa4dbf06"}, - {url: "https://my.fsf.org/misc/jquery.once.js?v=1.2", - hash:"1430f42c0d760ba8e05bb3762480502e541f654fec5739ee40625ab22dc38c4f"}, - {url: "https://my.fsf.org/misc/drupal.js?qmaukd", - hash:"2e08dccbd4d8b728a6871562995a4636b89bfe0ed3b8fb0138191c922228b116"}, - {url: "https://my.fsf.org/sites/all/modules/civicrm/bower_components/jquery/dist/jquery.min.js?qmaukd", - hash:"a6d01520d28d15dbe476de84eea90eb3ee2d058722efc062ec73cb5fad78a17b"}, - {url: "https://my.fsf.org/sites/all/modules/civicrm/bower_components/jquery-ui/jquery-ui.min.js?qmaukd", - hash:"28ce75d953678c4942df47a11707a15e3c756021cf89090e3e6aa7ad6b6971c3"}, - {url: "https://my.fsf.org/sites/all/modules/civicrm/bower_components/lodash-compat/lodash.min.js?qmaukd", - hash:"f2871cc80c52fe8c04c582c4a49797c9c8fd80391cf1452e47f7fe97835ed5cc"}, - {url: "https://my.fsf.org/sites/all/modules/civicrm/packages/jquery/plugins/jquery.mousewheel.min.js?qmaukd", - hash:"f50233e84c2ac7ada37a094d3f7d3b3f7c97716d6b7b47bf69619d93ee4ac1ce"}, - {url: "https://my.fsf.org/sites/all/modules/civicrm/bower_components/select2/select2.min.js?qmaukd", - hash:"ce61298fb9aa4ec49ccd4172d097e36a9e5db3af06a7b82796659368f15b7c1b"}, - {url: "https://my.fsf.org/sites/all/modules/civi crm/packages/jquery/plugins/jquery.form.min.js?qmaukd", - hash:"c90f0e501d2948fbc2b61bffd654fa4ab64741fd48923782419eeb14d3816fb8"}, - {url: "https://my.fsf.org/sites/all/modules/civicrm/packages/jquery/plugins/jquery.timeentry.min.js?qmaukd", - hash:"8e85df981e8ad7049d06dfb075277d038734d36a7097c7f021021b2bdccfe9bb"}, - {url: "https://my.fsf.org/sites/all/modules/civicrm/packages/jquery/plugins/jquery.blockUI.min.js?qmaukd", - hash:"806aedff52ac822f2adc5797073e1e5c5cec32eb9f15f2319cb32a347dcd232b"}, - {url: "https://my.fsf.org/sites/all/modules/civicrm/bower_components/datatables/media/js/jquery.dataTables.min.js?qmaukd", - hash:"b796504d9b1b422f0dc6ccc2d740ac78a8c9e5078cc3934836d39742b1121925"}, - {url: "https://my.fsf.org/sites/all/modules/civicrm/bower_components/jquery-validation/dist/jquery.validate.min.js?qmaukd", - hash:"f0f5373ad203101ea91bf826c5a7ef8f7cd74887f06bad2cb9277a504503b9e2"}, - {url: "https://my.fsf.org/sites/all/modules/civicrm/packages/jquery/plugins/jquery.ui.datepicker.validation.min.js?qmaukd", - hash:"c6e6f6bf7f8fff25cca338045774e267e8eaa2d48ac9100540f3d59a6d2b3c61"}, - {url: "https://my.fsf.org/sites/all/modules/civicrm/js/Common.js?qmaukd", - hash:"17aa222a3af2e8958be16accb5e77ef39f67009cb3b500718d8fffd45b399148"}, - {url: "https://my.fsf.org/sites/all/modules/civicrm/js/crm.datepicker.js?qmaukd", - hash:"9bd8d10208aa99c156325f7819da6f0dd62ba221ac4119c3ccd4834e2cf36535"}, - {url: "https://my.fsf.org/sites/all/modules/civicrm/js/crm.ajax.js?qmaukd", - hash:"6401a4e257b7499ae4a00be2c200e4504a2c9b3d6b278a830c31a7b63374f0fe"}, - {url: "https://my.fsf.org/sites/all/modules/civicrm/js/wysiwyg/crm.wysiwyg.js?qmaukd", - hash:"fa962356072a36672c3b4b25bdeb657f020995a067e20a29cd5bb84b05157762"}, - {url: "https://my.fsf.org/sites/all/modules/civicrm/js/noconflict.js?qmaukd", - hash:"58d6d9f496a235d23cf891926d71f2104e4f2afe1d14bb4e2b5233f646c35e62"}, - {url: "https://my.fsf.org/sites/all/modules/matomo/matomo.js?qmaukd", - hash:"7f39ccd085f348189cd2fb62ea4d4a658d96f6bba266265880b98605e777e2de"}, - {url: "https://my.fsf.org/sites/all/themes/fsf_venture/js/global.js?qmaukd", - hash:"aa7983f6b902f9f4415cfc8346e0c3f194cc95b78f52f2ad09ec7effa1326b9c"}, - {url: "https://my.fsf.org/sites/all/themes/fsf_venture/js/jquery.superfish.min.js?qmaukd", - hash:"5ef1f93bf3901227056bf9ed0ed93a148eec4dda30f419756b12bedd1098815e"}, - {url: "https://my.fsf.org/sites/all/themes/fsf_venture/js/jquery.sidr.min.js?qmaukd", - hash:"c4914d415826676c6af2e61f16edb72c5388f8600ba6de9049892aee49d980a0"}, - {url: "https://my.fsf.org/sites/all/themes/fsf_venture/js/jquery.flexslider.min.js?qmaukd", - hash:"cefaf715761b4494913851249b9d40dacb4a8cb61242b0efc859dc586d56e0d4"}, - {url: "http://127.0.0.1:8000/myfsf_crap.js", - hash:"d91ccf21592d0f861ea0ba946bc257fc5d88269327cad0a91387da6cb8ff633e"}, - {url: "https://my.fsf.org/sites/all/modules/civicrm/templates/CRM/Core/BillingBlock.js?r=lp7Di", - hash:"2f25d35e7a0c0060ab0a444a577f09dd3c9934ae898a7ee0eb20b6c986ab5a1c"}, - {url: "https://my.fsf.org/extensions/com.aghstrategies.giftmemberships/js/giftpricefield.js?r=lp7Di", - hash:"f86080e6bd306fe46474039aeca2808235005bce5a2a29416d08210022039a45"}, - {url: "https://my.fsf.org/extensions/com.ginkgostreet.negativenegator/js/negativenegator.js?r=lp7Di", - hash:"d0e87bac832856db70947d82a7ab4e0b7c8b1070d5f1a32335345e033ece3a14"} - ]) { - let name_regex = /\/([^/]+)\.js/; - let name = name_regex.exec(script_data.url)[1]; - await storage.set(TYPE_PREFIX.SCRIPT, name, script_data); - components.push([TYPE_PREFIX.SCRIPT, name]); - } - - await storage.set(TYPE_PREFIX.PAGE, "https://my.fsf.org/join", {components}); - - let hello_script = { - text: "console.log(\"hello, every1!\");\n" - }; - await storage.set(TYPE_PREFIX.SCRIPT, "hello", hello_script); - await storage.set(TYPE_PREFIX.BUNDLE, "hello", - [[TYPE_PREFIX.SCRIPT, "hello"]]); - await storage.set(TYPE_PREFIX.PAGE, "https://my.fsf.org/", { - components: [[TYPE_PREFIX.BUNDLE, "hello"]], - allow: true - }); - - let opencores_script = { - text: `\ -let data = JSON.parse(document.getElementById("__NEXT_DATA__").textContent); -let sections = {}; -for (let h1 of document.getElementsByClassName("cMJCrc")) { - let ul = document.createElement("ul"); - if (h1.nextElementSibling !== null) - h1.parentNode.insertBefore(ul, h1.nextElementSibling); - else - h1.parentNode.appendChild(ul); - - sections[h1.children[1].firstChild.textContent] = ul; -} - -for (let prop of data.props.pageProps.list) { - let ul = sections[prop.category]; - if (ul === undefined) { - console.log(\`unknown category "\${prop.category}" for project "\${prop.title}"\`); - continue; - } - - let li = document.createElement("li"); - let a = document.createElement("a"); - a.setAttribute("href", "/projects/" + prop.slug); - a.textContent = prop.title; - - li.appendChild(a); - ul.appendChild(li); -} -` - }; - - await storage.set(TYPE_PREFIX.SCRIPT, "opencores", opencores_script); - await storage.set(TYPE_PREFIX.PAGE, "https://opencores.org/projects", { - components: [[TYPE_PREFIX.SCRIPT, "opencores"]], - allow: false - }); -} - -browser.runtime.onInstalled.addListener(init_myext); - -console.log("hello, myext"); diff --git a/background/message_server.js b/background/message_server.js new file mode 100644 index 0000000..612be0c --- /dev/null +++ b/background/message_server.js @@ -0,0 +1,35 @@ +/** +* Myext message server +* +* Copyright (C) 2021 Wojtek Kosior +* +* Dual-licensed under: +* - 0BSD license +* - GPLv3 or (at your option) any later version +*/ + +"use strict"; + +(() => { + const browser = window.browser; + + var listeners = {}; + + /* magic should be one of the constants from /common/connection_types.js */ + + function listen_for_connection(magic, cb) + { + listeners[magic] = cb; + } + + function raw_listen(port) { + if (listeners[port.name] === undefined) + return; + + listeners[port.name](port); + } + + browser.runtime.onConnect.addListener(raw_listen); + + window.listen_for_connection = listen_for_connection; +})(); diff --git a/background/message_server.mjs b/background/message_server.mjs deleted file mode 100644 index e3f83d0..0000000 --- a/background/message_server.mjs +++ /dev/null @@ -1,31 +0,0 @@ -/** -* Myext message server -* -* Copyright (C) 2021 Wojtek Kosior -* -* Dual-licensed under: -* - 0BSD license -* - GPLv3 or (at your option) any later version -*/ - -"use strict"; - -import browser from '/common/browser.mjs'; - -var listeners = {}; - -/* magic should be one of the constants from /common/connection_types.mjs */ - -export default function listen_for_connection(magic, cb) -{ - listeners[magic] = cb; -} - -function raw_listen(port) { - if (listeners[port.name] === undefined) - return; - - listeners[port.name](port); -} - -browser.runtime.onConnect.addListener(raw_listen); diff --git a/background/page_actions_server.js b/background/page_actions_server.js new file mode 100644 index 0000000..fbb672e --- /dev/null +++ b/background/page_actions_server.js @@ -0,0 +1,149 @@ +/** +* Myext serving of page actions to content scripts +* +* Copyright (C) 2021 Wojtek Kosior +* +* Dual-licensed under: +* - 0BSD license +* - GPLv3 or (at your option) any later version +*/ + +"use strict"; + +(() => { + const get_storage = window.get_storage; + const TYPE_PREFIX = window.TYPE_PREFIX; + const CONNECTION_TYPE = window.CONNECTION_TYPE; + const browser = window.browser; + const listen_for_connection = window.listen_for_connection; + const url_item = window.url_item; + const sha256 = window.sha256; + + var storage; + var handler; + + function send_scripts(url, port) + { + let settings = storage.get(TYPE_PREFIX.PAGE, url_item(url)); + if (settings === undefined) + return; + + let components = settings.components; + let processed_bundles = new Set(); + + send_scripts_rec(components, port, processed_bundles); + } + + // TODO: parallelize script fetching + async function send_scripts_rec(components, port, processed_bundles) + { + for (let [prefix, name] of components) { + if (prefix === TYPE_PREFIX.BUNDLE) { + if (processed_bundles.has(name)) { + console.log(`preventing recursive inclusion of bundle ${name}`); + continue; + } + + var bundle = storage.get(TYPE_PREFIX.BUNDLE, name); + + if (bundle === undefined) { + console.log(`no bundle in storage for key ${name}`); + continue; + } + + processed_bundles.add(name); + await send_scripts_rec(bundle, port, processed_bundles); + processed_bundles.delete(name); + } else { + let script_text = await get_script_text(name); + if (script_text === undefined) + continue; + + port.postMessage({inject : [script_text]}); + } + } + } + + async function get_script_text(script_name) + { + try { + let script_data = storage.get(TYPE_PREFIX.SCRIPT, script_name); + if (script_data === undefined) { + console.log(`missing data for ${script_name}`); + return; + } + let script_text = script_data.text; + if (!script_text) + script_text = await fetch_remote_script(script_data); + return script_text; + } catch (e) { + console.log(e); + } + } + + function ajax_callback() + { + if (this.readyState == 4) + this.resolve_callback(this); + } + + function initiate_ajax_request(resolve, method, url) + { + var xhttp = new XMLHttpRequest(); + xhttp.resolve_callback = resolve; + xhttp.onreadystatechange = ajax_callback; + xhttp.open(method, url, true); + xhttp.send(); + } + + function make_ajax_request(method, url) + { + return new Promise((resolve, reject) => + initiate_ajax_request(resolve, method, url)); + } + + async function fetch_remote_script(script_data) + { + try { + let xhttp = await make_ajax_request("GET", script_data.url); + if (xhttp.status === 200) { + let computed_hash = sha256(xhttp.responseText); + if (computed_hash !== script_data.hash) { + console.log(`Bad hash for ${script_data.url}\n got ${computed_hash} instead of ${script_data.hash}`); + return; + } + return xhttp.responseText; + } else { + console.log("script not fetched: " + script_data.url); + return; + } + } catch (e) { + console.log(e); + } + } + + function handle_message(port, message, handler) + { + port.onMessage.removeListener(handler[0]); + let url = message.url; + console.log({url}); + send_scripts(url, port); + } + + function new_connection(port) + { + console.log("new page actions connection!"); + let handler = []; + handler.push(m => handle_message(port, m, handler)); + port.onMessage.addListener(handler[0]); + } + + async function start() + { + storage = await get_storage(); + + listen_for_connection(CONNECTION_TYPE.PAGE_ACTIONS, new_connection); + } + + window.start_page_actions_server = start; +})(); diff --git a/background/page_actions_server.mjs b/background/page_actions_server.mjs deleted file mode 100644 index 5fb4924..0000000 --- a/background/page_actions_server.mjs +++ /dev/null @@ -1,145 +0,0 @@ -/** -* Myext serving of page actions to content scripts -* -* Copyright (C) 2021 Wojtek Kosior -* -* Dual-licensed under: -* - 0BSD license -* - GPLv3 or (at your option) any later version -*/ - -import get_storage from './storage.mjs'; -import {TYPE_PREFIX} from '/common/stored_types.mjs'; -import CONNECTION_TYPE from '/common/connection_types.mjs'; -import browser from '/common/browser.mjs'; -import listen_for_connection from './message_server.mjs'; -import url_item from './url_item.mjs'; -import sha256 from './sha256.mjs'; - -"use strict"; - -var storage; -var handler; - -function send_scripts(url, port) -{ - let settings = storage.get(TYPE_PREFIX.PAGE, url_item(url)); - if (settings === undefined) - return; - - let components = settings.components; - let processed_bundles = new Set(); - - send_scripts_rec(components, port, processed_bundles); -} - -// TODO: parallelize script fetching -async function send_scripts_rec(components, port, processed_bundles) -{ - for (let [prefix, name] of components) { - if (prefix === TYPE_PREFIX.BUNDLE) { - if (processed_bundles.has(name)) { - console.log(`preventing recursive inclusion of bundle ${name}`); - continue; - } - - var bundle = storage.get(TYPE_PREFIX.BUNDLE, name); - - if (bundle === undefined) { - console.log(`no bundle in storage for key ${name}`); - continue; - } - - processed_bundles.add(name); - await send_scripts_rec(bundle, port, processed_bundles); - processed_bundles.delete(name); - } else { - let script_text = await get_script_text(name); - if (script_text === undefined) - continue; - - port.postMessage({inject : [script_text]}); - } - } -} - -async function get_script_text(script_name) -{ - try { - let script_data = storage.get(TYPE_PREFIX.SCRIPT, script_name); - if (script_data === undefined) { - console.log(`missing data for ${script_name}`); - return; - } - let script_text = script_data.text; - if (!script_text) - script_text = await fetch_remote_script(script_data); - return script_text; - } catch (e) { - console.log(e); - } -} - -function ajax_callback() -{ - if (this.readyState == 4) - this.resolve_callback(this); -} - -function initiate_ajax_request(resolve, method, url) -{ - var xhttp = new XMLHttpRequest(); - xhttp.resolve_callback = resolve; - xhttp.onreadystatechange = ajax_callback; - xhttp.open(method, url, true); - xhttp.send(); -} - -function make_ajax_request(method, url) -{ - return new Promise((resolve, reject) => - initiate_ajax_request(resolve, method, url)); -} - -async function fetch_remote_script(script_data) -{ - try { - let xhttp = await make_ajax_request("GET", script_data.url); - if (xhttp.status === 200) { - let computed_hash = sha256(xhttp.responseText); - if (computed_hash !== script_data.hash) { - console.log(`Bad hash for ${script_data.url}\n got ${computed_hash} instead of ${script_data.hash}`); - return; - } - return xhttp.responseText; - } else { - console.log("script not fetched: " + script_data.url); - return; - } - } catch (e) { - console.log(e); - } -} - -function handle_message(port, message, handler) -{ - port.onMessage.removeListener(handler[0]); - let url = message.url; - console.log({url}); - send_scripts(url, port); -} - -function new_connection(port) -{ - console.log("new page actions connection!"); - let handler = []; - handler.push(m => handle_message(port, m, handler)); - port.onMessage.addListener(handler[0]); -} - -export default async function start() -{ - storage = await get_storage(); - - listen_for_connection(CONNECTION_TYPE.PAGE_ACTIONS, new_connection); -} diff --git a/background/policy_smuggler.js b/background/policy_smuggler.js new file mode 100644 index 0000000..6d0da38 --- /dev/null +++ b/background/policy_smuggler.js @@ -0,0 +1,64 @@ +/** +* Myext smuggling policy to content script through url +* +* Copyright (C) 2021 Wojtek Kosior +* +* Dual-licensed under: +* - 0BSD license +* - GPLv3 or (at your option) any later version +*/ + +"use strict"; + +(() => { + const TYPE_PREFIX = window.TYPE_PREFIX; + const get_storage = window.get_storage; + const browser = window.browser; + const url_item = window.url_item; + + var storage; + + function redirect(request) + { + let url_re = /^([^#]*)((#[^#]*)(#.*)?)?$/; + let match = url_re.exec(request.url); + let base_url = match[1]; + let first_target = match[3]; + let second_target = match[4]; + + if (first_target === "#myext-allow") { + console.log(["not redirecting"]); + return {cancel : false}; + } + + let url = url_item(request.url); + let settings = storage.get(TYPE_PREFIX.PAGE, url); + console.log("got", storage.get(TYPE_PREFIX.PAGE, url), "for", url); + if (settings === undefined || !settings.allow) + return {cancel : false}; + + second_target = (first_target || "") + (second_target || "") + + console.log(["redirecting", request.url, + (base_url + "#myext-allow" + second_target)]); + + return { + redirectUrl : (base_url + "#myext-allow" + second_target) + }; + } + + async function start() { + storage = await get_storage(); + + chrome.webRequest.onBeforeRequest.addListener( + redirect, + { + urls: ["<all_urls>"], + types: ["main_frame", "sub_frame"] + }, + ["blocking"] + ); + } + + window.start_policy_smuggler = start; +})(); diff --git a/background/policy_smuggler.mjs b/background/policy_smuggler.mjs deleted file mode 100644 index 61f34c5..0000000 --- a/background/policy_smuggler.mjs +++ /dev/null @@ -1,60 +0,0 @@ -/** -* Myext smuggling policy to content script through url -* -* Copyright (C) 2021 Wojtek Kosior -* -* Dual-licensed under: -* - 0BSD license -* - GPLv3 or (at your option) any later version -*/ - -import {TYPE_PREFIX} from '/common/stored_types.mjs'; -import get_storage from './storage.mjs'; -import browser from '/common/browser.mjs'; -import url_item from './url_item.mjs'; - -"use strict"; - -var storage; - -function redirect(request) -{ - let url_re = /^([^#]*)((#[^#]*)(#.*)?)?$/; - let match = url_re.exec(request.url); - let base_url = match[1]; - let first_target = match[3]; - let second_target = match[4]; - - if (first_target === "#myext-allow") { - console.log(["not redirecting"]); - return {cancel : false}; - } - - let url = url_item(request.url); - let settings = storage.get(TYPE_PREFIX.PAGE, url); - console.log("got", storage.get(TYPE_PREFIX.PAGE, url), "for", url); - if (settings === undefined || !settings.allow) - return {cancel : false}; - - second_target = (first_target || "") + (second_target || "") - - console.log(["redirecting", request.url, - (base_url + "#myext-allow" + second_target)]); - - return { - redirectUrl : (base_url + "#myext-allow" + second_target) - }; -} - -export default async function start() { - storage = await get_storage(); - - chrome.webRequest.onBeforeRequest.addListener( - redirect, - { - urls: ["<all_urls>"], - types: ["main_frame", "sub_frame"] - }, - ["blocking"] - ); -} diff --git a/background/reverse_use_info.js b/background/reverse_use_info.js new file mode 100644 index 0000000..3399285 --- /dev/null +++ b/background/reverse_use_info.js @@ -0,0 +1,96 @@ +/** +* Myext scripts and bundles usage index +* +* Copyright (C) 2021 Wojtek Kosior +* +* Dual-licensed under: +* - 0BSD license +* - GPLv3 or (at your option) any later version +*/ + +"use strict"; + +/* + * We want to count referenes to scripts and bundles in order to know, + * for example, whether one can be safely deleted. + */ + +(() => { + const TYPE_PREFIX = window.TYPE_PREFIX; + const get_storage = window.get_storage; + const make_once = window.make_once; + + var component_uses = new Map(); + var storage; + + function add_use_info_by_item(prefix, item, components) + { + for (let component of components) { + component = component.join(""); + + let used_by_info = component_uses.get(component); + + if (used_by_info === undefined) { + used_by_info = {}; + component_uses.set(component, used_by_info); + } + + if (used_by_info[prefix] === undefined) + used_by_info[prefix] = new Set(); + + used_by_info[prefix].add(item); + } + } + + function remove_use_info_by_item(prefix, item, components) + { + for (let component of components) { + used_by_info = component_uses.get(component.join("")); + if (used_by_info === undefined || + used_by_info[prefix] === undefined) + return; + + used_by_info[prefix].delete(item); + } + } + + function build_reverse_uses_info(entries_it, type_prefix) + { + for (let [item, components] of storage.get_all_it(type_prefix)) + add_use_info_by_item(type_prefix, item, components); + } + + function handle_change(change) + { + if (change.old_val !== undefined) + remove_use_info_by_item(change.prefix, change.item, change.old_val); + + if (change.new_val !== undefined) + add_use_info_by_item(change.prefix, change.item, change.new_val); + } + + function get_uses(arg1, arg2=undefined) + { + let [prefix, item] = [arg1, arg2]; + + if (arg2 === undefined) + [prefix, item] = arg1; + + return component_uses.get(prefix + item); + } + + async function init() + { + storage = await get_storage(); + + prefixes = [TYPE_PREFIX.PAGE, TYPE_PREFIX.BUNDLE]; + for (let prefix of prefixes) + build_reverse_uses_info(prefix); + + storage.add_change_listener(handle_change, prefixes); + + return get_uses; + } + + window.get_reverse_use_info = make_once(init); +})(); diff --git a/background/reverse_use_info.mjs b/background/reverse_use_info.mjs deleted file mode 100644 index c38c52f..0000000 --- a/background/reverse_use_info.mjs +++ /dev/null @@ -1,93 +0,0 @@ -/** -* Myext scripts and bundles usage index -* -* Copyright (C) 2021 Wojtek Kosior -* -* Dual-licensed under: -* - 0BSD license -* - GPLv3 or (at your option) any later version -*/ - -import TYPE_PREFIX from '/common/stored_types.mjs'; -import get_storage from './storage.mjs'; -import make_once from '/common/once.mjs'; - -"use strict"; - -/* - * We want to count referenes to scripts and bundles in order to know, - * for example, whether one can be safely deleted. - */ - -var component_uses = new Map(); -var storage; - -function add_use_info_by_item(prefix, item, components) -{ - for (let component of components) { - component = component.join(""); - - let used_by_info = component_uses.get(component); - - if (used_by_info === undefined) { - used_by_info = {}; - component_uses.set(component, used_by_info); - } - - if (used_by_info[prefix] === undefined) - used_by_info[prefix] = new Set(); - - used_by_info[prefix].add(item); - } -} - -function remove_use_info_by_item(prefix, item, components) -{ - for (let component of components) { - used_by_info = component_uses.get(component.join("")); - if (used_by_info === undefined || used_by_info[prefix] === undefined) - return; - - used_by_info[prefix].delete(item); - } -} - -function build_reverse_uses_info(entries_it, type_prefix) -{ - for (let [item, components] of storage.get_all_it(type_prefix)) - add_use_info_by_item(type_prefix, item, components); -} - -function handle_change(change) -{ - if (change.old_val !== undefined) - remove_use_info_by_item(change.prefix, change.item, change.old_val); - - if (change.new_val !== undefined) - add_use_info_by_item(change.prefix, change.item, change.new_val); -} - -function get_uses(arg1, arg2=undefined) -{ - let [prefix, item] = [arg1, arg2]; - - if (arg2 === undefined) - [prefix, item] = arg1; - - return component_uses.get(prefix + item); -} - -async function init() -{ - storage = await get_storage(); - - prefixes = [TYPE_PREFIX.PAGE, TYPE_PREFIX.BUNDLE]; - for (let prefix of prefixes) - build_reverse_uses_info(prefix); - - storage.add_change_listener(handle_change, prefixes); - - return get_uses; -} - -export default make_once(init); diff --git a/background/sha256.mjs b/background/sha256.js index b8d9bda..271ec87 100644 --- a/background/sha256.mjs +++ b/background/sha256.js @@ -7,13 +7,12 @@ * @license MIT */ -var fake_window = {}; +"use strict"; -/*jslint bitwise: true */ -(function () { - 'use strict'; +(() => { + var fake_window = {}; - console.log('hello, crypto!'); + console.log('hello, crypto!'); var ERROR = 'input is invalid type'; var WINDOW = typeof window === 'object'; var root = /*WINDOW ? window : {}*/ fake_window; @@ -519,6 +518,6 @@ var fake_window = {}; }); } } -})(); -export default fake_window.sha256; + window.sha256 = fake_window.sha256; +})(); diff --git a/background/storage.js b/background/storage.js new file mode 100644 index 0000000..ea390ef --- /dev/null +++ b/background/storage.js @@ -0,0 +1,397 @@ +/** +* Myext storage manager +* +* Copyright (C) 2021 Wojtek Kosior +* +* Dual-licensed under: +* - 0BSD license +* - GPLv3 or (at your option) any later version +*/ + +"use strict"; + +(() => { + const TYPE_PREFIX = window.TYPE_PREFIX; + const TYPE_NAME = window.TYPE_NAME; + const list_prefixes = window.list_prefixes; + const make_lock = window.make_lock; + const lock = window.lock; + const unlock = window.unlock; + const make_once = window.make_once; + const browser = window.browser; + const is_chrome = window.is_chrome; + + var exports = {}; + + /* We're yet to decide how to handle errors... */ + + /* Here are some basic wrappers for storage API functions */ + + async function get(key) + { + try { + /* Fix for fact that Chrome does not use promises here */ + let promise = is_chrome ? + new Promise((resolve, reject) => + chrome.storage.local.get(key, + val => resolve(val))) : + browser.storage.local.get(key); + + return (await promise)[key]; + } catch (e) { + console.log(e); + } + } + + async function set(key, value) + { + try { + return browser.storage.local.set({[key]: value}); + } catch (e) { + console.log(e); + } + } + + async function setn(keys_and_values) + { + let obj = Object(); + while (keys_and_values.length > 1) { + let value = keys_and_values.pop(); + let key = keys_and_values.pop(); + obj[key] = value; + } + + try { + return browser.storage.local.set(obj); + } catch (e) { + console.log(e); + } + } + + async function set_var(name, value) + { + return set(TYPE_PREFIX.VAR + name, value); + } + + async function get_var(name) + { + return get(TYPE_PREFIX.VAR + name); + } + + /* + * A special case of persisted variable is one that contains list + * of items. + */ + + async function get_list_var(name) + { + let list = await get_var(name); + + return list === undefined ? [] : list; + } + + /* We maintain in-memory copies of some stored lists. */ + + async function list(prefix) + { + let name = TYPE_NAME[prefix] + "s"; /* Make plural. */ + let map = new Map(); + + for (let item of await get_list_var(name)) + map.set(item, await get(prefix + item)); + + return {map, prefix, name, listeners : new Set(), lock : make_lock()}; + } + + var pages; + var bundles; + var scripts; + + var list_by_prefix = {}; + + async function init() + { + for (let prefix of list_prefixes) + list_by_prefix[prefix] = await list(prefix); + + return exports; + } + + /* + * Facilitate listening to changes + */ + + exports.add_change_listener = function (cb, prefixes=list_prefixes) + { + if (typeof(prefixes) === "string") + prefixes = [prefixes]; + + for (let prefix of prefixes) + list_by_prefix[prefix].listeners.add(cb); + } + + exports.remove_change_listener = function (cb, prefixes=list_prefixes) + { + if (typeof(prefixes) === "string") + prefixes = [prefixes]; + + for (let prefix of prefixes) + list_by_prefix[prefix].listeners.delete(cb); + } + + function broadcast_change(change, list) + { + for (let listener_callback of list.listeners) + listener_callback(change); + } + + /* Prepare some hepler functions to get elements of a list */ + + function list_items_it(list, with_values=false) + { + return with_values ? list.map.entries() : list.map.keys(); + } + + function list_entries_it(list) + { + return list_items_it(list, true); + } + + function list_items(list, with_values=false) + { + let array = []; + + for (let item of list_items_it(list, with_values)) + array.push(item); + + return array; + } + + function list_entries(list) + { + return list_items(list, true); + } + + /* + * Below we make additional effort to update map of given kind of items + * every time an item is added/removed to keep everything coherent. + */ + async function set_item(item, value, list) + { + await lock(list.lock); + let result = await _set_item(...arguments); + unlock(list.lock) + return result; + } + async function _set_item(item, value, list) + { + let key = list.prefix + item; + let old_val = list.map.get(item); + if (old_val === undefined) { + let items = list_items(list); + items.push(item); + await setn([key, value, "_" + list.name, items]); + } else { + await set(key, value); + } + + list.map.set(item, value) + + let change = { + prefix : list.prefix, + item, + old_val, + new_val : value + }; + + broadcast_change(change, list); + + return old_val; + } + + // TODO: The actual idea to set value to undefined is good - this way we can + // also set a new list of items in the same API call. But such key + // is still stored in the storage. We need to somehow remove it later. + // For that, we're going to have to store 1 more list of each kind. + async function remove_item(item, list) + { + await lock(list.lock); + let result = await _remove_item(...arguments); + unlock(list.lock) + return result; + } + async function _remove_item(item, list) + { + let old_val = list.map.get(item); + if (old_val === undefined) + return; + + let key = list.prefix + item; + let items = list_items(list); + let index = items.indexOf(item); + items.splice(index, 1); + + await setn([key, undefined, "_" + list.name, items]); + + list.map.delete(item); + + let change = { + prefix : list.prefix, + item, + old_val, + new_val : undefined + }; + + broadcast_change(change, list); + + return old_val; + } + + // TODO: same as above applies here + async function replace_item(old_item, new_item, list, new_val=undefined) + { + await lock(list.lock); + let result = await _replace_item(...arguments); + unlock(list.lock) + return result; + } + async function _replace_item(old_item, new_item, list, new_val=undefined) + { + let old_val = list.map.get(old_item); + if (new_val === undefined) { + if (old_val === undefined) + return; + new_val = old_val + } else if (new_val === old_val && new_item === old_item) { + return old_val; + } + + if (old_item === new_item || old_val === undefined) { + await _set_item(new_item, new_val, list); + return old_val; + } + + let new_key = list.prefix + new_item; + let old_key = list.prefix + old_item; + let items = list_items(list); + let index = items.indexOf(old_item); + items[index] = new_item; + await setn([old_key, undefined, new_key, new_val, + "_" + list.name, items]); + + list.map.delete(old_item); + + let change = { + prefix : list.prefix, + item : old_item, + old_val, + new_val : undefined + }; + + broadcast_change(change, list); + + list.map.set(new_item, new_val); + + change.item = new_item; + change.old_val = undefined; + change.new_val = new_val; + + broadcast_change(change, list); + + return old_val; + } + + /* + * For scripts, item name is chosen by user, data should be + * an object containing: + * - script's url and hash or + * - script's text or + * - all three + */ + + /* + * For bundles, item name is chosen by user, data is an array of 2-element + * arrays with type prefix and script/bundle names. + */ + + /* + * For pages data argument is an object with properties `allow' + * and `components'. Item name is url. + */ + + exports.set = async function (prefix, item, data) + { + return set_item(item, data, list_by_prefix[prefix]); + } + + exports.get = function (prefix, item) + { + return list_by_prefix[prefix].map.get(item); + } + + exports.remove = async function (prefix, item) + { + return remove_item(item, list_by_prefix[prefix]); + } + + exports.replace = async function (prefix, old_item, new_item, + new_data=undefined) + { + return replace_item(old_item, new_item, list_by_prefix[prefix], + new_data); + } + + exports.get_all_names = function (prefix) + { + return list_items(list_by_prefix[prefix]); + } + + exports.get_all_names_it = function (prefix) + { + return list_items_it(list_by_prefix[prefix]); + } + + exports.get_all = function (prefix) + { + return list_entries(list_by_prefix[prefix]); + } + + exports.get_all_it = function (prefix) + { + return list_entries_it(list_by_prefix[prefix]); + } + + /* Finally, a quick way to wipe all the data. */ + // TODO: maybe delete items in such order that none of them ever references + // an already-deleted one? + exports.clear = async function () + { + let lists = list_prefixes.map((p) => list_by_prefix[p]); + + for (let list of lists) + await lock(list.lock); + + for (let list of lists) { + + let change = { + prefix : list.prefix, + new_val : undefined + }; + + for (let [item, val] of list_entries_it(list)) { + change.item = item; + change.old_val = val; + broadcast_change(change, list); + } + + list.map = new Map(); + } + + await browser.storage.local.clear(); + + for (let list of lists) + unlock(list.lock); + } + + window.get_storage = make_once(init); +})(); diff --git a/background/storage.mjs b/background/storage.mjs deleted file mode 100644 index 00b1ace..0000000 --- a/background/storage.mjs +++ /dev/null @@ -1,380 +0,0 @@ -/** -* Myext storage manager -* -* Copyright (C) 2021 Wojtek Kosior -* -* Dual-licensed under: -* - 0BSD license -* - GPLv3 or (at your option) any later version -*/ - -import {TYPE_PREFIX, TYPE_NAME, list_prefixes} from '/common/stored_types.mjs'; -import {make_lock, lock, unlock} from '/common/lock.mjs'; -import make_once from '/common/once.mjs'; -import browser from '/common/browser.mjs'; - -"use strict"; - -var exports = {}; - -/* We're yet to decide how to handle errors... */ - -/* Here are some basic wrappers for storage API functions */ - -async function get(key) -{ - try { - /* Fix for fact that Chrome does not use promises here */ - let promise = window.browser === undefined ? - new Promise((resolve, reject) => - chrome.storage.local.get(key, val => resolve(val))) : - browser.storage.local.get(key); - - return (await promise)[key]; - } catch (e) { - console.log(e); - } -} - -async function set(key, value) -{ - try { - return browser.storage.local.set({[key]: value}); - } catch (e) { - console.log(e); - } -} - -async function setn(keys_and_values) -{ - let obj = Object(); - while (keys_and_values.length > 1) { - let value = keys_and_values.pop(); - let key = keys_and_values.pop(); - obj[key] = value; - } - - try { - return browser.storage.local.set(obj); - } catch (e) { - console.log(e); - } -} - -async function set_var(name, value) -{ - return set(TYPE_PREFIX.VAR + name, value); -} - -async function get_var(name) -{ - return get(TYPE_PREFIX.VAR + name); -} - -/* A special case of a persisted variable is one that contains list of items. */ - -async function get_list_var(name) -{ - let list = await get_var(name); - - return list === undefined ? [] : list; -} - -/* We maintain in-memory copies of some stored lists. */ - -async function list(prefix) -{ - let name = TYPE_NAME[prefix] + "s"; /* Make plural. */ - let map = new Map(); - - for (let item of await get_list_var(name)) - map.set(item, await get(prefix + item)); - - return {map, prefix, name, listeners : new Set(), lock : make_lock()}; -} - -var pages; -var bundles; -var scripts; - -var list_by_prefix = {}; - -async function init() -{ - for (let prefix of list_prefixes) - list_by_prefix[prefix] = await list(prefix); - - return exports; -} - -/* - * Facilitate listening to changes - */ - -exports.add_change_listener = function (cb, prefixes=list_prefixes) -{ - if (typeof(prefixes) === "string") - prefixes = [prefixes]; - - for (let prefix of prefixes) - list_by_prefix[prefix].listeners.add(cb); -} - -exports.remove_change_listener = function (cb, prefixes=list_prefixes) -{ - if (typeof(prefixes) === "string") - prefixes = [prefixes]; - - for (let prefix of prefixes) - list_by_prefix[prefix].listeners.delete(cb); -} - -function broadcast_change(change, list) -{ - for (let listener_callback of list.listeners) - listener_callback(change); -} - -/* Prepare some hepler functions to get elements of a list */ - -function list_items_it(list, with_values=false) -{ - return with_values ? list.map.entries() : list.map.keys(); -} - -function list_entries_it(list) -{ - return list_items_it(list, true); -} - -function list_items(list, with_values=false) -{ - let array = []; - - for (let item of list_items_it(list, with_values)) - array.push(item); - - return array; -} - -function list_entries(list) -{ - return list_items(list, true); -} - -/* - * Below we make additional effort to update map of given kind of items - * every time an item is added/removed to keep everything coherent. - */ -async function set_item(item, value, list) -{ - await lock(list.lock); - let result = await _set_item(...arguments); - unlock(list.lock) - return result; -} -async function _set_item(item, value, list) -{ - let key = list.prefix + item; - let old_val = list.map.get(item); - if (old_val === undefined) { - let items = list_items(list); - items.push(item); - await setn([key, value, "_" + list.name, items]); - } else { - await set(key, value); - } - - list.map.set(item, value) - - let change = { - prefix : list.prefix, - item, - old_val, - new_val : value - }; - - broadcast_change(change, list); - - return old_val; -} - -// TODO: The actual idea to set value to undefined is good - this way we can -// also set a new list of items in the same API call. But such key -// is still stored in the storage. We need to somehow remove it later. -// For that, we're going to have to store 1 more list of each kind. -async function remove_item(item, list) -{ - await lock(list.lock); - let result = await _remove_item(...arguments); - unlock(list.lock) - return result; -} -async function _remove_item(item, list) -{ - let old_val = list.map.get(item); - if (old_val === undefined) - return; - - let key = list.prefix + item; - let items = list_items(list); - let index = items.indexOf(item); - items.splice(index, 1); - - await setn([key, undefined, "_" + list.name, items]); - - list.map.delete(item); - - let change = { - prefix : list.prefix, - item, - old_val, - new_val : undefined - }; - - broadcast_change(change, list); - - return old_val; -} - -// TODO: same as above applies here -async function replace_item(old_item, new_item, list, new_val=undefined) -{ - await lock(list.lock); - let result = await _replace_item(...arguments); - unlock(list.lock) - return result; -} -async function _replace_item(old_item, new_item, list, new_val=undefined) -{ - let old_val = list.map.get(old_item); - if (new_val === undefined) { - if (old_val === undefined) - return; - new_val = old_val - } else if (new_val === old_val && new_item === old_item) { - return old_val; - } - - if (old_item === new_item || old_val === undefined) { - await _set_item(new_item, new_val, list); - return old_val; - } - - let new_key = list.prefix + new_item; - let old_key = list.prefix + old_item; - let items = list_items(list); - let index = items.indexOf(old_item); - items[index] = new_item; - await setn([old_key, undefined, new_key, new_val, "_" + list.name, items]); - - list.map.delete(old_item); - - let change = { - prefix : list.prefix, - item : old_item, - old_val, - new_val : undefined - }; - - broadcast_change(change, list); - - list.map.set(new_item, new_val); - - change.item = new_item; - change.old_val = undefined; - change.new_val = new_val; - - broadcast_change(change, list); - - return old_val; -} - -/* - * For scripts, item name is chosen by user, data should be an object containing - * - script's url and hash or - * - script's text or - * - all three - */ - -/* - * For bundles, item name is chosen by user, data is an array of 2-element - * arrays with type prefix and script/bundle names. - */ - -/* For pages data argument is same as for bundles. Item name is url. */ - -exports.set = async function (prefix, item, data) -{ - return set_item(item, data, list_by_prefix[prefix]); -} - -exports.get = function (prefix, item) -{ - return list_by_prefix[prefix].map.get(item); -} - -exports.remove = async function (prefix, item) -{ - return remove_item(item, list_by_prefix[prefix]); -} - -exports.replace = async function (prefix, old_item, new_item, - new_data=undefined) -{ - return replace_item(old_item, new_item, list_by_prefix[prefix], new_data); -} - -exports.get_all_names = function (prefix) -{ - return list_items(list_by_prefix[prefix]); -} - -exports.get_all_names_it = function (prefix) -{ - return list_items_it(list_by_prefix[prefix]); -} - -exports.get_all = function (prefix) -{ - return list_entries(list_by_prefix[prefix]); -} - -exports.get_all_it = function (prefix) -{ - return list_entries_it(list_by_prefix[prefix]); -} - -/* Finally, a quick way to wipe all the data. */ -// TODO: maybe delete items in such order that none of them ever references -// an already-deleted one? -exports.clear = async function () -{ - let lists = list_prefixes.map((p) => list_by_prefix[p]); - - for (let list of lists) - await lock(list.lock); - - for (let list of lists) { - - let change = { - prefix : list.prefix, - new_val : undefined - }; - - for (let [item, val] of list_entries_it(list)) { - change.item = item; - change.old_val = val; - broadcast_change(change, list); - } - - list.map = new Map(); - } - - await browser.storage.local.clear(); - - for (let list of lists) - unlock(list.lock); -} - -export default make_once(init); diff --git a/background/storage_server.js b/background/storage_server.js new file mode 100644 index 0000000..05f616b --- /dev/null +++ b/background/storage_server.js @@ -0,0 +1,65 @@ +/** +* Myext storage through connection (server side) +* +* Copyright (C) 2021 Wojtek Kosior +* +* Dual-licensed under: +* - 0BSD license +* - GPLv3 or (at your option) any later version +*/ + +"use strict"; + +(() => { + const listen_for_connection = window.listen_for_connection; + const get_storage = window.get_storage; + const TYPE_PREFIX = window.TYPE_PREFIX; + const CONNECTION_TYPE = window.CONNECTION_TYPE; + + var storage; + + async function handle_remote_call(port, message) + { + let [call_id, func, args] = message; + + try { + let result = await Promise.resolve(storage[func](...args)); + port.postMessage({call_id, result}); + } catch (error) { + error = error + ''; + port.postMessage({call_id, error}); + } + } + + function remove_storage_listener(cb) { + storage.remove_change_listener(cb); + } + + function new_connection(port) + { + console.log("new remote storage connection!"); + + port.postMessage({ + [TYPE_PREFIX.SCRIPT] : storage.get_all(TYPE_PREFIX.SCRIPT), + [TYPE_PREFIX.BUNDLE] : storage.get_all(TYPE_PREFIX.BUNDLE), + [TYPE_PREFIX.PAGE] : storage.get_all(TYPE_PREFIX.PAGE) + }); + + let handle_change = change => port.postMessage(change); + + storage.add_change_listener(handle_change); + + port.onMessage.addListener(m => handle_remote_call(port, m)); + port.onDisconnect.addListener(() => + remove_storage_listener(handle_change)); + } + + async function start() + { + storage = await get_storage(); + + listen_for_connection(CONNECTION_TYPE.REMOTE_STORAGE, new_connection); + } + + window.start_storage_server = start; +})(); diff --git a/background/storage_server.mjs b/background/storage_server.mjs deleted file mode 100644 index 3a141ff..0000000 --- a/background/storage_server.mjs +++ /dev/null @@ -1,58 +0,0 @@ -/** -* Myext storage through connection (server side) -* -* Copyright (C) 2021 Wojtek Kosior -* -* Dual-licensed under: -* - 0BSD license -* - GPLv3 or (at your option) any later version -*/ - -import listen_for_connection from './message_server.mjs'; -import get_storage from './storage.mjs'; -import {TYPE_PREFIX} from '/common/stored_types.mjs'; -import CONNECTION_TYPE from '/common/connection_types.mjs'; - -var storage; - -async function handle_remote_call(port, message) -{ - let [call_id, func, args] = message; - - try { - let result = await Promise.resolve(storage[func](...args)); - port.postMessage({call_id, result}); - } catch (error) { - error = error + ''; - port.postMessage({call_id, error}); - } -} - -function remove_storage_listener(cb) { - storage.remove_change_listener(cb); -} - -function new_connection(port) -{ - console.log("new remote storage connection!"); - - port.postMessage({ - [TYPE_PREFIX.SCRIPT] : storage.get_all(TYPE_PREFIX.SCRIPT), - [TYPE_PREFIX.BUNDLE] : storage.get_all(TYPE_PREFIX.BUNDLE), - [TYPE_PREFIX.PAGE] : storage.get_all(TYPE_PREFIX.PAGE) - }); - - let handle_change = change => port.postMessage(change); - - storage.add_change_listener(handle_change); - - port.onMessage.addListener(m => handle_remote_call(port, m)); - port.onDisconnect.addListener(() => remove_storage_listener(handle_change)); -} - -export default async function start() -{ - storage = await get_storage(); - - listen_for_connection(CONNECTION_TYPE.REMOTE_STORAGE, new_connection); -} diff --git a/background/url_item.mjs b/background/url_item.js index 2ee1d6d..7850871 100644 --- a/background/url_item.mjs +++ b/background/url_item.js @@ -1,6 +1,6 @@ /** * Myext stripping url from query and target -* +* * Copyright (C) 2021 Wojtek Kosior * * Dual-licensed under: @@ -10,9 +10,13 @@ "use strict"; -export default function url_item(url) -{ - let url_re = /^([^?#]*).*$/; - let match = url_re.exec(url); - return match[1]; -} +(() => { + function url_item(url) + { + let url_re = /^([^?#]*).*$/; + let match = url_re.exec(url); + return match[1]; + } + + window.url_item = url_item; +})(); diff --git a/common/browser.mjs b/common/browser.js index 0d1b233..1d9edda 100644 --- a/common/browser.mjs +++ b/common/browser.js @@ -15,4 +15,13 @@ * chrome-based and firefox-based browsers. */ -export default (window.browser === undefined) ? chrome : browser; +(() => { + if (window.browser === undefined) { + window.browser = window.chrome; + window.is_chrome = true; + window.is_mozilla = false; + } else { + window.is_chrome = false; + window.is_mozilla = true; + } +})(); diff --git a/common/connection_types.mjs b/common/connection_types.js index 12d6de3..a01a777 100644 --- a/common/connection_types.mjs +++ b/common/connection_types.js @@ -8,14 +8,18 @@ * - GPLv3 or (at your option) any later version */ +"use strict"; + /* * Those need to be strings so they can be used as 'name' parameter * to browser.runtime.connect() */ -const CONNECTION_TYPE = { - REMOTE_STORAGE : "0", - PAGE_ACTIONS : "1" -}; +(() => { + const CONNECTION_TYPE = { + REMOTE_STORAGE : "0", + PAGE_ACTIONS : "1" + }; -export default CONNECTION_TYPE; + window.CONNECTION_TYPE = CONNECTION_TYPE; +})(); diff --git a/common/is_background.mjs b/common/is_background.mjs deleted file mode 100644 index ef728a7..0000000 --- a/common/is_background.mjs +++ /dev/null @@ -1,17 +0,0 @@ -/** -* Myext programmatic check of where the script is being run -* -* Copyright (C) 2021 Wojtek Kosior -* -* Dual-licensed under: -* - 0BSD license -* - GPLv3 or (at your option) any later version -*/ - -/* This needs to be changed if we ever modify the html file path. */ - -export default function is_background() -{ - return window.location.protocol === "moz-extension:" && - window.location.pathname === "/background/background.html"; -} diff --git a/common/lock.mjs b/common/lock.js index 596dd9c..107287f 100644 --- a/common/lock.mjs +++ b/common/lock.js @@ -24,32 +24,38 @@ "use strict"; -export function make_lock() { - return {free: true, queue: []}; -} - -function _lock(lock, cb) { - if (lock.free) { - lock.free = false; - setTimeout(cb); - } else { - lock.queue.push(cb); +(() => { + function make_lock() { + return {free: true, queue: []}; } -} - -export function lock(lock) { - return new Promise((resolve, reject) => _lock(lock, resolve)); -} - -export function unlock(lock) { - if (lock.free) - throw new Exception("Attempting to release a free lock"); - - if (lock.queue.length === 0) { - lock.free = true; - } else { - let cb = lock.queue[0]; - lock.queue.splice(0, 1); - setTimeout(cb); + + function _lock(lock, cb) { + if (lock.free) { + lock.free = false; + setTimeout(cb); + } else { + lock.queue.push(cb); + } + } + + function lock(lock) { + return new Promise((resolve, reject) => _lock(lock, resolve)); } -} + + function unlock(lock) { + if (lock.free) + throw new Exception("Attempting to release a free lock"); + + if (lock.queue.length === 0) { + lock.free = true; + } else { + let cb = lock.queue[0]; + lock.queue.splice(0, 1); + setTimeout(cb); + } + } + + window.make_lock = make_lock; + window.lock = lock; + window.unlock = unlock; +})(); diff --git a/common/once.js b/common/once.js new file mode 100644 index 0000000..1842a47 --- /dev/null +++ b/common/once.js @@ -0,0 +1,44 @@ +/** +* Myext feature initialization promise +* +* Copyright (C) 2021 Wojtek Kosior +* +* Dual-licensed under: +* - 0BSD license +* - GPLv3 or (at your option) any later version +*/ + +"use strict"; + +/* + * This module provides an easy way to wrap an async function into a promise + * so that it only gets executed once. + */ + +(() => { + async function assign_result(state, result_producer) + { + state.result = await result_producer(); + state.ready = true; + for (let cb of state.waiting) + setTimeout(cb, 0, state.result); + state.waiting = undefined; + } + + async function get_result(state) + { + if (state.ready) + return state.result; + + return new Promise((resolve, reject) => state.waiting.push(resolve)); + } + + function make_once(result_producer) + { + let state = {waiting : [], ready : false, result : undefined}; + assign_result(state, result_producer); + return () => get_result(state); + } + + window.make_once = make_once; +})(); diff --git a/common/once.mjs b/common/once.mjs deleted file mode 100644 index 0f76366..0000000 --- a/common/once.mjs +++ /dev/null @@ -1,42 +0,0 @@ -/** -* Myext feature initialization promise -* -* Copyright (C) 2021 Wojtek Kosior -* -* Dual-licensed under: -* - 0BSD license -* - GPLv3 or (at your option) any later version -*/ - -"use strict"; - -/* - * This module provides an easy way to wrap an async function into a promise - * so that it only gets executed once. - */ - -async function assign_result(state, result_producer) -{ - state.result = await result_producer(); - state.ready = true; - for (let cb of state.waiting) - setTimeout(cb, 0, state.result); - state.waiting = undefined; -} - -async function get_result(state) -{ - if (state.ready) - return state.result; - - return new Promise((resolve, reject) => state.waiting.push(resolve)); -} - -export function make_once(result_producer) -{ - let state = {waiting : [], ready : false, result : undefined}; - assign_result(state, result_producer); - return () => get_result(state); -} - -export default make_once; diff --git a/common/storage_client.js b/common/storage_client.js new file mode 100644 index 0000000..39ece44 --- /dev/null +++ b/common/storage_client.js @@ -0,0 +1,189 @@ +/** +* Myext storage through connection (client side) +* +* Copyright (C) 2021 Wojtek Kosior +* +* Dual-licensed under: +* - 0BSD license +* - GPLv3 or (at your option) any later version +*/ + +"use strict"; + +(() => { + const CONNECTION_TYPE = window.CONNECTION_TYPE; + const TYPE_PREFIX = window.TYPE_PREFIX; + const list_prefixes = window.list_prefixes; + const make_once = window.make_once; + const browser = window.browser; + + var call_id = 0; + var port; + var calls_waiting = new Map(); + + function set_call_callback(resolve, reject, func, args) + { + port.postMessage([call_id, func, args]); + calls_waiting.set(call_id++, [resolve, reject]); + } + + async function remote_call(func, args) + { + return new Promise((resolve, reject) => + set_call_callback(resolve, reject, func, args)); + } + + function handle_message(message) + { + let callbacks = calls_waiting.get(message.call_id); + if (callbacks === undefined) { + handle_change(message); + return; + } + + let [resolve, reject] = callbacks; + calls_waiting.delete(message.call_id); + if (message.error !== undefined) + setTimeout(reject, 0, message.error); + else + setTimeout(resolve, 0, message.result); + } + + function list(name, prefix) + { + return {prefix, name, listeners : new Set()}; + } + + var scripts = list("scripts", TYPE_PREFIX.SCRIPT); + var bundles = list("bundles", TYPE_PREFIX.BUNDLE); + var pages = list("pages", TYPE_PREFIX.PAGE); + + const list_by_prefix = { + [TYPE_PREFIX.SCRIPT] : scripts, + [TYPE_PREFIX.BUNDLE] : bundles, + [TYPE_PREFIX.PAGE] : pages + }; + + var resolve_init; + + function handle_first_message(message) + { + for (let prefix of Object.keys(message)) + list_by_prefix[prefix].map = new Map(message[prefix]); + + port.onMessage.removeListener(handle_first_message); + port.onMessage.addListener(handle_message); + + resolve_init(); + } + + function handle_change(change) + { + let list = list_by_prefix[change.prefix]; + + if (change.new_val === undefined) + list.map.delete(change.item); + else + list.map.set(change.item, change.new_val); + + for (let listener_callback of list.listeners) + listener_callback(change); + } + + var exports = {}; + + function start_connection(resolve) + { + resolve_init = resolve; + port = browser.runtime.connect({name : CONNECTION_TYPE.REMOTE_STORAGE}); + port.onMessage.addListener(handle_first_message); + } + + async function init() { + await new Promise((resolve, reject) => start_connection(resolve)); + return exports; + } + + for (let call_name of ["set", "remove", "replace", "clear"]) + exports [call_name] = (...args) => remote_call(call_name, args); + + // TODO: Much of the code below is copy-pasted from /background/storage.mjs. + // This should later be refactored into a separate module + // to avoid duplication. + + /* + * Facilitate listening to changes + */ + + exports.add_change_listener = function (cb, prefixes=list_prefixes) + { + if (typeof(prefixes) === "string") + prefixes = [prefixes]; + + for (let prefix of prefixes) + list_by_prefix[prefix].listeners.add(cb); + } + + exports.remove_change_listener = function (cb, prefixes=list_prefixes) + { + if (typeof(prefixes) === "string") + prefixes = [prefixes]; + + for (let prefix of prefixes) + list_by_prefix[prefix].listeners.delete(cb); + } + + /* Prepare some hepler functions to get elements of a list */ + + function list_items_it(list, with_values=false) + { + return with_values ? list.map.entries() : list.map.keys(); + } + + function list_entries_it(list) + { + return list_items_it(list, true); + } + + function list_items(list, with_values=false) + { + let array = []; + + for (let item of list_items_it(list, with_values)) + array.push(item); + + return array; + } + + function list_entries(list) + { + return list_items(list, true); + } + + exports.get = function (prefix, item) + { + return list_by_prefix[prefix].map.get(item); + } + + exports.get_all_names = function (prefix) + { + return list_items(list_by_prefix[prefix]); + } + + exports.get_all_names_it = function (prefix) + { + return list_items_it(list_by_prefix[prefix]); + } + + exports.get_all = function (prefix) + { + return list_entries(list_by_prefix[prefix]); + } + + exports.get_all_it = function (prefix) + { + return list_entries_it(list_by_prefix[prefix]); + } + + window.get_storage = make_once(init); +})(); diff --git a/common/storage_client.mjs b/common/storage_client.mjs deleted file mode 100644 index 8260ad7..0000000 --- a/common/storage_client.mjs +++ /dev/null @@ -1,186 +0,0 @@ -/** -* Myext storage through connection (client side) -* -* Copyright (C) 2021 Wojtek Kosior -* -* Dual-licensed under: -* - 0BSD license -* - GPLv3 or (at your option) any later version -*/ - -"use strict"; - -import CONNECTION_TYPE from './connection_types.mjs'; -import {TYPE_PREFIX, list_prefixes} from '/common/stored_types.mjs'; -import make_once from './once.mjs'; -import browser from '/common/browser.mjs'; - -var call_id = 0; -var port; -var calls_waiting = new Map(); - -function set_call_callback(resolve, reject, func, args) -{ - port.postMessage([call_id, func, args]); - calls_waiting.set(call_id++, [resolve, reject]); -} - -async function remote_call(func, args) -{ - return new Promise((resolve, reject) => - set_call_callback(resolve, reject, func, args)); -} - -function handle_message(message) -{ - let callbacks = calls_waiting.get(message.call_id); - if (callbacks === undefined) { - handle_change(message); - return; - } - - let [resolve, reject] = callbacks; - calls_waiting.delete(message.call_id); - if (message.error !== undefined) - setTimeout(reject, 0, message.error); - else - setTimeout(resolve, 0, message.result); -} - -function list(name, prefix) -{ - return {prefix, name, listeners : new Set()}; -} - -var scripts = list("scripts", TYPE_PREFIX.SCRIPT); -var bundles = list("bundles", TYPE_PREFIX.BUNDLE); -var pages = list("pages", TYPE_PREFIX.PAGE); - -const list_by_prefix = { - [TYPE_PREFIX.SCRIPT] : scripts, - [TYPE_PREFIX.BUNDLE] : bundles, - [TYPE_PREFIX.PAGE] : pages -}; - -var resolve_init; - -function handle_first_message(message) -{ - for (let prefix of Object.keys(message)) - list_by_prefix[prefix].map = new Map(message[prefix]); - - port.onMessage.removeListener(handle_first_message); - port.onMessage.addListener(handle_message); - - resolve_init(); -} - -function handle_change(change) -{ - let list = list_by_prefix[change.prefix]; - - if (change.new_val === undefined) - list.map.delete(change.item); - else - list.map.set(change.item, change.new_val); - - for (let listener_callback of list.listeners) - listener_callback(change); -} - -var exports = {}; - -function start_connection(resolve) -{ - resolve_init = resolve; - port = browser.runtime.connect({name : CONNECTION_TYPE.REMOTE_STORAGE}); - port.onMessage.addListener(handle_first_message); -} - -async function init() { - await new Promise((resolve, reject) => start_connection(resolve)); - return exports; -} - -for (let call_name of ["set", "remove", "replace", "clear"]) - exports [call_name] = (...args) => remote_call(call_name, args); - -// TODO: Much of the code below is copy-pasted from /background/storage.mjs. -// This should later be refactored into a separate module -// to avoid duplication. - -/* - * Facilitate listening to changes - */ - -exports.add_change_listener = function (cb, prefixes=list_prefixes) -{ - if (typeof(prefixes) === "string") - prefixes = [prefixes]; - - for (let prefix of prefixes) - list_by_prefix[prefix].listeners.add(cb); -} - -exports.remove_change_listener = function (cb, prefixes=list_prefixes) -{ - if (typeof(prefixes) === "string") - prefixes = [prefixes]; - - for (let prefix of prefixes) - list_by_prefix[prefix].listeners.delete(cb); -} - -/* Prepare some hepler functions to get elements of a list */ - -function list_items_it(list, with_values=false) -{ - return with_values ? list.map.entries() : list.map.keys(); -} - -function list_entries_it(list) -{ - return list_items_it(list, true); -} - -function list_items(list, with_values=false) -{ - let array = []; - - for (let item of list_items_it(list, with_values)) - array.push(item); - - return array; -} - -function list_entries(list) -{ - return list_items(list, true); -} - -exports.get = function (prefix, item) -{ - return list_by_prefix[prefix].map.get(item); -} - -exports.get_all_names = function (prefix) -{ - return list_items(list_by_prefix[prefix]); -} - -exports.get_all_names_it = function (prefix) -{ - return list_items_it(list_by_prefix[prefix]); -} - -exports.get_all = function (prefix) -{ - return list_entries(list_by_prefix[prefix]); -} - -exports.get_all_it = function (prefix) -{ - return list_entries_it(list_by_prefix[prefix]); -} - -export default make_once(init); diff --git a/common/stored_types.js b/common/stored_types.js new file mode 100644 index 0000000..de0ec71 --- /dev/null +++ b/common/stored_types.js @@ -0,0 +1,44 @@ +/** + * Myext stored item types "enum" + * + * Copyright (C) 2021 Wojtek Kosior + * + * Dual-licensed under: + * - 0BSD license + * - GPLv3 or (at your option) any later version + */ + +/* + * Key for item that is stored in quantity (script, page) is constructed by + * prepending its name with first letter of its list name. However, we also + * need to store some items that don't belong to any list. Let's call them + * persisted variables. In such case item's key is its "name" prepended with + * an underscore. + */ + +"use strict"; + +(() => { + const TYPE_PREFIX = { + PAGE : "p", + BUNDLE : "b", + SCRIPT : "s", + VAR : "_" + }; + + const TYPE_NAME = { + [TYPE_PREFIX.PAGE] : "page", + [TYPE_PREFIX.BUNDLE] : "bundle", + [TYPE_PREFIX.SCRIPT] : "script" + } + + const list_prefixes = [ + TYPE_PREFIX.PAGE, + TYPE_PREFIX.BUNDLE, + TYPE_PREFIX.SCRIPT + ]; + + window.TYPE_PREFIX = TYPE_PREFIX; + window.TYPE_NAME = TYPE_NAME; + window.list_prefixes = list_prefixes; +})(); diff --git a/common/stored_types.mjs b/common/stored_types.mjs deleted file mode 100644 index 8545d44..0000000 --- a/common/stored_types.mjs +++ /dev/null @@ -1,38 +0,0 @@ -/** -* Myext stored item types "enum" -* -* Copyright (C) 2021 Wojtek Kosior -* -* Dual-licensed under: -* - 0BSD license -* - GPLv3 or (at your option) any later version -*/ - -/* - * Key for item that is stored in quantity (script, page) is constructed by - * prepending its name with first letter of its list name. However, we also - * need to store some items that don't belong to any list. Let's call them - * persisted variables. In such case item's key is its "name" prepended with - * an underscore. - */ - -const TYPE_PREFIX = { - PAGE : "p", - BUNDLE : "b", - SCRIPT : "s", - VAR : "_" -}; - -const TYPE_NAME = { - [TYPE_PREFIX.PAGE] : "page", - [TYPE_PREFIX.BUNDLE] : "bundle", - [TYPE_PREFIX.SCRIPT] : "script" -} - -const list_prefixes = [ - TYPE_PREFIX.PAGE, - TYPE_PREFIX.BUNDLE, - TYPE_PREFIX.SCRIPT -]; - -export {TYPE_PREFIX, TYPE_NAME, list_prefixes}; diff --git a/content/main.js b/content/main.js index 12a94c9..282c7b5 100644 --- a/content/main.js +++ b/content/main.js @@ -1,95 +1,97 @@ /** -* Myext main content script run in all frames -* -* Copyright (C) 2021 Wojtek Kosior -* -* Dual-licensed under: -* - 0BSD license -* - GPLv3 or (at your option) any later version -*/ + * Myext main content script run in all frames + * + * Copyright (C) 2021 Wojtek Kosior + * + * Dual-licensed under: + * - 0BSD license + * - GPLv3 or (at your option) any later version + */ -var url_re = /^([^#]*)((#[^#]*)(#.*)?)?$/; -var match = url_re.exec(document.URL); -var base_url = match[1]; -var first_target = match[3]; -var second_target = match[4]; +"use strict"; -var block = true; -if (first_target !== undefined && - first_target === "#myext-allow") { - block = false; - console.log(["allowing", document.URL]); - if (second_target !== undefined) - window.location.href = base_url + second_target; - else - history.replaceState(null, "", base_url); -} else { - console.log(["not allowing", document.URL]); -} +(() => { + const handle_page_actions = window.handle_page_actions; -function handle_mutation(mutations, observer) -{ - if (document.readyState === 'complete') { - console.log("complete"); - observer.disconnect(); - return; + var url_re = /^([^#]*)((#[^#]*)(#.*)?)?$/; + var match = url_re.exec(document.URL); + var base_url = match[1]; + var first_target = match[3]; + var second_target = match[4]; + + var block = true; + if (first_target !== undefined && + first_target === "#myext-allow") { + block = false; + console.log(["allowing", document.URL]); + if (second_target !== undefined) + window.location.href = base_url + second_target; + else + history.replaceState(null, "", base_url); + } else { + console.log(["not allowing", document.URL]); } - for (let mutation of mutations) { - for (let node of mutation.addedNodes) { - if (node.tagName === "SCRIPT") - block_script(node); - else - sanitize_attributes(node); + + function handle_mutation(mutations, observer) + { + if (document.readyState === 'complete') { + console.log("complete"); + observer.disconnect(); + return; + } + for (let mutation of mutations) { + for (let node of mutation.addedNodes) { + if (node.tagName === "SCRIPT") + block_script(node); + else + sanitize_attributes(node); + } } } -} -function block_script(node) -{ - console.log(node); + function block_script(node) + { + console.log(node); - /* - * Disabling scripts this way allows them to still be relatively accessed - * in case they contain some useful data. - */ - if (node.hasAttribute("type")) - node.setAttribute("blocked-type", node.getAttribute("type")); - node.setAttribute("type", "application/json"); -} + /* + * Disabling scripts this way allows them to still be relatively + * easily accessed in case they contain some useful data. + */ + if (node.hasAttribute("type")) + node.setAttribute("blocked-type", node.getAttribute("type")); + node.setAttribute("type", "application/json"); + } -function sanitize_attributes(node) -{ - if (node.attributes === undefined) - return; + function sanitize_attributes(node) + { + if (node.attributes === undefined) + return; - /* We have to do it in 2 loops, removing attribute modifies our iterator */ - let attr_names = []; - for (let attr of node.attributes) { - let attr_name = attr.localName; - if (attr_name.startsWith("on")) - attr_names.push(attr_name); - } + /* + * We have to do it in 2 loops, removing attribute modifies + * our iterator + */ + let attr_names = []; + for (let attr of node.attributes) { + let attr_name = attr.localName; + if (attr_name.startsWith("on")) + attr_names.push(attr_name); + } - for (let attr_name of attr_names) { - node.removeAttribute(attr_name); - console.log("sanitized", attr_name); + for (let attr_name of attr_names) { + node.removeAttribute(attr_name); + console.log("sanitized", attr_name); + } } -} -async function run_module() -{ - let src = chrome.runtime.getURL("content/page_actions.mjs"); - let module = await import(src); - module.default(); -} - -if (block) { - var observer = new MutationObserver(handle_mutation); - observer.observe(document.documentElement, { - attributes: true, - childList: true, - subtree: true - }); -} + if (block) { + var observer = new MutationObserver(handle_mutation); + observer.observe(document.documentElement, { + attributes: true, + childList: true, + subtree: true + }); + } -run_module(); + handle_page_actions(); +})(); diff --git a/content/page_actions.js b/content/page_actions.js new file mode 100644 index 0000000..047bf24 --- /dev/null +++ b/content/page_actions.js @@ -0,0 +1,63 @@ +/** +* Myext handling of page actions in content scripts +* +* Copyright (C) 2021 Wojtek Kosior +* +* Dual-licensed under: +* - 0BSD license +* - GPLv3 or (at your option) any later version +*/ + +"use strict"; + +(() => { + const CONNECTION_TYPE = window.CONNECTION_TYPE; + const browser = window.browser; + + var port; + var loaded = false; + var scripts_awaiting = []; + + function handle_message(message) + { + console.log(["message", message]); + + if (message.inject === undefined) + return; + + for (let script_text of message.inject) { + if (loaded) + add_script(script_text); + else + scripts_awaiting.push(script_text); + } + } + + function document_loaded(event) + { + console.log("loaded"); + + loaded = true; + + for (let script_text of scripts_awaiting) + add_script(script_text); + + scripts_awaiting = undefined; + } + + function add_script(script_text) + { + let script = document.createElement("script"); + script.textContent = script_text; + document.body.appendChild(script); + } + + function handle_page_actions() { + document.addEventListener("DOMContentLoaded", document_loaded); + port = browser.runtime.connect({name : CONNECTION_TYPE.PAGE_ACTIONS}); + port.onMessage.addListener(handle_message); + port.postMessage({url: document.URL}); + } + + window.handle_page_actions = handle_page_actions; +})(); diff --git a/content/page_actions.mjs b/content/page_actions.mjs deleted file mode 100644 index 3ce5b73..0000000 --- a/content/page_actions.mjs +++ /dev/null @@ -1,58 +0,0 @@ -/** -* Myext handling of page actions in content scripts -* -* Copyright (C) 2021 Wojtek Kosior -* -* Dual-licensed under: -* - 0BSD license -* - GPLv3 or (at your option) any later version -*/ - -import CONNECTION_TYPE from '/common/connection_types.mjs'; -import make_once from '/common/once.mjs'; -import browser from '/common/browser.mjs'; - -var port; -var loaded = false; -var scripts_awaiting = []; - -function handle_message(message) -{ - console.log(["message", message]); - - if (message.inject === undefined) - return; - - for (let script_text of message.inject) { - if (loaded) - add_script(script_text); - else - scripts_awaiting.push(script_text); - } -} - -function document_loaded(event) -{ - console.log("loaded"); - - loaded = true; - - for (let script_text of scripts_awaiting) - add_script(script_text); - - scripts_awaiting = undefined; -} - -function add_script(script_text) -{ - let script = document.createElement("script"); - script.textContent = script_text; - document.body.appendChild(script); -} - -export default function main() { - document.addEventListener("DOMContentLoaded", document_loaded); - port = browser.runtime.connect({name : CONNECTION_TYPE.PAGE_ACTIONS}); - port.onMessage.addListener(handle_message); - port.postMessage({url: document.URL}); -} diff --git a/html/display-panel.html b/html/display-panel.html index 9c8de5e..4adeb53 100644 --- a/html/display-panel.html +++ b/html/display-panel.html @@ -6,6 +6,7 @@ </head> <body> <button id="settings_but" type="button">Settings</button> - <script src="./display-panel.mjs" type="module"></script> + <script src="/common/browser.js"></script> + <script src="./display-panel.js"></script> </body> </html> diff --git a/html/display-panel.mjs b/html/display-panel.js index 38f2a42..91b0df7 100644 --- a/html/display-panel.mjs +++ b/html/display-panel.js @@ -10,7 +10,9 @@ "use strict"; -import browser from '/common/browser.mjs'; +(() => { + const browser = window.browser; -document.getElementById("settings_but") - .addEventListener("click", (e) => browser.runtime.openOptionsPage()); + document.getElementById("settings_but") + .addEventListener("click", (e) => browser.runtime.openOptionsPage()); +})(); diff --git a/html/options.html b/html/options.html index 921b423..0bce6fc 100644 --- a/html/options.html +++ b/html/options.html @@ -170,6 +170,11 @@ </div> </div> - <script src="./options_main.mjs" type="module"></script> + <script src="/common/connection_types.js"></script> + <script src="/common/stored_types.js"></script> + <script src="/common/once.js"></script> + <script src="/common/browser.js"></script> + <script src="/common/storage_client.js"></script> + <script src="./options_main.js"></script> </body> </html> diff --git a/html/options_main.js b/html/options_main.js new file mode 100644 index 0000000..979fb44 --- /dev/null +++ b/html/options_main.js @@ -0,0 +1,399 @@ +/** + * Myext HTML options page main script + * + * Copyright (C) 2021 Wojtek Kosior + * + * Dual-licensed under: + * - 0BSD license + * - GPLv3 or (at your option) any later version + */ + +"use strict"; + +(() => { + const get_storage = window.get_storage; + const TYPE_PREFIX = window.TYPE_PREFIX; + const TYPE_NAME = window.TYPE_NAME; + const list_prefixes = window.list_prefixes; + + var storage; + function by_id(id) + { + return document.getElementById(id); + } + + const item_li_template = by_id("item_li_template"); + const component_li_template = by_id("component_li_template"); + const selectable_component_li_template = + by_id("selectable_component_li_template"); + /* Make sure they are later cloned without id. */ + item_li_template.removeAttribute("id"); + component_li_template.removeAttribute("id"); + selectable_component_li_template.removeAttribute("id"); + + function item_li_id(prefix, item) + { + return `li_${prefix}_${item}`; + } + + function add_li(prefix, item, at_the_end=false) + { + let ul = ul_by_prefix[prefix]; + let li = item_li_template.cloneNode(true); + li.id = item_li_id(prefix, item); + + let span = li.firstElementChild; + span.textContent = item; + + let edit_button = span.nextElementSibling; + edit_button.addEventListener("click", () => edit_item(prefix, item)); + + let remove_button = edit_button.nextElementSibling; + remove_button.addEventListener("click", + () => storage.remove(prefix, item)); + + if (!at_the_end) { + for (let element of ul.ul.children) { + if (element.id < li.id || element.id.startsWith("work_")) + continue; + + ul.ul.insertBefore(li, element); + return; + } + } + + ul.ul.appendChild(li); + } + + const selectable_components_ul = by_id("selectable_components_ul"); + + function selectable_li_id(prefix, item) + { + return `sli_${prefix}_${item}`; + } + + function add_selectable(prefix, name) + { + if (prefix === TYPE_PREFIX.PAGE) + return; + + let li = selectable_component_li_template.cloneNode(true); + li.id = selectable_li_id(prefix, name); + li.setAttribute("data-prefix", prefix); + li.setAttribute("data-name", name); + + let chbx = li.firstElementChild; + let span = chbx.nextElementSibling; + + span.textContent = `${name} (${TYPE_NAME[prefix]})`; + + selectable_components_ul.appendChild(li); + } + + /* + * Used to construct and update components list of edited + * bundle as well as edited page. + */ + function add_components(ul, components) + { + let components_ul = ul.work_name_input.nextElementSibling; + + for (let component of components) { + let [prefix, name] = component; + let li = component_li_template.cloneNode(true); + li.setAttribute("data-prefix", prefix); + li.setAttribute("data-name", name); + let span = li.firstElementChild; + span.textContent = `${name} (${TYPE_NAME[prefix]})`; + let remove_but = span.nextElementSibling; + remove_but.addEventListener("click", () => + components_ul.removeChild(li)); + components_ul.appendChild(li); + } + + components_ul.appendChild(ul.work_empty_component_li); + } + + /* Used to reset edited bundle as well as edited page. */ + function generic_reset_work_li(ul, item, components) + { + if (item === undefined) { + item = ""; + components = []; + }; + + ul.work_name_input.value = item; + let old_components_ul = ul.work_name_input.nextElementSibling; + let components_ul = old_components_ul.cloneNode(false); + + ul.work_li.insertBefore(components_ul, old_components_ul); + ul.work_li.removeChild(old_components_ul); + + add_components(ul, components); + } + + function reset_work_page_li(ul, item, settings) + { + ul.work_page_allow_chbx.checked = !!settings?.allow; + generic_reset_work_li(ul, item, settings?.components); + } + + /* Used to get edited bundle as well as edited page data for saving. */ + function generic_work_li_data(ul) + { + let components_ul = ul.work_name_input.nextElementSibling; + let component_li = components_ul.firstElementChild; + + let components = []; + + /* Last list element is empty li with id set. */ + while (component_li.id === '') { + components.push([component_li.getAttribute("data-prefix"), + component_li.getAttribute("data-name")]); + component_li = component_li.nextElementSibling; + } + + return [ul.work_name_input.value, components]; + } + + function work_page_li_data(ul) + { + let [url, components] = generic_work_li_data(ul); + let settings = {components, allow : !!ul.work_page_allow_chbx.checked}; + + return [url, settings]; + } + + const script_url_input = by_id("script_url_field"); + const script_sha256_input = by_id("script_sha256_field"); + const script_contents_field = by_id("script_contents_field"); + + function maybe_string(maybe_defined) + { + return maybe_defined === undefined ? "" : maybe_defined + ""; + } + + function reset_work_script_li(ul, name, data) + { + ul.work_name_input.value = maybe_string(name); + script_url_input.value = maybe_string(data?.url); + script_sha256_input.value = maybe_string(data?.hash); + script_contents_field.value = maybe_string(data?.text); + } + + function work_script_li_data(ul) + { + return [ul.work_name_input.value, { + url : script_url_input.value, + hash : script_sha256_input.value, + text : script_contents_field.value + }]; + } + + function cancel_work(prefix) + { + let ul = ul_by_prefix[prefix]; + + if (ul.state === UL_STATE.IDLE) + return; + + if (ul.state === UL_STATE.EDITING_ENTRY) { + add_li(prefix, ul.edited_item); + } + + ul.work_li.classList.add("hide"); + ul.state = UL_STATE.IDLE; + } + + function save_work(prefix) + { + let ul = ul_by_prefix[prefix]; + + if (ul.state === UL_STATE.IDLE) + return; + + let [item, data] = ul.get_work_li_data(ul); + + if (prefix == TYPE_PREFIX.PAGE) + + /* Here we fire promises and return without waiting. */ + + if (ul.state === UL_STATE.EDITING_ENTRY) + storage.replace(prefix, ul.edited_item, item, data); + + if (ul.state === UL_STATE.ADDING_ENTRY) + storage.set(prefix, item, data); + + cancel_work(prefix); + } + + function edit_item(prefix, item) + { + cancel_work(prefix); + + let ul = ul_by_prefix[prefix]; + let li = by_id(item_li_id(prefix, item)); + ul.reset_work_li(ul, item, storage.get(prefix, item)); + ul.ul.insertBefore(ul.work_li, li); + ul.ul.removeChild(li); + ul.work_li.classList.remove("hide"); + + ul.state = UL_STATE.EDITING_ENTRY; + ul.edited_item = item; + } + + function add_new_item(prefix) + { + cancel_work(prefix); + + let ul = ul_by_prefix[prefix]; + ul.reset_work_li(ul); + ul.work_li.classList.remove("hide"); + ul.ul.appendChild(ul.work_li); + + ul.state = UL_STATE.ADDING_ENTRY; + } + + const select_components_window = by_id("select_components_window"); + var select_prefix; + + function select_components(prefix) + { + select_prefix = prefix; + select_components_window.classList.remove("hide"); + + for (let li of selectable_components_ul.children) { + let chbx = li.firstElementChild; + chbx.checked = false; + } + } + + function commit_components() + { + let selected = []; + + for (let li of selectable_components_ul.children) { + let chbx = li.firstElementChild; + if (!chbx.checked) + continue; + + selected.push([li.getAttribute("data-prefix"), + li.getAttribute("data-name")]); + } + + add_components(ul_by_prefix[select_prefix], selected); + cancel_components(); + } + + function cancel_components() + { + select_components_window.classList.add("hide"); + } + + const UL_STATE = { + EDITING_ENTRY : 0, + ADDING_ENTRY : 1, + IDLE : 2 + }; + + const ul_by_prefix = { + [TYPE_PREFIX.PAGE] : { + ul : by_id("pages_ul"), + work_li : by_id("work_page_li"), + work_name_input : by_id("page_url_field"), + work_empty_component_li : by_id("empty_page_component_li"), + work_page_allow_chbx : by_id("page_allow_chbx"), + reset_work_li : reset_work_page_li, + get_work_li_data : work_page_li_data, + state : UL_STATE.IDLE, + edited_item : undefined, + }, + [TYPE_PREFIX.BUNDLE] : { + ul : by_id("bundles_ul"), + work_li : by_id("work_bundle_li"), + work_name_input : by_id("bundle_name_field"), + work_empty_component_li : by_id("empty_bundle_component_li"), + reset_work_li : generic_reset_work_li, + get_work_li_data : generic_work_li_data, + state : UL_STATE.IDLE, + edited_item : undefined, + }, + [TYPE_PREFIX.SCRIPT] : { + ul : by_id("scripts_ul"), + work_li : by_id("work_script_li"), + work_name_input : by_id("script_name_field"), + reset_work_li : reset_work_script_li, + get_work_li_data : work_script_li_data, + state : UL_STATE.IDLE, + edited_item : undefined, + } + } + + async function main() + { + storage = await get_storage(); + + for (let prefix of list_prefixes) { + for (let item of storage.get_all_names(prefix).sort()) { + add_li(prefix, item, true); + add_selectable(prefix, item); + } + } + + storage.add_change_listener(handle_change); + + let commit_components_but = by_id("commit_components_but"); + let cancel_components_but = by_id("cancel_components_but"); + commit_components_but.addEventListener("click", commit_components); + cancel_components_but.addEventListener("click", cancel_components); + + for (let prefix of list_prefixes) { + let add_but = by_id(`add_${TYPE_NAME[prefix]}_but`); + let discard_but = by_id(`${TYPE_NAME[prefix]}_discard_but`); + let save_but = by_id(`${TYPE_NAME[prefix]}_save_but`); + let select_components_but = + by_id(`${TYPE_NAME[prefix]}_select_components_but`); + + add_but.addEventListener("click", () => add_new_item(prefix)); + discard_but.addEventListener("click", () => cancel_work(prefix)); + save_but.addEventListener("click", () => save_work(prefix)); + if (select_components_but === null) + continue; + + select_components_but.addEventListener( + "click", + () => select_components(prefix) + ); + } + } + + function handle_change(change) + { + if (change.old_val === undefined) { + add_li(change.prefix, change.item); + add_selectable(change.prefix, change.item); + return; + } + + if (change.new_val !== undefined) + return; + + let ul = ul_by_prefix[change.prefix]; + if (ul.state === UL_STATE.EDITING_ENTRY && + ul.edited_item === change.item) { + ul.state = UL_STATE.ADDING_ENTRY; + return; + } + + let li = by_id(item_li_id(change.prefix, change.item)); + ul.ul.removeChild(li); + + if (change.prefix === TYPE_PREFIX.PAGE) + return; + + let sli = by_id(selectable_li_id(change.prefix, change.item)); + selectable_components_ul.removeChild(sli); + } + + main(); +})(); diff --git a/html/options_main.mjs b/html/options_main.mjs deleted file mode 100644 index f288971..0000000 --- a/html/options_main.mjs +++ /dev/null @@ -1,398 +0,0 @@ -/** -* Myext HTML options page main script -* -* Copyright (C) 2021 Wojtek Kosior -* -* Dual-licensed under: -* - 0BSD license -* - GPLv3 or (at your option) any later version -*/ - -"use strict"; - -import get_storage from '/common/storage_client.mjs'; -import {TYPE_PREFIX, TYPE_NAME, list_prefixes} from '/common/stored_types.mjs'; - -var storage; - -const item_li_template = document.getElementById("item_li_template"); -const component_li_template = document.getElementById("component_li_template"); -const selectable_component_li_template = - document.getElementById("selectable_component_li_template"); -/* Make sure they are later cloned without id. */ -item_li_template.removeAttribute("id"); -component_li_template.removeAttribute("id"); -selectable_component_li_template.removeAttribute("id"); - -function item_li_id(prefix, item) -{ - return `li_${prefix}_${item}`; -} - -function add_li(prefix, item, at_the_end=false) -{ - let ul = ul_by_prefix[prefix]; - let li = item_li_template.cloneNode(true); - li.id = item_li_id(prefix, item); - - let span = li.firstElementChild; - span.textContent = item; - - let edit_button = span.nextElementSibling; - edit_button.addEventListener("click", () => edit_item(prefix, item)); - - let remove_button = edit_button.nextElementSibling; - remove_button.addEventListener("click", () => storage.remove(prefix, item)); - - if (!at_the_end) { - for (let element of ul.ul.children) { - if (element.id < li.id || element.id.startsWith("work_")) - continue; - - ul.ul.insertBefore(li, element); - return; - } - } - - ul.ul.appendChild(li); -} - -const selectable_components_ul = - document.getElementById("selectable_components_ul"); - -function selectable_li_id(prefix, item) -{ - return `sli_${prefix}_${item}`; -} - -function add_selectable(prefix, name) -{ - if (prefix === TYPE_PREFIX.PAGE) - return; - - let li = selectable_component_li_template.cloneNode(true); - li.id = selectable_li_id(prefix, name); - li.setAttribute("data-prefix", prefix); - li.setAttribute("data-name", name); - - let chbx = li.firstElementChild; - let span = chbx.nextElementSibling; - - span.textContent = `${name} (${TYPE_NAME[prefix]})`; - - selectable_components_ul.appendChild(li); -} - -/* - * Used to construct and update components list of edited - * bundle as well as edited page. - */ -function add_components(ul, components) -{ - let components_ul = ul.work_name_input.nextElementSibling; - - for (let component of components) { - let [prefix, name] = component; - let li = component_li_template.cloneNode(true); - li.setAttribute("data-prefix", prefix); - li.setAttribute("data-name", name); - let span = li.firstElementChild; - span.textContent = `${name} (${TYPE_NAME[prefix]})`; - let remove_but = span.nextElementSibling; - remove_but.addEventListener("click", () => - components_ul.removeChild(li)); - components_ul.appendChild(li); - } - - components_ul.appendChild(ul.work_empty_component_li); -} - -/* Used to reset edited bundle as well as edited page. */ -function generic_reset_work_li(ul, item, components) -{ - if (item === undefined) { - item = ""; - components = []; - }; - - ul.work_name_input.value = item; - let old_components_ul = ul.work_name_input.nextElementSibling; - let components_ul = old_components_ul.cloneNode(false); - - ul.work_li.insertBefore(components_ul, old_components_ul); - ul.work_li.removeChild(old_components_ul); - - add_components(ul, components); -} - -function reset_work_page_li(ul, item, settings) -{ - ul.work_page_allow_chbx.checked = !!settings?.allow; - generic_reset_work_li(ul, item, settings?.components); -} - -/* Used to get edited bundle as well as edited page data for saving. */ -function generic_work_li_data(ul) -{ - let components_ul = ul.work_name_input.nextElementSibling; - let component_li = components_ul.firstElementChild; - - let components = []; - - /* Last list element is empty li with id set. */ - while (component_li.id === '') { - components.push([component_li.getAttribute("data-prefix"), - component_li.getAttribute("data-name")]); - component_li = component_li.nextElementSibling; - } - - return [ul.work_name_input.value, components]; -} - -function work_page_li_data(ul) -{ - let [url, components] = generic_work_li_data(ul); - let settings = {components, allow : !!ul.work_page_allow_chbx.checked}; - - return [url, settings]; -} - -const script_url_input = document.getElementById("script_url_field"); -const script_sha256_input = document.getElementById("script_sha256_field"); -const script_contents_field = document.getElementById("script_contents_field"); - -function maybe_string(maybe_defined) -{ - return maybe_defined === undefined ? "" : maybe_defined + ""; -} - -function reset_work_script_li(ul, name, data) -{ - ul.work_name_input.value = maybe_string(name); - script_url_input.value = maybe_string(data?.url); - script_sha256_input.value = maybe_string(data?.hash); - script_contents_field.value = maybe_string(data?.text); -} - -function work_script_li_data(ul) -{ - return [ul.work_name_input.value, { - url : script_url_input.value, - hash : script_sha256_input.value, - text : script_contents_field.value - }]; -} - -function cancel_work(prefix) -{ - let ul = ul_by_prefix[prefix]; - - if (ul.state === UL_STATE.IDLE) - return; - - if (ul.state === UL_STATE.EDITING_ENTRY) { - add_li(prefix, ul.edited_item); - } - - ul.work_li.classList.add("hide"); - ul.state = UL_STATE.IDLE; -} - -function save_work(prefix) -{ - let ul = ul_by_prefix[prefix]; - - if (ul.state === UL_STATE.IDLE) - return; - - let [item, data] = ul.get_work_li_data(ul); - - if (prefix == TYPE_PREFIX.PAGE) - - /* Here we fire promises and return without waiting. */ - - if (ul.state === UL_STATE.EDITING_ENTRY) - storage.replace(prefix, ul.edited_item, item, data); - - if (ul.state === UL_STATE.ADDING_ENTRY) - storage.set(prefix, item, data); - - cancel_work(prefix); -} - -function edit_item(prefix, item) -{ - cancel_work(prefix); - - let ul = ul_by_prefix[prefix]; - let li = document.getElementById(item_li_id(prefix, item)); - ul.reset_work_li(ul, item, storage.get(prefix, item)); - ul.ul.insertBefore(ul.work_li, li); - ul.ul.removeChild(li); - ul.work_li.classList.remove("hide"); - - ul.state = UL_STATE.EDITING_ENTRY; - ul.edited_item = item; -} - -function add_new_item(prefix) -{ - cancel_work(prefix); - - let ul = ul_by_prefix[prefix]; - ul.reset_work_li(ul); - ul.work_li.classList.remove("hide"); - ul.ul.appendChild(ul.work_li); - - ul.state = UL_STATE.ADDING_ENTRY; -} - -const select_components_window = - document.getElementById("select_components_window"); -var select_prefix; - -function select_components(prefix) -{ - select_prefix = prefix; - select_components_window.classList.remove("hide"); - - for (let li of selectable_components_ul.children) { - let chbx = li.firstElementChild; - chbx.checked = false; - } -} - -function commit_components() -{ - let selected = []; - - for (let li of selectable_components_ul.children) { - let chbx = li.firstElementChild; - if (!chbx.checked) - continue; - - selected.push([li.getAttribute("data-prefix"), - li.getAttribute("data-name")]); - } - - add_components(ul_by_prefix[select_prefix], selected); - cancel_components(); -} - -function cancel_components() -{ - select_components_window.classList.add("hide"); -} - -const UL_STATE = { - EDITING_ENTRY : 0, - ADDING_ENTRY : 1, - IDLE : 2 -}; - -const ul_by_prefix = { - [TYPE_PREFIX.PAGE] : { - ul : document.getElementById("pages_ul"), - work_li : document.getElementById("work_page_li"), - work_name_input : document.getElementById("page_url_field"), - work_empty_component_li : - document.getElementById("empty_page_component_li"), - work_page_allow_chbx : document.getElementById("page_allow_chbx"), - reset_work_li : reset_work_page_li, - get_work_li_data : work_page_li_data, - state : UL_STATE.IDLE, - edited_item : undefined, - }, - [TYPE_PREFIX.BUNDLE] : { - ul : document.getElementById("bundles_ul"), - work_li : document.getElementById("work_bundle_li"), - work_name_input : document.getElementById("bundle_name_field"), - work_empty_component_li : - document.getElementById("empty_bundle_component_li"), - reset_work_li : generic_reset_work_li, - get_work_li_data : generic_work_li_data, - state : UL_STATE.IDLE, - edited_item : undefined, - }, - [TYPE_PREFIX.SCRIPT] : { - ul : document.getElementById("scripts_ul"), - work_li : document.getElementById("work_script_li"), - work_name_input : document.getElementById("script_name_field"), - reset_work_li : reset_work_script_li, - get_work_li_data : work_script_li_data, - state : UL_STATE.IDLE, - edited_item : undefined, - } -} - -async function main() -{ - storage = await get_storage(); - - for (let prefix of list_prefixes) { - for (let item of storage.get_all_names(prefix).sort()) { - add_li(prefix, item, true); - add_selectable(prefix, item); - } - } - - storage.add_change_listener(handle_change); - - let commit_components_but = - document.getElementById("commit_components_but"); - let cancel_components_but = - document.getElementById("cancel_components_but"); - commit_components_but.addEventListener("click", commit_components); - cancel_components_but.addEventListener("click", cancel_components); - - for (let prefix of list_prefixes) { - let add_but = document.getElementById(`add_${TYPE_NAME[prefix]}_but`); - let discard_but = - document.getElementById(`${TYPE_NAME[prefix]}_discard_but`); - let save_but = document.getElementById(`${TYPE_NAME[prefix]}_save_but`); - let select_components_but = document.getElementById( - `${TYPE_NAME[prefix]}_select_components_but` - ); - - add_but.addEventListener("click", () => add_new_item(prefix)); - discard_but.addEventListener("click", () => cancel_work(prefix)); - save_but.addEventListener("click", () => save_work(prefix)); - if (select_components_but === null) - continue; - - select_components_but.addEventListener( - "click", - () => select_components(prefix) - ); - } -} - -function handle_change(change) -{ - if (change.old_val === undefined) { - add_li(change.prefix, change.item); - add_selectable(change.prefix, change.item); - return; - } - - if (change.new_val !== undefined) - return; - - let ul = ul_by_prefix[change.prefix]; - if (ul.state === UL_STATE.EDITING_ENTRY && ul.edited_item === change.item) { - ul.state = UL_STATE.ADDING_ENTRY; - return; - } - - let li = document.getElementById(item_li_id(change.prefix, change.item)); - ul.ul.removeChild(li); - - if (change.prefix === TYPE_PREFIX.PAGE) - return; - - let sli = document.getElementById(selectable_li_id(change.prefix, - change.item)); - selectable_components_ul.removeChild(sli); -} - -main(); diff --git a/manifest.json b/manifest.json index d9ff113..85dde71 100644 --- a/manifest.json +++ b/manifest.json @@ -38,10 +38,6 @@ "open_in_tab": true }, "web_accessible_resources": [ - "/common/connection_types.mjs", - "/common/once.mjs", - "/common/browser.mjs", - "/content/page_actions.mjs" ], "background": { "page": "background/background.html" @@ -53,6 +49,9 @@ "match_about_blank": true, "all_frames": true, "js": [ + "common/browser.js", + "common/connection_types.js", + "content/page_actions.js", "content/main.js" ] } |