/**
* GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
* *
* Copyright (C) 2017 Nathan Nichols, Loic J. Duros, Nik Nyby
* Copyright (C) 2018 Giorgio Maone
* Copyright (C) 2022 Yuchen Pei
*
* This file is part of GNU LibreJS.
*
* GNU LibreJS 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.
*
* GNU LibreJS 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.
*
* You should have received a copy of the GNU General Public License
* along with GNU LibreJS. If not, see .
*/
// TO TEST THE CONTACT FINDER:
// - open the manifest.json
// - add a comma after the closing bracket of the key "background"
// - Copy and paste this after it:
/*
"content_scripts": [{
"matches": [""],
"js": ["/content/contactFinder.js"],
"css": ["/content/contactFinder.css"]
}]
*/
// Now, the contact finder will load on every page and you can test it where ever you want.
//*********************************************************************************************
function debug(format, ...args) {
console.debug(`LibreJS - ${format}`, ...args);
}
debug("Injecting contact finder in %s", document.URL);
/**
* contactSearchStrings
* Contains arrays of strings classified by language
* and by degree of certainty.
*/
function contactFrags() {
return [
// de
{
'certain': [
'^[\\s]*Kontakt os[\\s]*$',
'^[\\s]*Email Os[\\s]*$',
'^[\\s]*Kontakt[\\s]*$'
],
'probable': ['^[\\s]Kontakt', '^[\\s]*Email'],
'uncertain': [
'^[\\s]*Om Us',
'^[\\s]*Om',
'Hvem vi er'
]
},
// en
{
'certain': [
'^[\\s]*Contact Us[\\s]*$',
'^[\\s]*Email Us[\\s]*$',
'^[\\s]*Contact[\\s]*$',
'^[\\s]*Feedback[\\s]*$',
'^[\\s]*Web.?site Feedback[\\s]*$'
],
'probable': ['^[\\s]*Contact', '^[\\s]*Email'],
'uncertain': [
'^[\\s]*About Us',
'^[\\s]*About',
'Who we are',
'Who I am',
'Company Info',
'Customer Service'
]
},
// es
{
'certain': [
'^[\\s]*contáctenos[\\s]*$',
'^[\\s]*Email[\\s]*$'
],
'probable': ['^[\\s]contáctenos', '^[\\s]*Email'],
'uncertain': [
'Acerca de nosotros'
]
},
// fr
{
'certain': [
'^[\\s]*Contactez nous[\\s]*$',
'^[\\s]*(Nous )?contacter[\\s]*$',
'^[\\s]*Email[\\s]*$',
'^[\\s]*Contact[\\s]*$',
'^[\\s]*Commentaires[\\s]*$'
],
'probable': ['^[\\s]Contact', '^[\\s]*Email'],
'uncertain': [
'^[\\s]*(A|À) propos',
'Qui nous sommes',
'Qui suis(-| )?je',
'Info',
'Service Client(e|è)le'
]
}
];
}
function contactLinkLimit() {
return 5;
}
// Taken from http://emailregex.com/
function emailRegex() {
return new RegExp(/(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/g);
}
//*********************************************************************************************
function findMatch(link, frag) {
const result = (link.innerText.match(new RegExp(frag, "g")) || []).filter(x => typeof x == "string");
if (result.length) return true;
return false;
}
/**
* Tests all links on the page for regexes under a certain certainty level.
*
* Will return either all regex matches from the selected certainty level,
* up to a limit.
*
* certainty can be "certain" > "probable" > "uncertain"
*/
function attempt(certainty, limit) {
// There needs to be some kind of max so that people can't troll by for example leaving a comment with a bunch of emails
// to cause LibreJS users to slow down.
const matches = [];
const links = Array.from(document.links).filter(link => (typeof (link.innerText) === "string" || typeof (link.href) === "string"));
for (const link of links) {
for (const byLevel of contactFrags()) {
for (const frag of byLevel[certainty]) {
findMatch(link, frag) && matches.push(link);
if (matches.length >= limit) return { 'fail': false, 'result': [link] };
}
}
}
return { "fail": matches.length === 0, "result": matches };
}
/**
* "LibreJS detects contact pages and email addresses that are likely to be owned by the
* maintainer of the site."
*/
function findContacts() {
for (const type of ["certain", "probable", "uncertain"]) {
const attempted = attempt(type, contactLinkLimit());
if (!attempted["fail"]) {
return [type, attempted["result"]];
}
}
return null;
}
function createWidget(id, tag, parent = document.body) {
const oldWidget = document.getElementById(id);
if (oldWidget) oldWidget.remove();
const widget = parent.appendChild(document.createElement(tag));
widget.id = id;
return widget;
}
/**
*
* Creates the contact finder / complain UI as a semi-transparent overlay
*
*/
function main() {
const overlay = createWidget("_LibreJS_overlay", "div");
const frame = createWidget("_LibreJS_frame", "iframe");
const close = () => {
frame.remove();
overlay.remove();
};
// Clicking the "outer area" closes the dialog.
overlay.addEventListener("click", close);
const initFrame = prefs => {
debug("initFrame");
const contentDoc = frame.contentWindow.document;
const addText = (text, tag, wherein) => {
el = wherein.appendChild(contentDoc.createElement(tag));
el.textContent = text;
}
// Header of the dialog
const { body } = contentDoc;
body.id = "_LibreJS_dialog";
addText('LibreJS Complaint', 'h1', body);
const closeButton = body.appendChild(contentDoc.createElement('button'));
closeButton.classList.toggle('close', true);
closeButton.textContent = 'x';
closeButton.addEventListener("click", close);
const content = body.appendChild(contentDoc.createElement("div"));
content.id = "content";
// Add list of contact links
const res = findContacts();
if (!res) {
content.classList.toggle("_LibreJS_fail", true);
addText('Could not guess any contact page for this site.', 'div', content);
} else {
addText('Contact info guessed for this site', 'h3', content);
addText(res[0] + ':', 'span', content);
const list = content.appendChild(contentDoc.createElement("ul"));
for (const link of res[1]) {
const a = contentDoc.createElement("a");
a.href = link.href;
a.textContent = link.textContent;
list.appendChild(contentDoc.createElement("li")).appendChild(a);
}
}
// Add list of emails
const emails = (document.documentElement.textContent.match(emailRegex()) || []).filter(e => !!e);
if (emails.length) {
addText("Possible email addresses:", 'h5', content);
const list = content.appendChild(contentDoc.createElement("ul"));
for (const recipient of emails.slice(0, 10)) {
const a = contentDoc.createElement("a");
a.href = `mailto:${recipient}?subject=${encodeURIComponent(prefs["pref_subject"])
}&body=${encodeURIComponent(prefs["pref_body"])
}`;
a.textContent = recipient;
list.appendChild(contentDoc.createElement("li")).appendChild(a);
}
}
// contentDoc.querySelectorAll(".close, a").forEach(makeCloser);
debug("frame initialized");
}
frame.addEventListener("load", _ => {
debug("frame loaded");
browser.runtime.connect({ name: "contact_finder" }).onMessage.addListener(initFrame);
});
}
main();