/**
* This file is part of Haketilo.
*
* Function: Main content script that runs in all frames.
*
* Copyright (C) 2021 Wojtek Kosior
* Copyright (C) 2021 jahoti
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* As additional permission under GNU GPL version 3 section 7, you
* may distribute forms of that code without the copy of the GNU
* GPL normally required by section 4, provided you include this
* license notice and, in case of non-source distribution, a URL
* through which recipients can access the Corresponding Source.
* If you modify file(s) with this exception, you may extend this
* exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this
* exception statement from your version.
*
* As a special exception to the GPL, any HTML file which merely
* makes function calls to this code, and for that purpose
* includes it by reference shall be deemed a separate work for
* copyright law purposes. If you modify this code, you may extend
* this exception to your version of the code, but you are not
* obligated to do so. If you do not wish to do so, delete this
* exception statement from your version.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
* I, Wojtek Kosior, thereby promise not to sue for violation of this file's
* license. Although I request that you do not make use this code in a
* proprietary program, I am not going to enforce this in court.
*/
/*
* IMPORTS_START
* IMPORT handle_page_actions
* IMPORT extract_signed
* IMPORT sign_data
* IMPORT gen_nonce
* IMPORT is_privileged_url
* IMPORT is_chrome
* IMPORT is_mozilla
* IMPORT start_activity_info_server
* IMPORT make_csp_rule
* IMPORT csp_header_regex
* IMPORTS_END
*/
document.content_loaded = document.readyState === "complete";
const wait_loaded = e => e.content_loaded ? Promise.resolve() :
new Promise(c => e.addEventListener("DOMContentLoaded", c, {once: true}));
wait_loaded(document).then(() => document.content_loaded = true);
function extract_cookie_policy(cookie, min_time)
{
let best_result = {time: -1};
let policy = null;
const extracted_signatures = [];
for (const match of cookie.matchAll(/haketilo-(\w*)=([^;]*)/g)) {
const new_result = extract_signed(...match.slice(1, 3));
if (new_result.fail)
continue;
extracted_signatures.push(match[1]);
if (new_result.time < Math.max(min_time, best_result.time))
continue;
/* This should succeed - it's our self-produced valid JSON. */
const new_policy = JSON.parse(decodeURIComponent(new_result.data));
if (new_policy.url !== document.URL)
continue;
best_result = new_result;
policy = new_policy;
}
return [policy, extracted_signatures];
}
function extract_url_policy(url, min_time)
{
const [base_url, payload, anchor] =
/^([^#]*)#?([^#]*)(#?.*)$/.exec(url).splice(1, 4);
const match = /^haketilo_([^_]+)_(.*)$/.exec(payload);
if (!match)
return [null, url];
const result = extract_signed(...match.slice(1, 3));
if (result.fail)
return [null, url];
const original_url = base_url + anchor;
const policy = result.time < min_time ? null :
JSON.parse(decodeURIComponent(result.data));
return [policy.url === original_url ? policy : null, original_url];
}
function employ_nonhttp_policy(policy)
{
if (!policy.allow)
return;
policy.nonce = gen_nonce();
const [base_url, target] = /^([^#]*)(#?.*)$/.exec(policy.url).slice(1, 3);
const encoded_policy = encodeURIComponent(JSON.stringify(policy));
const payload = "haketilo_" +
sign_data(encoded_policy, new Date().getTime()).join("_");
const resulting_url = `${base_url}#${payload}${target}`;
location.href = resulting_url;
location.reload();
}
/*
* In the case of HTML documents:
* 1. When injecting some payload we need to sanitize CSP tags before
* they reach the document.
* 2. Only tags inside
are considered valid by the browser and
* need to be considered.
* 3. We want to detach from document, wait until its completes
* loading, sanitize it and re-attach .
* 4. We shall wait for anything to appear in or after and take that as
* a sign has finished loading.
* 5. Otherwise, getting the `DOMContentLoaded' event on the document shall also
* be a sign that is fully loaded.
*/
function make_body_start_observer(DOM_element, waiting)
{
const observer = new MutationObserver(() => try_body_started(waiting));
observer.observe(DOM_element, {childList: true});
return observer;
}
function try_body_started(waiting)
{
const body = waiting.detached_html.querySelector("body");
if ((body && (body.firstChild || body.nextSibling)) ||
waiting.doc.documentElement.nextSibling) {
finish_waiting(waiting);
return true;
}
if (body && waiting.observers.length < 2)
waiting.observers.push(make_body_start_observer(body, waiting));
}
function finish_waiting(waiting)
{
if (waiting.finished)
return;
waiting.finished = true;
waiting.observers.forEach(observer => observer.disconnect());
setTimeout(waiting.callback, 0);
}
function _wait_for_head(doc, detached_html, callback)
{
const waiting = {doc, detached_html, callback, observers: []};
if (try_body_started(waiting))
return;
waiting.observers = [make_body_start_observer(detached_html, waiting)];
wait_loaded(doc).then(() => finish_waiting(waiting));
}
function wait_for_head(doc, detached_html)
{
return new Promise(cb => _wait_for_head(doc, detached_html, cb));
}
const blocked_str = "blocked";
function block_attribute(node, attr, ns=null)
{
const [hasa, geta, seta, rema] = ["has", "get", "set", "remove"]
.map(m => (n, ...args) => typeof ns === "string" ?
n[`${m}AttributeNS`](ns, ...args) : n[`${m}Attribute`](...args));
/*
* Disabling attributes by prepending `-blocked' allows them to still be
* relatively easily accessed in case they contain some useful data.
*/
const construct_name = [attr];
while (hasa(node, construct_name.join("")))
construct_name.unshift(blocked_str);
while (construct_name.length > 1) {
construct_name.shift();
const name = construct_name.join("");
seta(node, `${blocked_str}-${name}`, geta(node, name));
}
rema(node, attr);
}
/*
* Used to disable `