aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--README11
-rw-r--r--bg/ExternalLicenses.js45
-rw-r--r--bg/ResponseProcessor.js35
-rwxr-xr-xbuild.sh15
-rw-r--r--common/Test.js55
-rw-r--r--content/externalLicenseChecker.js2
-rw-r--r--html/common.css12
-rw-r--r--html/display_panel/content/display-panel.html2
-rw-r--r--html/display_panel/content/main_panel.js12
-rw-r--r--html/display_panel/content/panel-styles.css3
-rw-r--r--main_background.js193
-rw-r--r--test/SpecRunner.html43
-rw-r--r--test/resources/app-trilicensed.js1
-rw-r--r--test/resources/index.html38
-rw-r--r--test/resources/jquery.js1
-rw-r--r--test/resources/jslicense.html25
-rw-r--r--test/resources/proprietary.js1
-rw-r--r--test/resources/tracker.js1
-rw-r--r--test/spec/LibreJSSpec.js208
20 files changed, 590 insertions, 114 deletions
diff --git a/.gitignore b/.gitignore
index 9a139c8..3314ced 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,7 @@ SDK LibreJS source/
# dependencies
node_modules/
+test/lib/
# artifacts
librejs.xpi
diff --git a/README b/README
index a380900..f5cf254 100644
--- a/README
+++ b/README
@@ -15,6 +15,11 @@ To build the extension run:
To build the extension plus create a .xpi package run:
$ ./build.sh
+To build the extension including the automated test suite (see TEST below) run:
+ $ ./build.sh -t
+ or
+ $ ./build.sh --test
+
Note: this build.sh script relies on no new source files being created.
@@ -24,6 +29,12 @@ To debug this add-on on IceCat and other Firefox derivatives, browse to the spec
LibreJS should work with other WebExtensions-compliant browsers; but currently, none of them meet the freedom standards of GNU, so no help will be provided for their usage.
+TEST:
+
+An automated test suite runs automatically in its own tab whenever the extension
+is loaded as a "temporary add-on" from about:debugging.
+Otherwise (if included in the xpi, see BUILD above) it can be launched from the
+UI by clicking the [Automated self test...] button.
CONTACT:
diff --git a/bg/ExternalLicenses.js b/bg/ExternalLicenses.js
index 27d9223..33bcbf6 100644
--- a/bg/ExternalLicenses.js
+++ b/bg/ExternalLicenses.js
@@ -26,10 +26,28 @@
"use strict";
let licensesByLabel = new Map();
+let licensesByUrl = new Map();
{
let {licenses} = require("../license_definitions");
- for (let l of Object.values(licenses).filter(l => l.identifier)) {
- licensesByLabel.set(l.identifier, l);
+ let mapByLabel = (label, license) => licensesByLabel.set(label.toUpperCase(), license);
+ for (let [id, l] of Object.entries(licenses)) {
+ let {identifier, canonicalUrl, licenseName} = l;
+ if (identifier) {
+ mapByLabel(identifier, l);
+ } else {
+ l.identifier = id;
+ }
+ if (id !== identifier) {
+ mapByLabel(id, l);
+ }
+ if (licenseName) {
+ mapByLabel(licenseName, l);
+ }
+ if (Array.isArray(canonicalUrl)) {
+ for (let url of canonicalUrl) {
+ licensesByUrl.set(url, l);
+ }
+ }
}
}
@@ -55,23 +73,26 @@ var ExternalLicenses = {
return null;
}
scriptInfo.licenses = new Set();
- scriptInfo.allFree = true;
scriptInfo.toString = function() {
let licenseIds = [...this.licenses].map(l => l.identifier).sort().join(", ");
return licenseIds
- ? (this.allFree ? `Free license${this.licenses.length > 1 ? "s" : ""} (${licenseIds})`
- : `Mixed free (${licenseIds}) and unknown licenses`)
+ ? `Free license${this.licenses.size > 1 ? "s" : ""} (${licenseIds})`
: "Unknown license(s)";
}
-
- for (let {label} of scriptInfo.licenseLinks) {
- if (licensesByLabel.has(label)) {
- scriptInfo.licenses.add(licensesByLabel.get(label));
- } else {
- scriptInfo.allFree = false;
- break;
+ let match = (map, key) => {
+ if (map.has(key)) {
+ scriptInfo.licenses.add(map.get(key));
+ return true;
}
+ return false;
+ };
+
+ for (let {label, url} of scriptInfo.licenseLinks) {
+ match(licensesByLabel, label = label.trim().toUpperCase()) ||
+ match(licensesByUrl, url) ||
+ match(licensesByLabel, label.replace(/^GNU-|-(?:OR-LATER|ONLY)$/, ''));
}
+ scriptInfo.free = scriptInfo.licenses.size > 0;
return scriptInfo;
},
diff --git a/bg/ResponseProcessor.js b/bg/ResponseProcessor.js
index 1af8cd7..4443d90 100644
--- a/bg/ResponseProcessor.js
+++ b/bg/ResponseProcessor.js
@@ -33,7 +33,7 @@ class ResponseProcessor {
static install(handler, types = ["main_frame", "sub_frame", "script"]) {
if (listeners.has(handler)) return false;
- let listener =
+ let listener =
async request => await new ResponseTextFilter(request).process(handler);
listeners.set(handler, listener);
webRequestEvent.addListener(
@@ -80,7 +80,7 @@ class ResponseTextFilter {
if (handler.post) handler = handler.post;
if (typeof handler !== "function") ResponseProcessor.ACCEPT;
}
-
+
let {requestId, responseHeaders} = request;
let filter = browser.webRequest.filterResponseData(requestId);
let buffer = [];
@@ -90,11 +90,29 @@ class ResponseTextFilter {
};
filter.onstop = async event => {
- let decoder = metaData.createDecoder();
+
let params = {stream: true};
- response.text = buffer.map(
- chunk => decoder.decode(chunk, params))
- .join('');
+ // concatenate chunks
+ let size = buffer.reduce((sum, chunk, n) => sum + chunk.byteLength, 0)
+ let allBytes = new Uint8Array(size);
+ let pos = 0;
+ for (let chunk of buffer) {
+ allBytes.set(new Uint8Array(chunk), pos);
+ pos += chunk.byteLength;
+ }
+ buffer = null; // allow garbage collection
+ if (allBytes.indexOf(0) !== -1) {
+ console.debug("Warning: zeroes in bytestream, probable cached encoding mismatch.", request);
+ if (request.type === "script") {
+ console.debug("It's a script, trying to refetch it.");
+ response.text = await (await fetch(request.url, {cache: "reload", credentials: "include"})).text();
+ } else {
+ console.debug("It's a %s, trying to decode it as UTF-16.", request.type);
+ response.text = new TextDecoder("utf-16be").decode(allBytes);
+ }
+ } else {
+ response.text = metaData.createDecoder().decode(allBytes, {stream: true});
+ }
let editedText = null;
try {
editedText = await handler(response);
@@ -108,10 +126,9 @@ class ResponseTextFilter {
filter.write(new TextEncoder().encode(editedText));
} else {
// ... otherwise pass all the raw bytes through
- for (let chunk of buffer) filter.write(chunk);
+ filter.write(allBytes);
}
-
- filter.disconnect();
+ filter.close();
}
return metaData.forceUTF8() ? {responseHeaders} : ResponseProcessor.ACCEPT;;
diff --git a/build.sh b/build.sh
index 1e07fcb..0cd41d7 100755
--- a/build.sh
+++ b/build.sh
@@ -1,6 +1,16 @@
PATH=$PATH:./node_modules/.bin
which browserify > /dev/null || (echo "can not find browserify" && false) || exit
+# prepare / update testing environment if needed
+JASMINE_VER=3.2.1
+JASMINE_LIB="test/lib/jasmine-$JASMINE_VER"
+if ! [ -d "$JASMINE_LIB" ]; then
+ mkdir -p test/lib
+ rm -rf test/lib/jasmine-*
+ JASMINE_URL="https://github.com/jasmine/jasmine/releases/download/v$JASMINE_VER/jasmine-standalone-$JASMINE_VER.zip"
+ curl -L -o "$JASMINE_LIB.zip" "$JASMINE_URL" && unzip -d test/ $JASMINE_LIB.zip lib/**
+ rm -f "$JASMINE_LIB.zip"
+fi
# Build the main file
browserify main_background.js -o bundle.js
@@ -9,7 +19,10 @@ browserify main_background.js -o bundle.js
mkdir ./build_temp
# Move source files to temp directory
-cp -r icons ./build_temp
+if [ "$1" == "-t" -o "$1" == "--test" ]; then
+ cp -r ./test ./build_temp
+fi
+cp -r ./icons ./build_temp
cp -r ./html ./build_temp
cp -r ./content ./build_temp
cp -r ./common ./build_temp
diff --git a/common/Test.js b/common/Test.js
new file mode 100644
index 0000000..e272d1e
--- /dev/null
+++ b/common/Test.js
@@ -0,0 +1,55 @@
+/**
+* GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+*
+* Copyright (C) 2018 Giorgio Maone <giorgio@maone.net>
+*
+* 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 <http://www.gnu.org/licenses/>.
+*/
+
+"use strict";
+var Test = (() => {
+ const RUNNER_URL = browser.extension.getURL("/test/SpecRunner.html");
+ return {
+ /*
+ returns RUNNER_URL if it's a test-enabled build or an about:debugging
+ temporary extension session, null otherwise
+ */
+ async getURL() {
+ let url = RUNNER_URL;
+ try {
+ await fetch(url);
+ } catch (e) {
+ url = null;
+ }
+ this.getURL = () => url;
+ return url;
+ },
+
+ async getTab(activate = false) {
+ let url = await this.getURL();
+ let tab = url ? (await browser.tabs.query({url}))[0] ||
+ (await browser.tabs.create({url}))
+ : null;
+ if (tab && activate) {
+ await browser.tabs.update(tab.id, {active: true});
+ }
+ return tab;
+ }
+ };
+})();
+if (typeof module === "object") {
+ module.exports = Test;
+}
diff --git a/content/externalLicenseChecker.js b/content/externalLicenseChecker.js
index 181e3f9..516057e 100644
--- a/content/externalLicenseChecker.js
+++ b/content/externalLicenseChecker.js
@@ -79,7 +79,7 @@
async checkLicensedScript(m) {
let {url, cache} = m;
if (!licensedScripts) licensedScripts = await fetchLicenseInfo(cache);
- return licensedScripts.get(url);
+ return licensedScripts.get(url) || licensedScripts.get(url.replace(/\?.*/, ''));
}
}
diff --git a/html/common.css b/html/common.css
index cf2c5d1..14931fa 100644
--- a/html/common.css
+++ b/html/common.css
@@ -3,6 +3,7 @@ html {
margin:0px;
color:#000 !important;
background:url('background-panel.png') !important;
+ font-family: sans-serif;
}
body {
padding:0;
@@ -26,4 +27,15 @@ h1.libre {
font-weight:bold;
background:url('librejs-title.png') no-repeat top left;
text-indent:-1000px;
+ position: relative;
+}
+h1.libre span {
+ font-family: sans-serif;
+ position: absolute;
+ bottom: 32px;
+ padding: 0 16px;
+ left: 230px;
+ display: block;
+ text-indent: 0;
+ vertical-align: bottom;
}
diff --git a/html/display_panel/content/display-panel.html b/html/display_panel/content/display-panel.html
index df153b3..80f4cb9 100644
--- a/html/display_panel/content/display-panel.html
+++ b/html/display_panel/content/display-panel.html
@@ -53,6 +53,7 @@
<button id="complain">Complain to site owner</button>
<button id="report-tab">Show this report in a new tab</button>
<button id="open-options">Settings...</button>
+ <button id="autotest">Automated self test...</button>
</div>
</div>
<div id="info">
@@ -100,5 +101,6 @@
</div>
</div>
</body>
+<script src="/common/Test.js"></script>
<script src="main_panel.js"></script>
</html>
diff --git a/html/display_panel/content/main_panel.js b/html/display_panel/content/main_panel.js
index c55b167..2509545 100644
--- a/html/display_panel/content/main_panel.js
+++ b/html/display_panel/content/main_panel.js
@@ -38,6 +38,18 @@ myPort.postMessage({"update": true, tabId: parseInt(currentReport && currentRepo
// Display the actual extension version Number
document.querySelector("#version").textContent = browser.runtime.getManifest().version;
+// Enable autotest button if this is a test-enabled build / session
+(async () => {
+ if (await Test.getURL()) {
+ let button = document.querySelector("#autotest");
+ button.style.display = "block";
+ button.onclick = async () => {
+ await Test.getTab(true);
+ close();
+ }
+ }
+})();
+
var liTemplate = document.querySelector("#li-template");
liTemplate.remove();
diff --git a/html/display_panel/content/panel-styles.css b/html/display_panel/content/panel-styles.css
index 0d849e4..940a8ff 100644
--- a/html/display_panel/content/panel-styles.css
+++ b/html/display_panel/content/panel-styles.css
@@ -141,7 +141,8 @@ span.blocked {
display: none;
}
-.tab #must-reload, .tab #buttons, .empty #buttons {
+.tab #must-reload, .tab #buttons,
+.empty #complain, .empty #report-tab, #autotest {
display: none;
}
diff --git a/main_background.js b/main_background.js
index a145cc5..5c2e8df 100644
--- a/main_background.js
+++ b/main_background.js
@@ -599,6 +599,8 @@ function full_evaluate(script){
* This can only determine if a script is bad, not if it's good
*
* If it passes the intitial pass, it runs the full pass and returns the result
+
+* It returns an array of [flag (boolean, false if "bad"), reason (string, human readable report)]
*
*/
function evaluate(script,name){
@@ -636,27 +638,24 @@ function evaluate(script,name){
return [flag,reason];
}
- var final = full_evaluate(script);
-// final[1] = final[1] + "<br>";
- return final;
+ return full_evaluate(script);
}
-function license_valid(matches){
- if(matches.length != 4){
- return [false, "malformed or unrecognized license tag"];
- }
- if(matches[1] != "@license"){
- return [false, "malformed or unrecognized license tag"];
+function validateLicense(matches) {
+ if (!(Array.isArray(matches) && matches.length === 4)){
+ return [false, "Malformed or unrecognized license tag."];
}
- if(licenses[matches[3]] === undefined){
- return [false, "malformed or unrecognized license tag"];
+ let [all, tag, link, id] = matches;
+ let license = licenses[id];
+ if(!license){
+ return [false, `Unrecognized license "${id}"`];
}
- if(licenses[matches[3]]["Magnet link"] != matches[2]){
- return [false, "malformed or unrecognized license tag"];
+ if(license["Magnet link"] != link){
+ return [false, `License magnet link does not match for "${id}".`];
}
- return [true,"Recognized license as '"+matches[3]+"'<br>"];
+ return [true, `Recognized license: "${id}".`];
}
/**
*
@@ -669,91 +668,89 @@ function license_valid(matches){
* reason text
* ]
*/
-function license_read(script_src, name, external = false){
-
- var reason_text = "";
-
- var edited_src = "";
- var unedited_src = script_src;
- var nontrivial_status;
- var parts_denied = false;
- var parts_accepted = false;
- var license = legacy_license_lib.check(script_src);
- if(license != false){
- return [true,script_src,"Licensed under: "+license];
+function license_read(scriptSrc, name, external = false){
+
+ let license = legacy_license_lib.check(scriptSrc);
+ if (license){
+ return [true, scriptSrc, `Licensed under: ${license}`];
}
- if (listManager.builtInHashes.has(hash(script_src))){
- return [true,script_src,"Common script known to be free software."];
+ if (listManager.builtInHashes.has(hash(scriptSrc))){
+ return [true, scriptSrc, "Common script known to be free software."];
}
- while(true){ // TODO: refactor me
- // TODO: support multiline comments
- var matches = /\/[\/\*]\s*?(@license)\s([\S]+)\s([\S]+$)/gm.exec(unedited_src);
- var empty = /[^\s]/gm.exec(unedited_src);
- if(empty == null){
- return [true,edited_src,reason_text];
+
+ let editedSrc = "";
+ let uneditedSrc = scriptSrc.trim();
+ let reason = uneditedSrc ? "" : "Empty source.";
+ let partsDenied = false;
+ let partsAccepted = false;
+
+ function checkTriviality(s) {
+ if (!s.trim()) {
+ return true; // empty, ignore it
}
- if(matches == null){
- if (external)
- return [false,edited_src,"External script with no known license."];
- else
- nontrivial_status = evaluate(unedited_src,name);
- if(nontrivial_status[0] == true){
- parts_accepted = true;
- edited_src += unedited_src;
- } else{
- parts_denied = true;
- edited_src += "\n/*\nLIBREJS BLOCKED:"+nontrivial_status[1]+"\n*/\n";
- }
- reason_text += "\n" + nontrivial_status[1];
+ let [trivial, message] = external ?
+ [false, "External script with no known license"]
+ : evaluate(s, name);
+ if (trivial) {
+ partsAccepted = true;
+ editedSrc += s;
+ } else {
+ partsDenied = true;
+ editedSrc += `\n/*\nLIBREJS BLOCKED: ${message}\n*/\n`;
+ }
+ reason += `\n${message}`;
+ return trivial;
+ }
- if(parts_denied == true && parts_accepted == true){
- reason_text = "Script was determined partly non-trivial after editing. (check source for details)\n"+reason_text;
- }
- if(parts_denied == true && parts_accepted == false){
- return [false,edited_src,reason_text];
- }
- else return [true,edited_src,reason_text];
+ while (uneditedSrc) {
+ let openingMatch = /\/[\/\*]\s*?(@license)\s+(\S+)\s+(\S+)\s*$/mi.exec(uneditedSrc);
+ if (!openingMatch) { // no license found, check for triviality
+ checkTriviality(uneditedSrc);
+ break;
+ }
+ let openingIndex = openingMatch.index;
+ if (openingIndex) {
+ // let's check the triviality of the code before the license tag, if any
+ checkTriviality(uneditedSrc.substring(0, openingIndex));
}
- // sponge
- dbg_print("undedited_src:");
- dbg_print(unedited_src);
- dbg_print(matches);
- dbg_print("chopping at " + matches["index"] + ".");
- var before = unedited_src.substring(0,matches["index"]);
- // sponge
- dbg_print("before:");
- dbg_print(before);
- if (external)
- nontrivial_status = [true, "External script with no known license"]
- else
- nontrivial_status = evaluate(before,name);
- if(nontrivial_status[0] == true){
- parts_accepted = true;
- edited_src += before;
- } else{
- parts_denied = true;
- edited_src += "\n/*\nLIBREJS BLOCKED:"+nontrivial_status[1]+"\n*/\n";
+ // let's check the actual license
+ uneditedSrc = uneditedSrc.substring(openingIndex);
+
+ let closureMatch = /\/([*/])\s*@license-end\b[^*/\n]*/i.exec(uneditedSrc);
+ if (!closureMatch) {
+ let msg = "ERROR: @license with no @license-end";
+ return [false, `\n/*\n ${msg} \n*/\n`, msg];
}
- unedited_src = unedited_src.substr(matches["index"],unedited_src.length);
- // TODO: support multiline comments
- var matches_end = /\/\/\s*?(@license-end)/gm.exec(unedited_src);
- if(matches_end == null){
- dbg_print("ERROR: @license with no @license-end");
- return [false,"\n/*\n ERROR: @license with no @license-end \n*/\n","ERROR: @license with no @license-end"];
+
+ let closureEndIndex = closureMatch.index + closureMatch[0].length;
+ let commentEndOffset = uneditedSrc.substring(closureEndIndex).indexOf(closureMatch[1] === "*" ? "*/" : "\n");
+ if (commentEndOffset !== -1) {
+ closureEndIndex += commentEndOffset;
}
- var endtag_end_index = matches_end["index"]+matches_end[0].length;
- var license_res = license_valid(matches);
- if(license_res[0] == true){
- edited_src = edited_src + unedited_src.substr(0,endtag_end_index);
- reason_text += "\n" + license_res[1];
- } else{
- edited_src = edited_src + "\n/*\n"+license_res[1]+"\n*/\n";
- reason_text += "\n" + license_res[1];
+
+ let [licenseOK, message] = validateLicense(openingMatch);
+ if(licenseOK) {
+ editedSrc += uneditedSrc.substr(0, closureEndIndex);
+ partsAccepted = true;
+ } else {
+ editedSrc += `\n/*\n${message}\n*/\n`;
+ partsDenied = true;
}
+ reason += `\n${message}`;
+
// trim off everything we just evaluated
- unedited_src = unedited_src.substr(endtag_end_index,unedited_src.length);
+ uneditedSrc = uneditedSrc.substring(closureEndIndex).trim();
+ }
+
+ if(partsDenied) {
+ if (partsAccepted) {
+ reason = `Some parts of the script have been disabled (check the source for details).\n^--- ${reason}`;
+ }
+ return [false, editedSrc, reason];
}
+
+ return [true, scriptSrc, reason];
}
/* *********************************************************************************************** */
@@ -862,6 +859,7 @@ var ResponseHandler = {
let {request} = response;
let {url, type, tabId, frameId, documentUrl} = request;
+ let fullUrl = url;
url = ListStore.urlItem(url);
let site = ListStore.siteItem(url);
@@ -891,11 +889,11 @@ var ResponseHandler = {
"whitelisted": [url, whitelistedSite ? `User whitelisted ${site}` : "Whitelisted by user"]});
return ResponseProcessor.ACCEPT;
} else {
- let scriptInfo = await ExternalLicenses.check({url, tabId, frameId, documentUrl});
+ let scriptInfo = await ExternalLicenses.check({url: fullUrl, tabId, frameId, documentUrl});
if (scriptInfo) {
let verdict, ret;
let msg = scriptInfo.toString();
- if (scriptInfo.allFree) {
+ if (scriptInfo.free) {
verdict = "accepted";
ret = ResponseProcessor.ACCEPT;
} else {
@@ -1177,6 +1175,21 @@ async function init_addon() {
ResponseProcessor.install(ResponseHandler);
legacy_license_lib.init();
+
+
+ let Test = require("./common/Test");
+ if (Test.getURL()) {
+ // export testable functions to the global scope
+ this.LibreJS = {
+ editHtml,
+ handle_script,
+ ExternalLicenses,
+ };
+ // create or focus the autotest tab if it's a debugging session
+ if ((await browser.management.getSelf()).installType === "development") {
+ Test.getTab(true);
+ }
+ }
}
diff --git a/test/SpecRunner.html b/test/SpecRunner.html
new file mode 100644
index 0000000..42a372f
--- /dev/null
+++ b/test/SpecRunner.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html>
+<!--
+/**
+* GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+*
+* Copyright (C) 2018 Giorgio Maone <giorgio@maone.net>
+*
+* 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 <http://www.gnu.org/licenses/>.
+*/
+-->
+<head>
+ <meta charset="utf-8">
+ <title>LibreJS Tests</title>
+
+ <link rel="shortcut icon" type="image/png" href="/icons/librejs.png">
+ <link rel="stylesheet" href="lib/jasmine-3.2.1/jasmine.css">
+ <link rel="stylesheet" href="/html/common.css">
+ <script src="lib/jasmine-3.2.1/jasmine.js"></script>
+ <script src="lib/jasmine-3.2.1/jasmine-html.js"></script>
+ <script src="lib/jasmine-3.2.1/boot.js"></script>
+
+ <script src="spec/LibreJSSpec.js"></script>
+
+</head>
+
+<body>
+ <h1 class="libre">LibreJS <span>Autotest</span></h1>
+</body>
+</html>
diff --git a/test/resources/app-trilicensed.js b/test/resources/app-trilicensed.js
new file mode 100644
index 0000000..7b2c2ea
--- /dev/null
+++ b/test/resources/app-trilicensed.js
@@ -0,0 +1 @@
+document.write(`<p>Executing ${document.currentScript.src}</p>`);
diff --git a/test/resources/index.html b/test/resources/index.html
new file mode 100644
index 0000000..c5f364c
--- /dev/null
+++ b/test/resources/index.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html>
+<!--
+/**
+* GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+*
+* Copyright (C) 2018 Giorgio Maone <giorgio@maone.net>
+*
+* 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 <http://www.gnu.org/licenses/>.
+*/
+-->
+<head>
+ <meta charset="utf-8">
+ <title>LibreJS test document</title>
+ <link rel="shortcut icon" type="image/png" href="/icons/librejs.png">
+</head>
+<body>
+<h1>LibreJS test document</h1>
+<a href="jslicense.html" rel="jslicense">JavaScript license information</a>
+<script src="jquery.js"></script>
+<script src="app-trilicensed.js"></script>
+<script src="proprietary.js"></script>
+<script src="tracker.js"></script>
+</body>
+</html>
diff --git a/test/resources/jquery.js b/test/resources/jquery.js
new file mode 100644
index 0000000..7b2c2ea
--- /dev/null
+++ b/test/resources/jquery.js
@@ -0,0 +1 @@
+document.write(`<p>Executing ${document.currentScript.src}</p>`);
diff --git a/test/resources/jslicense.html b/test/resources/jslicense.html
new file mode 100644
index 0000000..0cd0449
--- /dev/null
+++ b/test/resources/jslicense.html
@@ -0,0 +1,25 @@
+<table id="jslicense-labels1">
+<tr>
+<td><a href="jquery.js">Fake jQuery</a></td>
+<td><a href="http://www.jclark.com/xml/copying.txt">Expat</a>
+<td><a href="jquery-1.7.tar.gz">jquery-1.7.tar.gz</a></td>
+</tr>
+<tr>
+<td><a href="app-trilicensed.js">App specific script, tri-licensed (2 free and 1 proprietary license)</a></td>
+<td>
+<a href="magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt">GNU V3</a>
+<br>
+<a href="http://www.jclark.com/xml/copying.txt">Expat</a>
+<br>
+<a href="https://evil.com/someproprietarylicense">Proprietary, optional</a></td>
+</td>
+<td><a href="app-trilicensed.js">app-trilicensed.js</a></td>
+</tr>
+<tr>
+<td><a href="proprietary.js">App specific script, proprietary only</a></td>
+<td>
+<a href="https://evil.com/someproprietarylicense">Proprietary</a></td>
+</td>
+<td><a href="proprietary.js">proprietary.js</a></td>
+</tr>
+</table>
diff --git a/test/resources/proprietary.js b/test/resources/proprietary.js
new file mode 100644
index 0000000..7b2c2ea
--- /dev/null
+++ b/test/resources/proprietary.js
@@ -0,0 +1 @@
+document.write(`<p>Executing ${document.currentScript.src}</p>`);
diff --git a/test/resources/tracker.js b/test/resources/tracker.js
new file mode 100644
index 0000000..7b2c2ea
--- /dev/null
+++ b/test/resources/tracker.js
@@ -0,0 +1 @@
+document.write(`<p>Executing ${document.currentScript.src}</p>`);
diff --git a/test/spec/LibreJSSpec.js b/test/spec/LibreJSSpec.js
new file mode 100644
index 0000000..3d61973
--- /dev/null
+++ b/test/spec/LibreJSSpec.js
@@ -0,0 +1,208 @@
+/*
+* GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+*
+* Copyright (C) 2018 Giorgio Maone <giorgio@maone.net>
+*
+* 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 <http://www.gnu.org/licenses/>.
+*/
+"use strict";
+
+describe("LibreJS' components", () => {
+ let LibreJS = browser.extension.getBackgroundPage().LibreJS;
+ let license = {
+ id: 'GPL-3.0',
+ url: 'http://www.gnu.org/licenses/gpl-3.0.html',
+ magnet: 'magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt',
+ };
+ let unknownLicense = {
+ id: 'Acme-proprietary-1.5',
+ url: 'http://www.acme.com/license-1.5.html',
+ magnet: 'magnet:?xt=urn:btih:2f739d935676111cfff4b4693e3816e664797050&dn=acme-1.5.txt'
+ };
+
+ let trivial = "1+1";
+ let nontrivial = `function nt() { document.documentElement.innerHTML=""; nt(); }`;
+ let licensed = `// @license ${license.magnet} ${license.id}\n${nontrivial}\n// @license-end`;
+ let unknownLicensed = `// @license ${unknownLicense.magnet} ${unknownLicense.id}\n${nontrivial}\n// @license-end`;
+ let malformedLicensed = `// @license\n${nontrivial}`;
+
+ let tab, documentUrl;
+
+ beforeAll(async () => {
+ let url = browser.extension.getURL("/test/resources/index.html");
+ tab = (await browser.tabs.query({url}))[0] || (await browser.tabs.create({url}));
+ documentUrl = url;
+ });
+
+ describe("The external script source processor", () => {
+ let url = "https://www.gnu.org/mock-script.js";
+
+ let processScript = async (source, whitelisted = false) =>
+ await LibreJS.handle_script({
+ text: source,
+ request: {url, tabId: tab.id, documentUrl, frameId: 0},
+ }, whitelisted);
+
+ it("should accept whitelisted scripts", async () => {
+ expect(await processScript(nontrivial, true) || nontrivial).toContain(nontrivial);
+ });
+
+ it("should block trivial scripts too", async () => {
+ let processed = await processScript(trivial);
+ expect(processed || trivial).not.toContain(trivial);
+ });
+
+ it("should block non-trivial scripts", async () => {
+ let processed = await processScript(nontrivial);
+ expect(processed || nontrivial).not.toContain(nontrivial);
+ });
+
+ it("should accept scripts with known free license tags", async () => {
+ let processed = await processScript(licensed);
+ expect(processed || licensed).toContain(nontrivial);
+ });
+
+ it("should block scripts with unknown license tags", async () => {
+ let processed = await processScript(unknownLicensed);
+ expect(processed).not.toContain(nontrivial);
+ });
+
+ it("should block scripts with malformed license tags", async () => {
+ let processed = await processScript(malformedLicensed);
+ expect(processed).not.toContain(nontrivial);
+ });
+ });
+
+ describe("The HTML processor", () => {
+ let processHtml =
+ async (html, whitelisted = false) =>
+ LibreJS.editHtml(html, tab.url, tab.id, 0, whitelisted);
+
+ let addScript = (html, script, before = "</head>") =>
+ html.replace(before, `<script>${script}</script>${before}`);
+
+ function extractScripts(html, def = "") {
+ let matches = html && html.match(/<script>[^]*?<\/script>/g);
+ return matches && matches.join("") || def;
+ }
+
+ let html, nontrivialInHtml;
+ beforeAll(async () => {
+ html = (await browser.tabs.executeScript(tab.id, {
+ runAt: "document_start",
+ code: "document.documentElement.outerHTML"
+ }))[0];
+ nontrivialInHtml = addScript(html, nontrivial);
+ });
+
+ it("should not modify scriptless documents", async () => {
+ expect(await processHtml(html)).toBeNull();
+ });
+
+ it("should not modify whitelisted documents", async () => {
+ expect(await processHtml(nontrivialInHtml, true)).toBeNull();
+ });
+
+ it("should accept trivial scripts", async () => {
+ let trivialInHtml = addScript(html, trivial);
+ let processed = await processHtml(trivialInHtml);
+ expect(extractScripts(processed, trivial)).toContain(trivial);
+ });
+
+ it("should block non-trivial scripts", async () => {
+ let processed = await processHtml(nontrivialInHtml);
+ expect(extractScripts(processed, nontrivial)).not.toContain(nontrivial);
+ });
+
+ it("should accept scripts with known free license tags", async () => {
+ let licensedInHtml = addScript(html, licensed);
+ let processed = await processHtml(licensedInHtml);
+ expect(extractScripts(processed, licensed)).toContain(nontrivial);
+ });
+
+ it("should block scripts with unknown license tags", async () => {
+ let unknownInHtml = addScript(html, unknownLicensed);
+ let processed = await processHtml(unknownInHtml);
+ expect(extractScripts(processed, nontrivial)).not.toContain(nontrivial);
+ });
+
+ it("should block scripts with malformed license tags", async () => {
+ let malformedInHtml = addScript(html, malformedLicensed);
+ let processed = await processHtml(malformedInHtml);
+ expect(extractScripts(processed, nontrivial)).not.toContain(nontrivial);
+ });
+
+ it("should accept scripts on globally licensed pages", async () => {
+ let globalLicense = `/* @licstart The following is the entire license notice
+ for the JavaScript code in this page.
+ -- Some free license --
+ @licend The above is the entire license notice for the JavaScript code in this page. */`;
+
+ let licensed = addScript(nontrivialInHtml, globalLicense, "<script>");
+ let processed = await processHtml(html);
+ expect(extractScripts(processed, licensed)).toContain(nontrivial);
+ });
+
+ it("should discriminate trivial, non-trivial and licensed mixed on the same page", async () => {
+ let mixedPage = addScript(addScript(nontrivialInHtml, trivial), licensed);
+ let processed = await processHtml(mixedPage);
+ expect(processed).not.toBeNull();
+ let scripts = extractScripts(processed, nontrivial);
+ expect(scripts).toContain(trivial);
+ expect(scripts).toContain(licensed);
+ expect(scripts.replace(licensed, "")).not.toContain(nontrivial);
+ });
+ });
+
+ describe("The external (Web Labels) license checker", () => {
+ let {ExternalLicenses} = LibreJS;
+ let check;
+
+ beforeAll(async () => {
+ let args = {tabId: tab.id, frameId: 0, documentUrl};
+ let resolve = url => new URL(url, documentUrl).href;
+ check = async url => await ExternalLicenses.check(Object.assign({url: resolve(url)}, args));
+ await browser.tabs.executeScript(tab.id, {
+ file: "/content/externalLicenseChecker.js"
+ });
+ });
+
+ it("should recognize free licenses", async () => {
+ let scriptInfo = await check("jquery.js");
+ console.debug(scriptInfo);
+ expect(scriptInfo.free).toBeTruthy();
+ });
+ it("should accept scripts if any of multiple licenses is free", async () => {
+ let scriptInfo = await check("app-trilicensed.js");
+ console.debug(scriptInfo);
+ expect(scriptInfo.free).toBeTruthy();
+ });
+ it("should block scripts declaring only proprietary license(s)", async () => {
+ let scriptInfo = await check("proprietary.js");
+ console.debug(scriptInfo);
+ expect(scriptInfo.free).toBeFalsy();
+ });
+ it("should block scripts not declaring any license", async () => {
+ let scriptInfo = await check("tracker.js");
+ console.debug(scriptInfo);
+ expect(scriptInfo).toBeNull();
+ });
+ });
+ afterAll(async () => {
+ await browser.tabs.remove(tab.id);
+ browser.tabs.update((await browser.tabs.getCurrent()).id, {active: true});
+ });
+});