/** * Hachette modify HTML document as it loads and reconstruct HTML code from it * * Copyright (C) 2021 Wojtek Kosior * Redistribution terms are gathered in the `copyright' file. */ /* * IMPORTS_START * IMPORT gen_nonce * IMPORT csp_rule * IMPORT is_csp_header_name * IMPORT sanitize_csp_header * IMPORT sanitize_attributes * IMPORTS_END */ /* * Functions that sanitize elements. The script blocking measures are, when * possible, going to be applied together with CSP rules injected using * webRequest. */ const blocked = "blocked"; function block_attribute(node, attr) { /* * Disabling attributed this way allows them to still be relatively * easily accessed in case they contain some useful data. */ const construct_name = [attr]; while (node.hasAttribute(construct_name.join(""))) construct_name.unshift(blocked); while (construct_name.length > 1) { construct_name.shift(); const name = construct_name.join(""); node.setAttribute(`${blocked}-${name}`, node.getAttribute(name)); } node.removeAttribute(attr); } function sanitize_script(script, policy) { if (policy.allow) return; block_attribute(script, "type"); script.setAttribute("type", "application/json"); } function inject_csp(head, policy) { if (policy.allow) return; const meta = document.createElement("meta"); meta.setAttribute("http-equiv", "Content-Security-Policy"); meta.setAttribute("content", csp_rule(policy.nonce)); meta.hachette_ignore = true; head.prepend(meta); } function sanitize_http_equiv_csp_rule(meta, policy) { const http_equiv = meta.getAttribute("http-equiv"); if (!is_csp_header_name(http_equiv, !policy.allow)) return; if (policy.allow || is_csp_header_name(http_equiv, false)) { let value = meta.getAttribute("content"); block_attribute(meta, "content"); if (value) { value = sanitize_csp_header({value}, policy).value; meta.setAttribute("content", value); } return; } block_attribute(meta, "http-equiv"); } function sanitize_node(node, policy) { if (node.tagName === "SCRIPT") sanitize_script(node, policy); if (node.tagName === "HEAD") inject_csp(node, policy); if (node.tagName === "META") sanitize_http_equiv_csp_rule(node, policy); if (!policy.allow) sanitize_attributes(node, policy); } const serializer = new XMLSerializer(); function start_node(node, data) { if (!data.writer) return; node.hachette_started = true; const clone = node.cloneNode(false); clone.textContent = data.uniq; data.writer(data.uniq_reg.exec(clone.outerHTML)[1]); } function finish_node(node, data) { const nodes_to_process = [node]; while (true) { node = nodes_to_process.pop(); if (!node) break; nodes_to_process.push(node, node.hachette_last_added); } while (nodes_to_process.length > 0) { const node = nodes_to_process.pop(); node.remove(); if (!data.writer) continue; if (node.hachette_started) { node.textContent = data.uniq; data.writer(data.uniq_reg.exec(node.outerHTML)[2]); continue; } data.writer(node.outerHTML || serializer.serializeToString(node)); } } /* * Important! Due to some weirdness node.parentElement is not alway correct * under Chromium. Track node relations manually. */ function handle_added_node(node, true_parent, data) { if (node.hachette_ignore || true_parent.hachette_ignore) return; if (!true_parent.hachette_started) start_node(true_parent, data) sanitize_node(node, data.policy); if (data.node_eater) data.node_eater(node, true_parent); finish_node(true_parent.hachette_last_added, data); true_parent.hachette_last_added = node; } function handle_mutation(mutations, data) { /* * Chromium: for an unknown reason mutation.target is not always the same as * node.parentElement. The former is the correct one. */ for (const mutation of mutations) { for (const node of mutation.addedNodes) handle_added_node(node, mutation.target, data); } } function finish_processing(data) { handle_mutation(data.observer.takeRecords(), data); finish_node(data.html_element, data); data.observer.disconnect(); } function modify_on_the_fly(html_element, policy, consumers) { const uniq = gen_nonce(); const uniq_reg = new RegExp(`^(.*)${uniq}(.*)$`); const data = {policy, html_element, uniq, uniq_reg, ...consumers}; start_node(data.html_element, data); var observer = new MutationObserver(m => handle_mutation(m, data)); observer.observe(data.html_element, { attributes: true, childList: true, subtree: true }); data.observer = observer; return () => finish_processing(data); } /* * EXPORTS_START * EXPORT modify_on_the_fly * EXPORTS_END */